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 for src/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 for computeDiff + generateReadProof.
  • deepEqual — minimal structural-equality helper for the 4 serialized shapes.
  • isHex64Lower — validation helper for fork_id and rule_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') returns 0n when alice not in state.
  • F2b: getReputation('alice', 'unknown_domain') returns 0n when alice present but domain absent.
  • F2c: getStake('alice') returns 0n when 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 because set is 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') is undefined (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_binding execution time (measured by counting allocations) is constant across 0, 100, 1000 prior bindings. Specifically, the count of Map.prototype.set calls during a single with_binding(...) call is always exactly 1, independent of N — verified by Jest spy on Map.prototype.set for 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') returns 2n (child shadows parent).

§P3.7. F7 — computeDiff codepoint key ordering (3 cases)

  • F7a: With keys present in non-alphabetical insertion order, the returned diff is alpha-sorted.
  • F7b: Locale stability — sort result is identical when run with process.env.LANG = 'tr_TR.utf8' simulated (no localeCompare).
  • 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 as value.
  • F9c: Unknown key — returns value: undefined.

§P3.10. F10 — makeReadOnlyState validation throws (5 cases)

  • F10a: epoch < 0nReadOnlyStateError.
  • F10b: event_count < 0nReadOnlyStateError.
  • F10c: fork_id not 64-char hex → ReadOnlyStateError.
  • F10d: rule_version not 64-char hex → ReadOnlyStateError.
  • F10e: stakes containing negative value → ReadOnlyStateError.

§P3.11. F11 — Determinism harness self-scan (1 case)

  • F11a: inspectFunctionForbidden over makeReadOnlyState, computeDiff, generateReadProof returns [] 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

  1. npm run build — TypeScript compilation succeeds with zero warnings.
  2. npm run lint — ESLint clean.
  3. npm test — Jest green; all 31+ cases pass; total test count rises by ~31 from main d766db59.
  4. inspectFunctionForbidden over each public export returns [].
  5. Reading src/domains/rules/state-access.ts confirms no imports of engine.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.ts or 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.


Back to top

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

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