Contract — P2.4.1 Capability Gates (derived limits)
Task ID: fcaac02d-62bd-4ecc-8cdd-23984731c787
Branch: feature/p2-4-1-limits
Base: main @ 7e5cf9d3
Date: 2026-05-12
Step: 2 of 5 (audit → contract → packet → implement → verify)
§1. Purpose
Specify the behavioural contract for src/domains/reputation/limits.ts — a pure,
deterministic, integer-only derivation layer over ReputationRow that emits five
capability gates: max_parallel_tasks, rate_limit_bonus, stake_discount,
can_arbitrate, can_govern.
This contract is binding on the implementation in Step 4. Step 5 (verification) asserts every clause via Jest tests.
§2. Module surface
§2.1 Imports (closed set)
import { BUILTINS } from '../rules/builtins.js'; // P1.3.2 — κ isqrt + ilog2
import { bps_mul, safe_mul, safe_div } from '../rules/integer-math.js'; // P1.1.1
import { BPS_100_PERCENT } from '../rules/bps-constants.js'; // P1.1.3
import type { ReputationRow } from './schema.js'; // P2.1.1
No other imports are permitted in limits.ts. No node:*, no fs, no path,
no crypto. Determinism scanner globs catch violations at CI time.
§2.2 Exported functions (closed set, in this order)
max_parallel_tasks(rep_execution: ReputationRow): bigintrate_limit_bonus(rep_execution: ReputationRow, base_rate: bigint): bigintstake_discount(required_stake: bigint, rep_execution: ReputationRow): bigintcan_arbitrate(rep_arbitration: ReputationRow, rep_execution: ReputationRow, current_epoch: bigint): booleancan_govern(rep_governance: ReputationRow, current_epoch: bigint): boolean
No other exports. No default export. No re-exports.
§3. Invariants (binding on every call)
| # | Invariant |
|---|---|
| I1 | Pure: same inputs → byte-identical outputs across calls and processes. |
| I2 | Integer-only: every internal quantity is bigint; BigInt(rep.score) and BigInt(rep.ban_until_epoch) are the only allowed boundary coercions. |
| I3 | No mutation: ReputationRow inputs are treated as deeply readonly; no field is reassigned. |
| I4 | No IO: no DB, no fs, no fetch. |
| I5 | No clock: no Date.*, no process.hrtime, no performance.now. current_epoch is a parameter. |
| I6 | No RNG: no Math.random, no crypto.randomBytes, no crypto.getRandomValues. |
| I7 | No floats: zero number arithmetic. (row.score: number is coerced via BigInt(...) once at the boundary.) |
| I8 | Synchronous: no Promise, no async, no await. |
| I9 | Reference equality not exposed: BUILTINS.get('isqrt') is invoked positionally, never re-exported. |
| I10 | All BPS arithmetic via integer-math.ts; all sqrt/log2 via BUILTINS. No local re-implementation. |
§4. Algorithms (literal, line-by-line)
§4.1 max_parallel_tasks(rep_execution)
let score_b = BigInt(rep_execution.score)
let isqrt = BUILTINS.get('isqrt') as BuiltinFn // P1.3.2 floor(sqrt) for bigint
let raw = isqrt([score_b]) as bigint
return raw < 20n ? raw : 20n
Boundary semantics:
score = 0⇒isqrt(0n) = 0n⇒min(0n, 20n) = 0nscore = 400⇒isqrt(400n) = 20n⇒min(20n, 20n) = 20nscore = 399⇒isqrt(399n) = 19n⇒min(19n, 20n) = 19nscore = 10_000⇒isqrt(10000n) = 100n⇒min(100n, 20n) = 20n
§4.2 rate_limit_bonus(rep_execution, base_rate)
let score_b = BigInt(rep_execution.score)
let safe_score = score_b > 1n ? score_b : 1n // floor at 1 — ilog2(0n) === 0n by κ spec, but we guard explicitly
let ilog2 = BUILTINS.get('ilog2') as BuiltinFn
let log = ilog2([safe_score]) as bigint
return bps_mul(base_rate, log)
Boundary semantics:
score = 0,base_rate = 1_000n⇒ilog2(max(0n, 1n)) = ilog2(1n) = 0n⇒bps_mul(1000n, 0n) = 0nscore = 1,base_rate = 1_000n⇒ilog2(1n) = 0n⇒bps_mul(1000n, 0n) = 0nscore = 1024,base_rate = 1_000n⇒ilog2(1024n) = 10n⇒bps_mul(1000n, 10n) = 1n(floor of 1000×10/10000)score = 10_000,base_rate = 1_000n⇒ilog2(10_000n) = 13n⇒bps_mul(1000n, 13n) = 1n(floor of 1000×13/10000)
§4.3 stake_discount(required_stake, rep_execution)
let score_b = BigInt(rep_execution.score)
let safe_score = score_b > 1000n ? score_b : 1000n // floor at 1000 — prevents runaway discount
let numerator = safe_mul(required_stake, BPS_100_PERCENT)
return safe_div(numerator, safe_score)
Boundary semantics:
stake = 1000n,score = 0⇒safe_mul(1000n, 10000n) / 1000n = 10_000_000n / 1000n = 10_000n(stake × 10)stake = 1000n,score = 999⇒ same: floor activates ⇒10_000n(stake × 10)stake = 1000n,score = 1000⇒ same: at floor ⇒10_000nstake = 1000n,score = 10000⇒10_000_000n / 10000n = 1000n(stake × 1 — no discount)
§4.4 can_arbitrate(rep_arbitration, rep_execution, current_epoch)
if (rep_arbitration.ban_until_epoch !== null) {
let ban_b = BigInt(rep_arbitration.ban_until_epoch)
if (ban_b > current_epoch) return false
}
return BigInt(rep_arbitration.score) >= 5000n && BigInt(rep_execution.score) >= 3000n
Boundary semantics:
arb=4999,exec=3000, no ban ⇒false(arb below threshold)arb=5000,exec=2999, no ban ⇒false(exec below threshold)arb=5000,exec=3000, no ban ⇒true(exact threshold)arb=5000,exec=3000, ban=10, ce=9 ⇒false(ban active, 10 > 9)arb=5000,exec=3000, ban=10, ce=10 ⇒true(ban over, 10 > 10 is false)arb=5000,exec=3000, ban=10, ce=11 ⇒true(ban over)
§4.5 can_govern(rep_governance, current_epoch)
if (rep_governance.ban_until_epoch !== null) {
let ban_b = BigInt(rep_governance.ban_until_epoch)
if (ban_b > current_epoch) return false
}
return BigInt(rep_governance.score) >= 4000n
Boundary semantics:
gov=3999, no ban ⇒falsegov=4000, no ban ⇒truegov=4000, ban=10, ce=9 ⇒false(ban active)gov=4000, ban=10, ce=10 ⇒true(ban over)
§5. Error model
P2.4.1 functions do not throw under any input drawn from the validated ReputationRow schema (P2.1.1). The upstream guarantees are:
score ∈ [0, 10000](validated byReputationRowSchema)scar_bps ∈ [0, 10000](P2.4.1 does not read this field)ban_until_epoch ∈ Z ∪ {null}(validated; the null case is the no-ban path)
Inputs that bypass the schema (TS cast / runtime fabrication) may produce unexpected results — that is the schema’s responsibility, not P2.4.1’s.
Theoretically: safe_mul could throw OverflowError if required_stake ×
BPS_100_PERCENT > INT64_MAX. required_stake ≤ 9.22 × 10^14 keeps it safe;
realistic stake values are «10^12, so this is an unreachable guard in practice.
Step 5 will test the boundary with stake = INT64_MAX / BPS_100_PERCENT - 1 to
confirm the safe path; one-above-safe is left to caller validation.
§6. Acceptance criteria (Step 5 verification asserts each)
- AC-1
max_parallel_tasksreturnsbigint,≤ 20n,≥ 0n,== isqrt(score)forscore ≤ 400,== 20nforscore ≥ 400. - AC-2
rate_limit_bonusreturnsbigint,≥ 0nfor non-negativebase_rate; returns0nwhenscore ∈ [0, 1]. - AC-3
stake_discountreturnsbigint,≥ stake / 10nforscore ≤ 1000(floor active),≤ stakeforscore ≥ BPS_100_PERCENT. - AC-4
can_arbitrateis true iffarb ≥ 5000n ∧ exec ≥ 3000n ∧ ¬banned. Banned iffban_until_epoch !== null ∧ BigInt(ban) > current_epoch. - AC-5
can_governis true iffgov ≥ 4000n ∧ ¬banned. Same ban semantics as AC-4. - AC-6 All five functions are pure (idempotent across calls with the same inputs; no side effects).
- AC-7 Determinism scanner clean on
limits.ts— noMath.*,Date.*,Math.random,setTimeout, etc. - AC-8 ESLint clean on
limits.tsandlimits.test.ts— no unused imports, noany, no implicit casts. - AC-9
npm run build && npm run lint && npm testexit 0 with +15–25 tests added net of baseline 2478.
§7. Test matrix (Step 5 will implement)
Required boundary cases (15 mandatory + ≥ 5 defensive = ≥ 20 total):
max_parallel_tasks:
- rep_execution.score = 0 → 0n
- rep_execution.score = 399 → 19n
- rep_execution.score = 400 → 20n (exact cap boundary)
- rep_execution.score = 401 → 20n (cap active; isqrt(401) = 20)
- rep_execution.score = 10000 → 20n (cap active; isqrt(10000) = 100, capped)
rate_limit_bonus(base_rate=1000n):
- rep_execution.score = 0 → 0n (max(0,1) = 1; ilog2(1) = 0; bps_mul(1000, 0) = 0)
- rep_execution.score = 1 → 0n (ilog2(1) = 0)
- rep_execution.score = 1024 → 1n (ilog2(1024) = 10; bps_mul(1000, 10) = 1)
- rep_execution.score = 10000 → 1n (ilog2(10000) = 13; bps_mul(1000, 13) = 1)
- base_rate = 100000n, score = 1024 → 100n (bps_mul(100000, 10) = 100)
stake_discount:
- stake = 1000n, score = 0 → 10000n (floor active; stake × 10)
- stake = 1000n, score = 999 → 10000n (floor still active)
- stake = 1000n, score = 1000 → 10000n (at floor; stake × 10)
- stake = 1000n, score = 5000 → 2000n (stake × 2)
- stake = 1000n, score = 10000 → 1000n (stake × 1; no discount)
can_arbitrate:
- arb = 4999, exec = 3000, ban = null → false
- arb = 5000, exec = 2999, ban = null → false
- arb = 5000, exec = 3000, ban = null → true (exact threshold)
- arb = 5000, exec = 3000, ban_until = 10, ce = 9 → false (ban active, 10 > 9)
- arb = 5000, exec = 3000, ban_until = 10, ce = 10 → true (10 > 10 is false)
- arb = 5000, exec = 3000, ban_until = 10, ce = 11 → true
- arb = 10000, exec = 10000, ban = null → true (well above thresholds)
can_govern:
- gov = 3999, ban = null → false
- gov = 4000, ban = null → true (exact threshold)
- gov = 4000, ban_until = 10, ce = 9 → false
- gov = 4000, ban_until = 10, ce = 10 → true (10 > 10 is false)
- gov = 10000, ban_until = 10, ce = 11 → true
Determinism:
- All five derivations called twice with the same inputs return identical results.
§8. Out-of-scope (binding refusal list)
- ❌ No
Math.sqrt/Math.log— useBUILTINS.get('isqrt')/BUILTINS.get('ilog2'). - ❌ No
Date.now/new Date()—current_epochis a parameter. - ❌ No
Promise/async— synchronous only. - ❌ No DB writes — pure function.
- ❌ No reading
rep.scar_bps(not in derivation algebra). - ❌ No reading
rep.last_activity_epoch(decay layer’s concern, not ours). - ❌ No MCP tool registration (P2.5.1 does that).
§9. Forward references
- P2.5.1 —
reputation_check_gatesMCP tool. Composes all five derivations behind a snapshot read of thereputationstable. Adds JSON-boundary conversions (bigint → number for outputs that fit in safe integer range).
— End of contract —