From 33da20ece0893d0a3a442e540b2a19ce504bd1b3 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Mon, 14 Sep 2026 02:35:49 -0400 Subject: [PATCH 1/4] refactor: use private working memory --- src/engine.ts | 70 ++++++++++++++++++------------------ src/types.ts | 2 +- tests/private-engine.test.ts | 12 +++++++ 3 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 tests/private-engine.test.ts diff --git a/src/engine.ts b/src/engine.ts index f61438c..dc2517b 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,4 +1,4 @@ -import type { EngineState } from './types.js'; +import type { WorkingMemory } from './types.js'; import { CanonicalDirective, DirectiveKind as GrammarDirectiveKind, decompose_directive } from './grammar.js'; import { NoDirectiveDecision, @@ -13,25 +13,25 @@ export const POLICY_USE = 'use' as const; export const POLICY_PROHIBIT = 'prohibit' as const; export class Engine { - private _state: EngineState; + #workingMemory: WorkingMemory; constructor() { if (arguments.length > 0) { throw new TypeError('Engine constructor takes no arguments.'); } - this._state = initialState(); + this.#workingMemory = initialWorkingMemory(); } get premise(): string | null { - return this._state.premise; + return this.#workingMemory.premise; } get policies(): Record { - return { ...this._state.policies }; + return { ...this.#workingMemory.policies }; } export_json(): string { - return stringifyCanonicalJson(sortKeysDeep(this._state)); + return stringifyCanonicalJson(sortKeysDeep(this.#workingMemory)); } exportJson(): string { @@ -39,7 +39,7 @@ export class Engine { } import_json(payload: string): void { - this._state = loadStateJson(payload); + this.#workingMemory = loadWorkingMemoryJson(payload); } importJson(payload: string): void { @@ -50,15 +50,15 @@ export class Engine { if (!(directive instanceof CanonicalDirective)) { throw new TypeError('apply_directive requires a CanonicalDirective.'); } - const previous = cloneState(this._state); + const previousWorkingMemory = cloneWorkingMemory(this.#workingMemory); const failure = this.#semanticFailure(directive); if (failure !== null) { - this._state = previous; + this.#workingMemory = previousWorkingMemory; return failure; } this.#applyCanonicalDirective(directive); - return new UpdateDecision(!statesEqual(previous, this._state)); + return new UpdateDecision(!workingMemoriesEqual(previousWorkingMemory, this.#workingMemory)); } applyDirective(directive: CanonicalDirective): SemanticDecision { @@ -66,7 +66,7 @@ export class Engine { } #semanticFailure(directive: CanonicalDirective): SemanticErrorDecision | null { - if (directive.kind === GrammarDirectiveKind.SET_PREMISE && this._state.premise !== null) { + if (directive.kind === GrammarDirectiveKind.SET_PREMISE && this.#workingMemory.premise !== null) { return new SemanticErrorDecision( SemanticFailure.PREMISE_ALREADY_SET, directive, @@ -74,7 +74,7 @@ export class Engine { ); } - if (directive.kind === GrammarDirectiveKind.CHANGE_PREMISE && this._state.premise === null) { + if (directive.kind === GrammarDirectiveKind.CHANGE_PREMISE && this.#workingMemory.premise === null) { return new SemanticErrorDecision( SemanticFailure.PREMISE_NOT_SET, directive, @@ -84,7 +84,7 @@ export class Engine { if (directive.kind === GrammarDirectiveKind.USE_ITEM) { const itemKey = normalizeItem(directive.operands.item); - if (this._state.policies[itemKey] === POLICY_PROHIBIT) { + if (this.#workingMemory.policies[itemKey] === POLICY_PROHIBIT) { return new SemanticErrorDecision( SemanticFailure.ITEM_PROHIBITED, directive, @@ -98,7 +98,7 @@ export class Engine { if (directive.kind === GrammarDirectiveKind.PROHIBIT_ITEM) { const itemKey = normalizeItem(directive.operands.item); - if (this._state.policies[itemKey] === POLICY_USE) { + if (this.#workingMemory.policies[itemKey] === POLICY_USE) { return new SemanticErrorDecision( SemanticFailure.ITEM_ALREADY_IN_USE, directive, @@ -117,10 +117,10 @@ export class Engine { const oldKey = normalizeItem(oldItem); if (newKey === oldKey) return null; - if (this._state.policies[oldKey] === POLICY_PROHIBIT) { + if (this.#workingMemory.policies[oldKey] === POLICY_PROHIBIT) { return new SemanticErrorDecision(SemanticFailure.REPLACEMENT_SOURCE_PROHIBITED, directive); } - if (this._state.policies[newKey] === POLICY_PROHIBIT) { + if (this.#workingMemory.policies[newKey] === POLICY_PROHIBIT) { return new SemanticErrorDecision( SemanticFailure.REPLACEMENT_TARGET_PROHIBITED, directive, @@ -130,7 +130,7 @@ export class Engine { ] ); } - if (this._state.policies[oldKey] !== POLICY_USE) { + if (this.#workingMemory.policies[oldKey] !== POLICY_USE) { return new SemanticErrorDecision(SemanticFailure.REPLACEMENT_SOURCE_MISSING, directive); } } @@ -144,40 +144,40 @@ export class Engine { #applyCanonicalDirective(directive: CanonicalDirective): void { if (directive.kind === GrammarDirectiveKind.SET_PREMISE || directive.kind === GrammarDirectiveKind.CHANGE_PREMISE) { - this._state.premise = sanitizePremiseValue(directive.operands.value); + this.#workingMemory.premise = sanitizePremiseValue(directive.operands.value); return; } if (directive.kind === GrammarDirectiveKind.USE_ITEM) { - this._state.policies[normalizeItem(directive.operands.item)] = POLICY_USE; + this.#workingMemory.policies[normalizeItem(directive.operands.item)] = POLICY_USE; return; } if (directive.kind === GrammarDirectiveKind.PROHIBIT_ITEM) { - this._state.policies[normalizeItem(directive.operands.item)] = POLICY_PROHIBIT; + this.#workingMemory.policies[normalizeItem(directive.operands.item)] = POLICY_PROHIBIT; return; } if (directive.kind === GrammarDirectiveKind.REMOVE_POLICY) { - delete this._state.policies[normalizeItem(directive.operands.item)]; + delete this.#workingMemory.policies[normalizeItem(directive.operands.item)]; return; } if (directive.kind === GrammarDirectiveKind.REPLACE_USE) { const oldKey = normalizeItem(directive.operands.old_item); const newKey = normalizeItem(directive.operands.new_item); if (oldKey !== newKey) { - delete this._state.policies[oldKey]; - this._state.policies[newKey] = POLICY_USE; + delete this.#workingMemory.policies[oldKey]; + this.#workingMemory.policies[newKey] = POLICY_USE; } return; } if (directive.kind === GrammarDirectiveKind.CLEAR_PREMISE) { - this._state.premise = null; + this.#workingMemory.premise = null; return; } if (directive.kind === GrammarDirectiveKind.RESET_POLICIES) { - this._state.policies = {}; + this.#workingMemory.policies = {}; return; } if (directive.kind === GrammarDirectiveKind.CLEAR_STATE) { - this._state = initialState(); + this.#workingMemory = initialWorkingMemory(); } } @@ -190,7 +190,7 @@ export class Engine { } } -function initialState(): EngineState { +function initialWorkingMemory(): WorkingMemory { return { premise: null, policies: {}, @@ -198,15 +198,15 @@ function initialState(): EngineState { }; } -function cloneState(state: EngineState): EngineState { +function cloneWorkingMemory(workingMemory: WorkingMemory): WorkingMemory { return { - premise: state.premise, - policies: { ...state.policies }, + premise: workingMemory.premise, + policies: { ...workingMemory.policies }, version: 2 }; } -function statesEqual(left: EngineState, right: EngineState): boolean { +function workingMemoriesEqual(left: WorkingMemory, right: WorkingMemory): boolean { if (left.premise !== right.premise) return false; const leftKeys = Object.keys(left.policies); const rightKeys = Object.keys(right.policies); @@ -214,17 +214,17 @@ function statesEqual(left: EngineState, right: EngineState): boolean { return leftKeys.every((key) => left.policies[key] === right.policies[key]); } -function loadStateJson(payload: string): EngineState { +function loadWorkingMemoryJson(payload: string): WorkingMemory { let raw: unknown; try { raw = JSON.parse(payload); } catch { throw new Error('Invalid JSON payload.'); } - return loadStateObject(raw); + return loadWorkingMemoryObject(raw); } -function loadStateObject(raw: unknown): EngineState { +function loadWorkingMemoryObject(raw: unknown): WorkingMemory { if (raw === null || typeof raw !== 'object') { throw new Error('Invalid state payload.'); } @@ -336,4 +336,4 @@ function stringifyCanonicalJson(value: unknown): string { ); } -export type { EngineState }; +export type { WorkingMemory }; diff --git a/src/types.ts b/src/types.ts index 4983ecf..93e2f6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -export interface EngineState { +export interface WorkingMemory { premise: string | null; policies: Record; version: 2; diff --git a/tests/private-engine.test.ts b/tests/private-engine.test.ts new file mode 100644 index 0000000..f87e3d6 --- /dev/null +++ b/tests/private-engine.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest'; +import { Engine } from '../src/engine.js'; + +describe('Engine private working memory', () => { + it('does not expose its working memory as a runtime property', () => { + const engine = new Engine(); + expect('_state' in engine).toBe(false); + expect('workingMemory' in engine).toBe(false); + expect(Object.getOwnPropertyNames(engine)).toEqual([]); + expect(Reflect.ownKeys(engine)).toEqual([]); + }); +}); From cdcfad26ba15bd8f41a127e548960279788b3dfe Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 18 Sep 2026 02:13:45 -0400 Subject: [PATCH 2/4] refactor: harden Engine working memory --- tests/private-engine.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/private-engine.test.ts b/tests/private-engine.test.ts index f87e3d6..0d595e8 100644 --- a/tests/private-engine.test.ts +++ b/tests/private-engine.test.ts @@ -9,4 +9,19 @@ describe('Engine private working memory', () => { expect(Object.getOwnPropertyNames(engine)).toEqual([]); expect(Reflect.ownKeys(engine)).toEqual([]); }); + + it('ignores externally assigned legacy-looking properties', () => { + const engine = new Engine(); + const external = engine as unknown as Record; + external._state = { premise: 'injected', policies: { docker: 'prohibit' }, version: 2 }; + external._workingMemory = { premise: 'injected', policies: { docker: 'prohibit' }, version: 2 }; + + expect(engine.export_json()).toBe('{"policies":{},"premise":null,"version":2}'); + expect(engine.step('use sqlite')).toMatchObject({ kind: 'update', changed: true }); + expect(JSON.parse(engine.export_json())).toEqual({ + policies: { sqlite: 'use' }, + premise: null, + version: 2 + }); + }); }); From cb6bfffbedfde54fef1f321b14aff3f86949d569 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 18 Sep 2026 02:18:32 -0400 Subject: [PATCH 3/4] refactor: hide working memory declaration --- src/engine.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index dc2517b..6b12c9c 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -335,5 +335,3 @@ function stringifyCanonicalJson(value: unknown): string { `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}` ); } - -export type { WorkingMemory }; From 4f06c65e5a45052ee08fc9e6f382a57b031fee46 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Fri, 18 Sep 2026 02:21:17 -0400 Subject: [PATCH 4/4] refactor: align working memory terminology --- src/engine.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/engine.ts b/src/engine.ts index 6b12c9c..255f6b8 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -19,7 +19,7 @@ export class Engine { if (arguments.length > 0) { throw new TypeError('Engine constructor takes no arguments.'); } - this.#workingMemory = initialWorkingMemory(); + this.#workingMemory = initialState(); } get premise(): string | null { @@ -39,7 +39,7 @@ export class Engine { } import_json(payload: string): void { - this.#workingMemory = loadWorkingMemoryJson(payload); + this.#workingMemory = loadStateJson(payload); } importJson(payload: string): void { @@ -50,15 +50,15 @@ export class Engine { if (!(directive instanceof CanonicalDirective)) { throw new TypeError('apply_directive requires a CanonicalDirective.'); } - const previousWorkingMemory = cloneWorkingMemory(this.#workingMemory); + const previousState = cloneState(this.#workingMemory); const failure = this.#semanticFailure(directive); if (failure !== null) { - this.#workingMemory = previousWorkingMemory; + this.#workingMemory = previousState; return failure; } this.#applyCanonicalDirective(directive); - return new UpdateDecision(!workingMemoriesEqual(previousWorkingMemory, this.#workingMemory)); + return new UpdateDecision(!statesEqual(previousState, this.#workingMemory)); } applyDirective(directive: CanonicalDirective): SemanticDecision { @@ -177,7 +177,7 @@ export class Engine { return; } if (directive.kind === GrammarDirectiveKind.CLEAR_STATE) { - this.#workingMemory = initialWorkingMemory(); + this.#workingMemory = initialState(); } } @@ -190,7 +190,7 @@ export class Engine { } } -function initialWorkingMemory(): WorkingMemory { +function initialState(): WorkingMemory { return { premise: null, policies: {}, @@ -198,15 +198,15 @@ function initialWorkingMemory(): WorkingMemory { }; } -function cloneWorkingMemory(workingMemory: WorkingMemory): WorkingMemory { +function cloneState(state: WorkingMemory): WorkingMemory { return { - premise: workingMemory.premise, - policies: { ...workingMemory.policies }, + premise: state.premise, + policies: { ...state.policies }, version: 2 }; } -function workingMemoriesEqual(left: WorkingMemory, right: WorkingMemory): boolean { +function statesEqual(left: WorkingMemory, right: WorkingMemory): boolean { if (left.premise !== right.premise) return false; const leftKeys = Object.keys(left.policies); const rightKeys = Object.keys(right.policies); @@ -214,17 +214,17 @@ function workingMemoriesEqual(left: WorkingMemory, right: WorkingMemory): boolea return leftKeys.every((key) => left.policies[key] === right.policies[key]); } -function loadWorkingMemoryJson(payload: string): WorkingMemory { +function loadStateJson(payload: string): WorkingMemory { let raw: unknown; try { raw = JSON.parse(payload); } catch { throw new Error('Invalid JSON payload.'); } - return loadWorkingMemoryObject(raw); + return loadStateObject(raw); } -function loadWorkingMemoryObject(raw: unknown): WorkingMemory { +function loadStateObject(raw: unknown): WorkingMemory { if (raw === null || typeof raw !== 'object') { throw new Error('Invalid state payload.'); }