P1.3.3 — κ State Access Layer — Execution Packet
Step 3 of the 5-step executor chain. Builds on
docs/contracts/p1-3-3-state-access-contract.md. Defines the implementation plan, file layout, test matrix, and verification recipe forsrc/domains/rules/state-access.ts+src/__tests__/domains/rules/state-access.test.ts.
§P1. Implementation plan
§P1.1. File ownership
| File | LOC est. | Created in this slice? |
|---|---|---|
src/domains/rules/state-access.ts |
~330 | yes |
src/__tests__/domains/rules/state-access.test.ts |
~700 | yes |
docs/audits/p1-3-3-state-access-audit.md |
218 | yes (Step 1, committed) |
docs/contracts/p1-3-3-state-access-contract.md |
311 | yes (Step 2, committed) |
docs/packets/p1-3-3-state-access-packet.md |
this file | yes (Step 3) |
docs/verification/p1-3-3-state-access-verification.md |
TBD | yes (Step 5) |
No edits to any existing source files. No package.json deltas.
§P1.2. Source file structure
src/domains/rules/state-access.ts
├── §1. Header docblock
├── §2. Internal: FrozenMap<K, V> class (read-only Map adapter)
├── §3. Public: ReadOnlyMap<K, V> interface
├── §4. Public: TokenRecord interface
├── §5. Public: StateDiff interface
├── §6. Public: ReadProof interface
├── §7. Public: ReadOnlyState interface
├── §8. Public: ReadOnlyStateInit interface
├── §9. Public: ReadOnlyStateError class
├── §10. Internal: ReadOnlyStateImpl class
│ ├── constructor (private — factory only)
│ ├── 7 private fields backing the 7 readonly accessors
│ ├── readonly accessors via getters (reputation, tokens, stakes, epoch, event_count, fork_id, rule_version)
│ ├── getReputation / getStake / getTokens
│ ├── getBinding / getBindings (parent-chain walk)
│ ├── with_binding (constructs new instance, parent-pointer chain)
│ └── toEngineState
├── §11. Public: makeReadOnlyState factory (validates + freezes Maps)
├── §12. Public: computeDiff (codepoint-sorted top-level diff)
└── §13. Public: generateReadProof (η stub)
§P1.3. FrozenMap design
class FrozenMap<K, V> implements ReadOnlyMap<K, V> {
readonly #inner: Map<K, V>;
constructor(inner: Map<K, V>) { this.#inner = inner; }
get size(): number { return this.#inner.size; }
get(key: K): V | undefined { return this.#inner.get(key); }
has(key: K): boolean { return this.#inner.has(key); }
keys(): IterableIterator<K> { return this.#inner.keys(); }
values(): IterableIterator<V> { return this.#inner.values(); }
entries(): IterableIterator<[K, V]> { return this.#inner.entries(); }
forEach(cb: (v: V, k: K) => void): void { this.#inner.forEach(cb); }
[Symbol.iterator](): IterableIterator<[K, V]> { return this.#inner[Symbol.iterator](); }
}
Uses ES2022 private fields (#inner) — TypeScript supports them; no Object.freeze needed because the class never exposes set/delete/clear on its public type.
The Map identity is stable across the lifetime of the FrozenMap (never reassigned). Iteration order is the underlying Map’s insertion order — sufficient for our determinism contract (the inputs are constructed deterministically by callers).
§P1.4. Parent-pointer binding chain
The with_binding O(1) requirement is satisfied by:
class ReadOnlyStateImpl implements ReadOnlyState {
// ...7 fields shared by reference...
readonly #localBindings: ReadonlyMap<string, bigint | string | boolean>;
readonly #parent: ReadOnlyStateImpl | null;
with_binding(name: string, value: bigint | string | boolean): ReadOnlyState {
// Validate name (non-empty).
if (name.length === 0) {
throw new ReadOnlyStateError('binding name must be non-empty');
}
// O(1) — single-entry local map + parent pointer.
const local = new Map<string, bigint | string | boolean>();
local.set(name, value);
return new ReadOnlyStateImpl({
reputation: this.#reputation, // shared by reference
tokens: this.#tokens, // shared by reference
stakes: this.#stakes, // shared by reference
epoch: this.#epoch,
event_count: this.#event_count,
fork_id: this.#fork_id,
rule_version: this.#rule_version,
localBindings: local,
parent: this,
});
}
getBinding(name: string): bigint | string | boolean | undefined {
// Walk: local then parent chain.
let cursor: ReadOnlyStateImpl | null = this;
while (cursor !== null) {
if (cursor.#localBindings.has(name)) {
return cursor.#localBindings.get(name);
}
cursor = cursor.#parent;
}
return undefined;
}
getBindings(): ReadonlyMap<string, bigint | string | boolean> {
// Materialize the chain — O(depth × bindings). Used only when the
// engine.Context needs a flat Map. For per-query lookups, getBinding is preferred.
const out = new Map<string, bigint | string | boolean>();
// Walk from root to leaf so child bindings shadow parent bindings.
const chain: ReadOnlyStateImpl[] = [];
let cursor: ReadOnlyStateImpl | null = this;
while (cursor !== null) { chain.push(cursor); cursor = cursor.#parent; }
for (let i = chain.length - 1; i >= 0; i--) {
for (const [k, v] of chain[i]!.#localBindings) {
out.set(k, v);
}
}
return out;
}
}
Why this is O(1) for with_binding: the constructor allocates one Map with one entry plus 7 references. No size-N copy of an existing bindings Map. Total cost is bounded by a constant.
getBindings is NOT O(1) — it materializes the chain. But the contract only requires with_binding to be O(1). getBindings is called once per evaluation (when constructing the engine Context) and is amortized. Tests probe with_binding time directly via wall-clock-independent counter.
§P1.5. computeDiff implementation
export function computeDiff(before: ReadOnlyState, after: ReadOnlyState): StateDiff[] {
const diffs: StateDiff[] = [];
// 7 top-level keys, codepoint-sorted (Array.prototype.sort no-comparator).
const keys = ['epoch', 'event_count', 'fork_id', 'reputation', 'rule_version', 'stakes', 'tokens'].sort();
for (const key of keys) {
const oldV = serializeKey(before, key);
const newV = serializeKey(after, key);
if (!deepEqual(oldV, newV)) {
diffs.push({ key, old_value: oldV, new_value: newV });
}
}
return diffs;
}
serializeKey projects each ReadOnlyState key into a JSON-comparable shape:
- Scalar keys (
epoch,event_count,fork_id,rule_version) → the raw value. stakes(Map<string, bigint>) → sorted object{node: bigint}.reputation(Map<string, Map<string, bigint») → sorted nested object.tokens(Map<string, TokenRecord[]>) → sorted object of arrays.
deepEqual is structural (handles bigints + nested objects + arrays). Because we only diff at the top level, the helper is small (~30 lines).
§P1.6. generateReadProof stub
export function generateReadProof(state: ReadOnlyState, key: string): ReadProof {
let value: unknown;
switch (key) {
case 'epoch': value = state.epoch; break;
case 'event_count': value = state.event_count; break;
case 'fork_id': value = state.fork_id; break;
case 'rule_version': value = state.rule_version; break;
case 'reputation': value = serializeMap2(state.reputation); break;
case 'tokens': value = serializeTokens(state.tokens); break;
case 'stakes': value = serializeMap1(state.stakes); break;
default: value = undefined;
}
return {
state_root: state.rule_version,
key,
value,
proof_path: [],
};
}
No imports. The state_root is the rule_version string itself (a sha256 hex per spec s12 — already a hash, suitable as a stub root). Production η integration replaces this with a proper Merkle root.
§P2. Source file inventory (final shape)
§P2.1. Public exports (7 entities + 5 interfaces)
| Export | Kind | Status |
|---|---|---|
ReadOnlyMap |
interface | new |
TokenRecord |
interface | new |
StateDiff |
interface | new |
ReadProof |
interface | new |
ReadOnlyState |
interface | new |
ReadOnlyStateInit |
interface | new |
ReadOnlyStateError |
class | new |
makeReadOnlyState |
function | new |
computeDiff |
function | new |
generateReadProof |
function | new |
§P2.2. Internal symbols (not exported)
FrozenMap<K, V>— read-only Map adapter class.ReadOnlyStateImpl— the implementation class with parent-pointer chain.serializeKey,serializeMap1,serializeMap2,serializeTokens— helpers forcomputeDiff+generateReadProof.deepEqual— minimal structural-equality helper for the 4 serialized shapes.isHex64Lower— validation helper forfork_idandrule_version.
§P3. Test matrix (~31 cases across 12 fixture families)
§P3.1. F1 — Construction + 7 keys exposed (3 cases)
- F1a: minimum-shape construction (all-empty Maps + scalars) succeeds; all 7 keys readable.
- F1b: with non-empty Maps, getReputation / getStake / getTokens return inserted values.
- F1c: re-construction with the same input is equivalent (deep-equal).
§P3.2. F2 — Getter-fallback semantics (4 cases)
- F2a:
getReputation('alice', 'governance')returns0nwhen alice not in state. - F2b:
getReputation('alice', 'unknown_domain')returns0nwhen alice present but domain absent. - F2c:
getStake('alice')returns0nwhen alice absent. - F2d:
getTokens('alice')returns[](frozen / readonly) when alice absent.
§P3.3. F3 — Frozen-Map mutation rejection (2 cases)
- F3a:
(state.reputation as any).set('eve', new Map())does not exist on the FrozenMap surface (the cast call is a TypeError becausesetis undefined). - F3b: After repeated reads, the underlying Map size is unchanged (no covert mutation).
§P3.4. F4 — with_binding immutability (3 cases)
- F4a:
state.with_binding('actor', 'alice')returns a different reference (!== state). - F4b: After the call,
state.getBinding('actor')isundefined(original unchanged). - F4c: After the call,
state.getBindings()deep-equals its pre-call snapshot.
§P3.5. F5 — with_binding O(1) (1 case)
- F5a:
with_bindingexecution time (measured by counting allocations) is constant across 0, 100, 1000 prior bindings. Specifically, the count ofMap.prototype.setcalls during a singlewith_binding(...)call is always exactly 1, independent of N — verified by Jest spy onMap.prototype.setfor the duration of the call.
§P3.6. F6 — Binding chain semantics (2 cases)
- F6a: Multi-level chain —
state.with_binding('a', 1n).with_binding('b', 2n)exposes both bindings. - F6b: Shadowing —
state.with_binding('a', 1n).with_binding('a', 2n).getBinding('a')returns2n(child shadows parent).
§P3.7. F7 — computeDiff codepoint key ordering (3 cases)
- F7a: With keys present in non-alphabetical insertion order, the returned
diffis alpha-sorted. - F7b: Locale stability — sort result is identical when run with
process.env.LANG = 'tr_TR.utf8'simulated (nolocaleCompare). - F7c: Bindings are NOT included in the diff (two states with same 7 keys + different bindings →
diff = []).
§P3.8. F8 — computeDiff value semantics (3 cases)
- F8a: Scalar diff — change
epoch→ diff has 1 entry with correct old/new bigints. - F8b: Map diff — change
stakes→ diff has 1 entry with serialized old/new objects. - F8c: No change — identical states →
diff = [].
§P3.9. F9 — generateReadProof stub shape (3 cases)
- F9a: Scalar key (
epoch) — returns{ state_root: rule_version, key: 'epoch', value: <bigint>, proof_path: [] }. - F9b: Map key (
stakes) — returns serialized object asvalue. - F9c: Unknown key — returns
value: undefined.
§P3.10. F10 — makeReadOnlyState validation throws (5 cases)
- F10a:
epoch < 0n→ReadOnlyStateError. - F10b:
event_count < 0n→ReadOnlyStateError. - F10c:
fork_idnot 64-char hex →ReadOnlyStateError. - F10d:
rule_versionnot 64-char hex →ReadOnlyStateError. - F10e:
stakescontaining negative value →ReadOnlyStateError.
§P3.11. F11 — Determinism harness self-scan (1 case)
- F11a:
inspectFunctionForbiddenovermakeReadOnlyState,computeDiff,generateReadProofreturns[]for all three.
§P3.12. F12 — toEngineState projection (1 case)
- F12a:
state.toEngineState()returns a flat record with the 7 keys; the 3 Map keys are serialized to plain objects.
§P3.13. F13 — Defensive copy of input Maps (3 cases)
- F13a: After
makeReadOnlyState, mutating the caller’s input Map does NOT bleed through to the constructed state. - F13b: Same for nested reputation Maps.
- F13c: Same for stakes Map.
§P4. Verification recipe
npm run build— TypeScript compilation succeeds with zero warnings.npm run lint— ESLint clean.npm test— Jest green; all 31+ cases pass; total test count rises by ~31 from maind766db59.inspectFunctionForbiddenover each public export returns[].- Reading
src/domains/rules/state-access.tsconfirms no imports ofengine.ts,parser.ts,proof/merkle.ts, or any Node built-in.
§P5. Commit plan
| Step | Files | Commit message |
|---|---|---|
| 1 | docs/audits/p1-3-3-state-access-audit.md |
audit(p1-3-3-state-access): inventory surface |
| 2 | docs/contracts/p1-3-3-state-access-contract.md |
contract(p1-3-3-state-access): behavioral contract |
| 3 | docs/packets/p1-3-3-state-access-packet.md (this file) |
packet(p1-3-3-state-access): execution plan |
| 4 | src/domains/rules/state-access.ts + src/__tests__/domains/rules/state-access.test.ts |
feat(p1-3-3-state-access): immutable state layer with binding chain |
| 5 | docs/verification/p1-3-3-state-access-verification.md |
verify(p1-3-3-state-access): test evidence |
§P6. Forbiddens
- No edits to
src/domains/rules/engine.tsor any other κ source file outside this slice. - No
Math.*,Date.*,localeCompare,crypto.*,await,async. - No imports from
proof/merkle.ts,engine.ts,parser.ts,lexer.ts. - No commits on
main.
§P7. Packet completion
- §P1 implementation plan committed.
- §P2 source-file inventory bounded.
- §P3 test matrix sketched (12 families, ~31 cases).
- §P4 verification recipe defined.
- §P5 commit plan finalized.
- §P6 forbiddens enumerated.
Packet gates implementation. Implementation proceeds at src/domains/rules/state-access.ts + src/__tests__/domains/rules/state-access.test.ts.