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:
max_parallel_tasks(rep_execution)— concurrency cap,sqrt(score)floored at 20.rate_limit_bonus(rep_execution, base_rate)— rate-limit bonusbps_mul(base_rate, log2(score)).stake_discount(required_stake, rep_execution)— stake reductionsafe_div(safe_mul(stake, BPS_100_PERCENT), max(score, 1000n)).can_arbitrate(rep_arbitration, rep_execution, current_epoch)— boolean gate (arb ≥ 5000 ∧ exec ≥ 3000 ∧ ¬banned).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 returnfalse. - AC-7 All BPS math via
integer-math.ts; all built-ins viasrc/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))forn ≥ 0n(Newton’s method, all-bigint).ilog2(n) = floor(log2(n))forn > 0n,0nforn ≤ 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:
BUILTINSfrom../rules/builtins.js(κ DSL invocation surface;isqrt+ilog2)bps_mul,safe_mul,safe_divfrom../rules/integer-math.jsBPS_100_PERCENTfrom../rules/bps-constants.jstype { 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_gatestool. P2.4.1 ships only the library functions. - No DB writes. Pure functions; ReputationRow is input-only.
- No
Math.*, noDate.*. κ built-ins + integer-math do all the arithmetic.current_epochis a parameter, neverBigInt(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=0 → 0n |
score=400 → 20n (exact cap) |
score=10000 → 20n (capped from isqrt=100) |
rate_limit_bonus |
score=0 → 0n (via max(0,1)→ilog2(1)=0) |
score=1024 → bps_mul(base_rate, 10n) |
score=10000 → bps_mul(base_rate, 13n) |
stake_discount |
floor case score=999 → stake × 10 |
floor case score=1000 → stake × 10 |
score=10000 → stake / 1 (no discount math regression) |
can_arbitrate |
arb=4999 → false |
arb=5000 + exec=3000 → true |
banned arb → false regardless |
can_govern |
gov=3999 → false |
gov=4000 → true |
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
tsconfigtouch.
§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.1docs/3-world/social/reputation.md§Derived limitsdocs/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;isqrtlines 236–266,ilog2lines 273–281)src/domains/rules/integer-math.ts(P1.1.1;bps_mullines 114–116,safe_mul/safe_divlines 190–220)src/domains/rules/bps-constants.ts(P1.1.3;BPS_100_PERCENTline 38)src/domains/reputation/compute.ts(P2.1.2 prior-art for module style)
— End of audit —