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)

  1. max_parallel_tasks(rep_execution: ReputationRow): bigint
  2. rate_limit_bonus(rep_execution: ReputationRow, base_rate: bigint): bigint
  3. stake_discount(required_stake: bigint, rep_execution: ReputationRow): bigint
  4. can_arbitrate(rep_arbitration: ReputationRow, rep_execution: ReputationRow, current_epoch: bigint): boolean
  5. can_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 = 0isqrt(0n) = 0nmin(0n, 20n) = 0n
  • score = 400isqrt(400n) = 20nmin(20n, 20n) = 20n
  • score = 399isqrt(399n) = 19nmin(19n, 20n) = 19n
  • score = 10_000isqrt(10000n) = 100nmin(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_000nilog2(max(0n, 1n)) = ilog2(1n) = 0nbps_mul(1000n, 0n) = 0n
  • score = 1, base_rate = 1_000nilog2(1n) = 0nbps_mul(1000n, 0n) = 0n
  • score = 1024, base_rate = 1_000nilog2(1024n) = 10nbps_mul(1000n, 10n) = 1n (floor of 1000×10/10000)
  • score = 10_000, base_rate = 1_000nilog2(10_000n) = 13nbps_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 = 0safe_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_000n
  • stake = 1000n, score = 1000010_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 ⇒ false
  • gov=4000, no ban ⇒ true
  • gov=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 by ReputationRowSchema)
  • 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_tasks returns bigint, ≤ 20n, ≥ 0n, == isqrt(score) for score ≤ 400, == 20n for score ≥ 400.
  • AC-2 rate_limit_bonus returns bigint, ≥ 0n for non-negative base_rate; returns 0n when score ∈ [0, 1].
  • AC-3 stake_discount returns bigint, ≥ stake / 10n for score ≤ 1000 (floor active), ≤ stake for score ≥ BPS_100_PERCENT.
  • AC-4 can_arbitrate is true iff arb ≥ 5000n ∧ exec ≥ 3000n ∧ ¬banned. Banned iff ban_until_epoch !== null ∧ BigInt(ban) > current_epoch.
  • AC-5 can_govern is true iff gov ≥ 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 — no Math.*, Date.*, Math.random, setTimeout, etc.
  • AC-8 ESLint clean on limits.ts and limits.test.ts — no unused imports, no any, no implicit casts.
  • AC-9 npm run build && npm run lint && npm test exit 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 — use BUILTINS.get('isqrt') / BUILTINS.get('ilog2').
  • ❌ No Date.now / new Date()current_epoch is 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.1reputation_check_gates MCP tool. Composes all five derivations behind a snapshot read of the reputations table. Adds JSON-boundary conversions (bigint → number for outputs that fit in safe integer range).

— End of contract —


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.