Audit — P2.4.1 Capability Gates (derived limits)

Task ID: fcaac02d-62bd-4ecc-8cdd-23984731c787 Branch: feature/p2-4-1-limits Base: main @ 7e5cf9d3 (post-P2.1.2, post-P1.3.2) Date: 2026-05-12 Step: 1 of 5 (audit → contract → packet → implement → verify)

Purpose

Inventory the surface area for P2.4.1 — Capability Gates: a pure-function derivation layer that turns the current (node, domain) reputation rows shipped by P2.1.1 + recomputed by P2.1.2 into five derived limits that downstream admission, scheduling, and arbitration code can gate on:

  1. max_parallel_tasks(rep_execution) — concurrency cap, sqrt(score) floored at 20.
  2. rate_limit_bonus(rep_execution, base_rate) — rate-limit bonus bps_mul(base_rate, log2(score)).
  3. stake_discount(required_stake, rep_execution) — stake reduction safe_div(safe_mul(stake, BPS_100_PERCENT), max(score, 1000n)).
  4. can_arbitrate(rep_arbitration, rep_execution, current_epoch) — boolean gate (arb ≥ 5000 ∧ exec ≥ 3000 ∧ ¬banned).
  5. can_govern(rep_governance, current_epoch) — boolean gate (gov ≥ 4000 ∧ ¬banned).

This task is R89 Wave 3 — parallel slice with P2.3.1 (parallel by source-file disjointness). It introduces no schema changes, no MCP tools, no SQL writes. It is library-only and synchronous; the downstream P2.5.1 reputation_check_gates tool composes all five derivations behind a snapshot read.

§1. Spec inputs

§1.1 docs/guides/implementation/task-prompts/p2.1-lambda-reputation.md §P2.4.1 (lines 952–1101)

Authoritative source prompt. Locks the five derivation signatures, the ban-check semantics (ban_until_epoch > current_epoch ⇒ banned), and the per-derivation boundary-test matrix (3 boundaries × 5 derivations = 15 mandatory boundary cases).

Algorithm rules (literally from the prompt §993–1023):

max_parallel_tasks(rep_execution)             = min(sqrt_floor(BigInt(score)), 20n)
rate_limit_bonus(rep_execution, base_rate)    = bps_mul(base_rate, log2_floor(max(BigInt(score), 1n)))
stake_discount(required_stake, rep_execution) = safe_div(safe_mul(required_stake, BPS_100_PERCENT), max(BigInt(score), 1000n))
can_arbitrate(rep_arbitration, rep_execution, current_epoch):
  1. if rep_arbitration.ban_until_epoch && BigInt(ban_until_epoch) > current_epoch: return false
  2. return BigInt(arbitration.score) >= 5000n && BigInt(execution.score) >= 3000n
can_govern(rep_governance, current_epoch):
  1. if rep_governance.ban_until_epoch && BigInt(ban_until_epoch) > current_epoch: return false
  2. return BigInt(governance.score) >= 4000n

Acceptance criteria (literally from the prompt §968–975):

  • AC-1 max_parallel_tasks(rep): bigint = min(sqrt_floor(execution_rep), 20n) per s04 §Derived limits.
  • AC-2 rate_limit_bonus(rep, base_rate): bigint = bps_mul(base_rate, log2_floor(max(execution_rep, 1n))).
  • AC-3 stake_discount(stake, rep): bigint = safe_div(safe_mul(stake, BPS_100_PERCENT), max(execution_rep, 1000n)).
  • AC-4 can_arbitrate(rep_arb, rep_exec): boolean = arb ≥ 5000n ∧ exec ≥ 3000n.
  • AC-5 can_govern(rep_gov): boolean = governance ≥ 4000n.
  • AC-6 Banned nodes (ban_until_epoch > current_epoch) ⇒ both booleans return false.
  • AC-7 All BPS math via integer-math.ts; all built-ins via src/domains/rules/builtins.ts.
  • AC-8 Each derivation has 3 boundary tests: zero, threshold, above-threshold.

§1.2 docs/spec/s04-reputation.md §Derived limits (lines 38–46)

Confirms the four behavioural derivations (sqrt cap, log2 bonus, eligibility threshold, stake inverse rep). The exact thresholds + the min(rep, 1) / max(rep, 1000) floors are clarified only in the task prompt.

§1.3 docs/spec/s09-arbitration.md §Arbiter selection (lines 32–37)

Confirms arbitration eligibility is weighted by arbitration reputation, with correlation and participation caps applied downstream. P2.4.1 ships only the eligibility gate (≥ 5000 arbitration AND ≥ 3000 execution AND not banned); weighted selection itself is a Phase 2.5+ slice.

§1.4 docs/3-world/social/reputation.md §Derived limits

Confirms the closed 5-domain enum. The “max 20” cap on parallel tasks is rationalised as “the largest practical concurrent task count any single node should plausibly hold without overwhelming the global scheduler”. The max(rep, 1000n) floor in stake_discount is rationalised in §Gotcha #3 — without it, near-zero reputation yields runaway discount (the formula divides by reputation, so as rep → 0 the discount → ∞; clamping at 1000 caps the discount at 10× the required stake).

§2. Upstream dependencies (load-bearing)

§2.1 src/domains/reputation/schema.ts (P2.1.1 — R89 Wave 1, PR #226)

Exports ReputationRow with the five fields P2.4.1 needs:

interface ReputationRow {
  readonly node_id: string;
  readonly domain: Domain;
  readonly score: number;               // P2.4.1 reads — BPS_MIN..BPS_MAX, BigInt() at boundary
  readonly scar_bps: number;            // P2.4.1 does NOT read (not in derivation algebra)
  readonly ban_until_epoch: number | null;  // P2.4.1 reads on the two boolean gates
  readonly last_activity_epoch: number; // P2.4.1 does NOT read (decay/penalty timing only)
}

score is number at the storage boundary (better-sqlite3 returns INTEGER as JS number, and the bps range fits comfortably under Number.MAX_SAFE_INTEGER). P2.4.1 converts via BigInt(row.score) at its own IO boundary — never sees float arithmetic.

§2.2 src/domains/rules/builtins.ts (P1.3.2 — R86 Wave 5, PR #209)

Exports the κ DSL built-ins lookup table BUILTINS: BuiltinTable with 13 entries. Critical naming reconciliation: the source prompt §993–1023 uses the spelling sqrt_floor and log2_floor. The shipped κ built-ins use the canonical names isqrt (lines 236–266) and ilog2 (lines 273–281).

These are the same functions semantically:

  • isqrt(n) = floor(sqrt(n)) for n ≥ 0n (Newton’s method, all-bigint).
  • ilog2(n) = floor(log2(n)) for n > 0n, 0n for n ≤ 0n.

The spelling drift is harmless — the source prompt is using the spec vocabulary; the implementation is using the κ DSL vocabulary. P2.4.1 invokes the underlying functions via BUILTINS.get('isqrt')!(...) / BUILTINS.get('ilog2')!(...) to preserve a single source of truth.

isqrt signature: (args: readonly BuiltinValue[]) => BuiltinValue with args = [bigint] and returns bigint. Throws BuiltinTypeError for negative input; isqrt([0n]) returns 0n.

ilog2 signature: (args: readonly BuiltinValue[]) => BuiltinValue with args = [bigint] and returns bigint. Returns 0n for n ≤ 0n (cold-counter safe); ilog2([1n]) returns 0n; ilog2([1024n]) returns 10n.

§2.3 src/domains/rules/integer-math.ts (P1.1.1 — R83.B, PR #188)

Exports bps_mul(value, bps), safe_mul(a, b), safe_div(a, b). All take/return bigint. bps_mul performs floor rounding (value * bps) / 10_000n. safe_mul throws OverflowError outside int64 range. safe_div throws DivisionByZeroError on b === 0n.

§2.4 src/domains/rules/bps-constants.ts (P1.1.3 — R83.B, PR #188)

Exports BPS_100_PERCENT = 10_000n. This is the canonical denominator P2.4.1 needs for stake_discount. (Note: integer-math.ts has a private BPS_DENOMINATOR = 10_000n constant, not exported; the public name in bps-constants.ts is BPS_100_PERCENT. P2.4.1 imports from bps-constants.ts.)

§3. File targets

§3.1 src/domains/reputation/limits.ts (NEW)

The pure-function derivation surface. Five exported functions, no class state, no internal mutable globals. Module-level imports:

  • BUILTINS from ../rules/builtins.js (κ DSL invocation surface; isqrt + ilog2)
  • bps_mul, safe_mul, safe_div from ../rules/integer-math.js
  • BPS_100_PERCENT from ../rules/bps-constants.js
  • type { ReputationRow } from ./schema.js

No state, no IO, no async, no Math.*, no Date.*. The determinism scanner extension shipped in P2.1.2 (src/__tests__/domains/reputation/determinism.test.ts) already globs src/domains/reputation/**, so this file inherits the guard.

§3.2 src/__tests__/domains/reputation/limits.test.ts (NEW)

Boundary tests per derivation (3 per derivation × 5 derivations = 15 mandatory boundary cases) plus a small additional matrix for the ban-check semantics and the stake_discount floor. Target: ≥ 20 tests, 100% branch coverage on limits.ts.

§4. Out-of-scope (per prompt §1056–1063)

  • No MCP tool registration. P2.5.1 (next slice) composes the five derivations into a reputation_check_gates tool. P2.4.1 ships only the library functions.
  • No DB writes. Pure functions; ReputationRow is input-only.
  • No Math.*, no Date.*. κ built-ins + integer-math do all the arithmetic. current_epoch is a parameter, never BigInt(Date.now()).
  • No mutation of inputs. ReputationRow is readonly; the implementation never reassigns or rebinds fields.

§5. Determinism inheritance

P2.1.2 shipped a Jest test (src/__tests__/domains/reputation/determinism.test.ts) that scans every .ts file under src/domains/reputation/** for forbidden patterns (the same set the κ P1.1.2 determinism scanner uses). P2.4.1’s new limits.ts is automatically subject to this guard — no opt-in needed. The boundary test file (limits.test.ts) is allowed to use BigInt() and bigint literals freely; the scanner only restricts the production-source globs.

§6. Risk register

Risk Likelihood Mitigation
isqrt/ilog2 naming drift between prompt and shipped κ High Audit §2.2 documents the alias mapping; implementation imports from BUILTINS (κ source of truth).
BigInt(rep.score) overflow Negligible score is bps in [0, 10_000], never exceeds Number.MAX_SAFE_INTEGER.
stake_discount divide-by-zero Mitigated max(score, 1000n) floor; safe_div never sees 0n.
ilog2(0n) returning 0n instead of throwing Spec — required max(score, 1n) floor in rate_limit_bonus makes the 0n return unreachable in practice, but the prompt asks for max(score, 1n) explicitly for defence in depth.
ban_until_epoch off-by-one High Audit §1.1 algorithm + Step 5 verification will test both sides (ban_until_epoch = current_epoch ⇒ NOT banned; ban_until_epoch = current_epoch + 1n ⇒ banned).
Floats sneaking in via Math.min/Math.max Low The implementation uses native bigint comparison + ternary, not Math.*. Determinism scanner catches it if it does.

§7. Test plan preview (full matrix in Step 2 contract)

15 mandatory boundary cases:

Function Zero Threshold Above-threshold
max_parallel_tasks score=00n score=40020n (exact cap) score=1000020n (capped from isqrt=100)
rate_limit_bonus score=00n (via max(0,1)ilog2(1)=0) score=1024bps_mul(base_rate, 10n) score=10000bps_mul(base_rate, 13n)
stake_discount floor case score=999stake × 10 floor case score=1000stake × 10 score=10000stake / 1 (no discount math regression)
can_arbitrate arb=4999false arb=5000 + exec=3000true banned arb → false regardless
can_govern gov=3999false gov=4000true banned gov → false

Plus ≥ 5 additional cases for ban-check off-by-one + stake_discount near-floor.

§8. Build/lint/test posture

  • Baseline as of 7e5cf9d3: 2478 / 2478 passing across 50 suites (claimed in dispatch packet).
  • Expected delta after P2.4.1: +15–25 tests (20 boundary + a few defensive guards).
  • No existing test changes; no lint config touch; no tsconfig touch.

§9. References

  • docs/guides/implementation/task-prompts/p2.1-lambda-reputation.md §P2.4.1 (lines 952–1101)
  • docs/guides/implementation/task-breakdown.md §P2.4.1
  • docs/3-world/social/reputation.md §Derived limits
  • docs/spec/s04-reputation.md §Derived limits (lines 38–46)
  • docs/spec/s09-arbitration.md §Arbiter selection (lines 32–37)
  • src/domains/reputation/schema.ts (P2.1.1)
  • src/domains/rules/builtins.ts (P1.3.2; isqrt lines 236–266, ilog2 lines 273–281)
  • src/domains/rules/integer-math.ts (P1.1.1; bps_mul lines 114–116, safe_mul/safe_div lines 190–220)
  • src/domains/rules/bps-constants.ts (P1.1.3; BPS_100_PERCENT line 38)
  • src/domains/reputation/compute.ts (P2.1.2 prior-art for module style)

— End of audit —


Back to top

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

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