@remnic/core 9.3.691 → 9.3.693

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/access-boundary.d.ts +6 -5
  2. package/dist/access-boundary.js +12 -12
  3. package/dist/access-cli.js +71 -18
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +5 -4
  6. package/dist/access-http.js +16 -16
  7. package/dist/access-mcp.d.ts +7 -4
  8. package/dist/access-mcp.js +15 -15
  9. package/dist/access-operations.d.ts +12 -6
  10. package/dist/access-operations.js +16 -14
  11. package/dist/access-schema.js +3 -3
  12. package/dist/{access-service-CKnFdxH-.d.ts → access-service-DxCpgyVW.d.ts} +96 -2
  13. package/dist/access-service.d.ts +5 -4
  14. package/dist/access-service.js +11 -11
  15. package/dist/access-surface-catalog.d.ts +5 -4
  16. package/dist/access-surface-catalog.js +2 -0
  17. package/dist/access-surface-catalog.js.map +1 -1
  18. package/dist/bootstrap.d.ts +4 -3
  19. package/dist/{chunk-QNG35H27.js → chunk-4IQNAOP3.js} +5 -141
  20. package/dist/chunk-4IQNAOP3.js.map +1 -0
  21. package/dist/{chunk-VH2DHBHG.js → chunk-5HGPDRQZ.js} +2 -2
  22. package/dist/chunk-5HGPDRQZ.js.map +1 -0
  23. package/dist/chunk-6C2BMUZA.js +444 -0
  24. package/dist/chunk-6C2BMUZA.js.map +1 -0
  25. package/dist/{chunk-UUMYB3FR.js → chunk-6ZNIUGXB.js} +10 -10
  26. package/dist/{chunk-IPJD7WJH.js → chunk-E6BOD3UJ.js} +2 -2
  27. package/dist/{chunk-TJ5IIQIR.js → chunk-F7BVJ3IE.js} +40 -369
  28. package/dist/chunk-F7BVJ3IE.js.map +1 -0
  29. package/dist/{chunk-D3DNGQNQ.js → chunk-FM7KLRKN.js} +2 -2
  30. package/dist/{chunk-7MOTEVAA.js → chunk-HRUULBBV.js} +3 -1
  31. package/dist/chunk-HRUULBBV.js.map +1 -0
  32. package/dist/chunk-HXNMDPHN.js +149 -0
  33. package/dist/chunk-HXNMDPHN.js.map +1 -0
  34. package/dist/{chunk-7UVDIWLL.js → chunk-JNKH2XWQ.js} +36 -6
  35. package/dist/chunk-JNKH2XWQ.js.map +1 -0
  36. package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
  37. package/dist/{chunk-OOO7ZKOD.js → chunk-KYH53EFD.js} +3 -3
  38. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  39. package/dist/{chunk-KO6OL4I7.js → chunk-MQTPUPLP.js} +297 -17
  40. package/dist/chunk-MQTPUPLP.js.map +1 -0
  41. package/dist/{chunk-TSGREOYY.js → chunk-RHWFMBO5.js} +33 -6
  42. package/dist/chunk-RHWFMBO5.js.map +1 -0
  43. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  44. package/dist/{chunk-K7I24ZO2.js → chunk-THKNO6OX.js} +22 -21
  45. package/dist/{chunk-K7I24ZO2.js.map → chunk-THKNO6OX.js.map} +1 -1
  46. package/dist/{chunk-QLRYXOAD.js → chunk-UDJLF3BO.js} +2 -2
  47. package/dist/{chunk-NCRDCTEJ.js → chunk-WYB44HHV.js} +21 -7
  48. package/dist/chunk-WYB44HHV.js.map +1 -0
  49. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  50. package/dist/{cli-CrSW_KRq.d.ts → cli-DoQsv4ut.d.ts} +2 -2
  51. package/dist/cli.d.ts +6 -5
  52. package/dist/cli.js +26 -25
  53. package/dist/contradiction/index.js +4 -4
  54. package/dist/explicit-capture.d.ts +4 -3
  55. package/dist/index.d.ts +6 -5
  56. package/dist/index.js +33 -31
  57. package/dist/index.js.map +1 -1
  58. package/dist/mcp-memory-inspector-app.d.ts +5 -4
  59. package/dist/namespaces/migrate.js +8 -8
  60. package/dist/namespaces/search.js +7 -7
  61. package/dist/operator-toolkit.js +11 -10
  62. package/dist/orchestration/maintenance.d.ts +112 -0
  63. package/dist/orchestration/maintenance.js +39 -0
  64. package/dist/orchestration/maintenance.js.map +1 -0
  65. package/dist/{orchestrator-DvswGK76.d.ts → orchestrator-CNJCYjZ1.d.ts} +9 -33
  66. package/dist/orchestrator.d.ts +2 -1
  67. package/dist/orchestrator.js +13 -11
  68. package/dist/schemas.d.ts +28 -28
  69. package/dist/search/factory.js +6 -6
  70. package/dist/search/index.js +11 -11
  71. package/dist/search/lancedb-backend.js +2 -2
  72. package/dist/search/meilisearch-backend.js +2 -2
  73. package/dist/search/orama-backend.js +2 -2
  74. package/dist/transfer/autodetect.js +1 -1
  75. package/dist/transfer/backup.js +1 -1
  76. package/dist/transfer/capsule-export.js +2 -2
  77. package/dist/transfer/types.d.ts +12 -12
  78. package/package.json +12 -2
  79. package/src/access-boundary.ts +2 -1
  80. package/src/access-cli.ts +65 -2
  81. package/src/access-http.ts +20 -0
  82. package/src/access-mcp.ts +29 -0
  83. package/src/access-operations.ts +46 -0
  84. package/src/access-service.ts +55 -0
  85. package/src/access-surface-catalog.test.ts +1 -1
  86. package/src/access-surface-catalog.ts +2 -0
  87. package/src/cli.ts +1 -0
  88. package/src/coding/coding-namespace.ts +1 -1
  89. package/src/coding/session-delta-surfaces.test.ts +383 -0
  90. package/src/coding/session-delta-surfaces.ts +325 -0
  91. package/src/coding/session-delta.test.ts +411 -0
  92. package/src/coding/session-delta.ts +442 -0
  93. package/src/orchestration/maintenance.test.ts +738 -0
  94. package/src/orchestration/maintenance.ts +557 -0
  95. package/src/orchestrator-flush.test.ts +0 -599
  96. package/src/orchestrator.ts +33 -419
  97. package/dist/chunk-7MOTEVAA.js.map +0 -1
  98. package/dist/chunk-7UVDIWLL.js.map +0 -1
  99. package/dist/chunk-KO6OL4I7.js.map +0 -1
  100. package/dist/chunk-NCRDCTEJ.js.map +0 -1
  101. package/dist/chunk-QNG35H27.js.map +0 -1
  102. package/dist/chunk-TJ5IIQIR.js.map +0 -1
  103. package/dist/chunk-TSGREOYY.js.map +0 -1
  104. package/dist/chunk-VH2DHBHG.js.map +0 -1
  105. /package/dist/{chunk-UUMYB3FR.js.map → chunk-6ZNIUGXB.js.map} +0 -0
  106. /package/dist/{chunk-IPJD7WJH.js.map → chunk-E6BOD3UJ.js.map} +0 -0
  107. /package/dist/{chunk-D3DNGQNQ.js.map → chunk-FM7KLRKN.js.map} +0 -0
  108. /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
  109. /package/dist/{chunk-OOO7ZKOD.js.map → chunk-KYH53EFD.js.map} +0 -0
  110. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  111. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  112. /package/dist/{chunk-QLRYXOAD.js.map → chunk-UDJLF3BO.js.map} +0 -0
  113. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Surface contract tests for the session-delta handler
3
+ * (issue #1548 Track A PR 4).
4
+ *
5
+ * Contract under test:
6
+ * - Gate predicate: enabled + sessionDelta + coding context (rule 39).
7
+ * - get with no prior state → first_run, state initialized.
8
+ * - get with prior head == current → unchanged.
9
+ * - get with prior head ancestor → changed with capped delta.
10
+ * - get with unreachable prior head → tagged failure (rule 34).
11
+ * - State write happens AFTER compute (rule 25); failures are logged not fatal.
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import test from "node:test";
15
+
16
+ import {
17
+ DELTA_SUBCOMMANDS,
18
+ formatDeltaSubcommands,
19
+ handleCodingDelta,
20
+ isDeltaSubcommand,
21
+ isSessionDeltaSurfaceEnabled,
22
+ isSessionDeltaSurfaceVisible,
23
+ type DeltaSurfaceContext,
24
+ type DeltaSurfaceStorage,
25
+ } from "./session-delta-surfaces.js";
26
+ import type {
27
+ CodingKnowledgeConfig,
28
+ CodingContext,
29
+ } from "../types.js";
30
+ import type { SessionDeltaGitInvoker } from "./session-delta.js";
31
+
32
+ // ──────────────────────────────────────────────────────────────────────────
33
+ // Fixtures
34
+ // ──────────────────────────────────────────────────────────────────────────
35
+
36
+ const DEFAULT_CONFIG: CodingKnowledgeConfig = {
37
+ enabled: true,
38
+ decisionRecords: true,
39
+ architectureCard: true,
40
+ sessionDelta: true,
41
+ architectureCardLlmSummary: false,
42
+ structuralProvider: "none",
43
+ structuralProviderCommand: "",
44
+ };
45
+
46
+ const CODING_CONTEXT: CodingContext = {
47
+ projectId: "origin:deadbeef",
48
+ branch: "main",
49
+ rootPath: "/fake/repo",
50
+ defaultBranch: "main",
51
+ };
52
+
53
+ function makeStorage(): DeltaSurfaceStorage {
54
+ return { memoryDir: "/fake/mem", namespace: "project-test" };
55
+ }
56
+
57
+ function makeInvoker(responses: Array<{ args: string[]; stdout: string; exitCode?: number }>): SessionDeltaGitInvoker {
58
+ return (_cwd: string, args: string[]) => {
59
+ const match = responses.find((r) => r.args.join(" ") === args.join(" "));
60
+ if (!match) return { stdout: "", exitCode: 1 };
61
+ return { stdout: match.stdout, exitCode: match.exitCode ?? 0 };
62
+ };
63
+ }
64
+
65
+ function makeContext(overrides: Partial<DeltaSurfaceContext> = {}): DeltaSurfaceContext {
66
+ return {
67
+ codingKnowledge: DEFAULT_CONFIG,
68
+ getCodingContext: () => CODING_CONTEXT,
69
+ resolveStorage: async () => makeStorage(),
70
+ gitInvoker: makeInvoker([]),
71
+ throwInputError: (msg) => {
72
+ throw new Error(msg);
73
+ },
74
+ ...overrides,
75
+ };
76
+ }
77
+
78
+ // Stub the pure module's state read/write by importing the real module and
79
+ // shimming its filesystem. The handler imports readLastSeenState / writeLastSeenState
80
+ // from ./session-delta.js, which hit the real fs. We point memoryDir at a
81
+ // temp dir per-test.
82
+ import { mkdtemp, rm } from "node:fs/promises";
83
+ import { tmpdir } from "node:os";
84
+ import path from "node:path";
85
+
86
+ async function makeTempStorage(): Promise<{ storage: DeltaSurfaceStorage; cleanup: () => Promise<void> }> {
87
+ const dir = await mkdtemp(path.join(tmpdir(), "delta-surface-"));
88
+ return {
89
+ storage: { memoryDir: dir, namespace: "project-test" },
90
+ cleanup: async () => { await rm(dir, { recursive: true, force: true }); },
91
+ };
92
+ }
93
+
94
+ // ──────────────────────────────────────────────────────────────────────────
95
+ // Subcommand + gate helpers
96
+ // ──────────────────────────────────────────────────────────────────────────
97
+
98
+ test("subcommands: the surface exposes only `get`", () => {
99
+ assert.deepEqual([...DELTA_SUBCOMMANDS], ["get"]);
100
+ assert.equal(formatDeltaSubcommands(), "get");
101
+ });
102
+
103
+ test("isDeltaSubcommand: narrows valid + rejects unknown", () => {
104
+ assert.equal(isDeltaSubcommand("get"), true);
105
+ assert.equal(isDeltaSubcommand("refresh"), false);
106
+ assert.equal(isDeltaSubcommand(undefined), false);
107
+ assert.equal(isDeltaSubcommand(123), false);
108
+ });
109
+
110
+ test("gate: surface enabled when config + sessionDelta + coding context all true", () => {
111
+ assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, CODING_CONTEXT), true);
112
+ });
113
+
114
+ test("gate: surface disabled when master gate off", () => {
115
+ const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, enabled: false };
116
+ assert.equal(isSessionDeltaSurfaceEnabled(config, CODING_CONTEXT), false);
117
+ });
118
+
119
+ test("gate: surface disabled when sessionDelta off", () => {
120
+ const config: CodingKnowledgeConfig = { ...DEFAULT_CONFIG, sessionDelta: false };
121
+ assert.equal(isSessionDeltaSurfaceEnabled(config, CODING_CONTEXT), false);
122
+ });
123
+
124
+ test("gate: surface disabled when no coding context", () => {
125
+ assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, null), false);
126
+ assert.equal(isSessionDeltaSurfaceEnabled(DEFAULT_CONFIG, undefined), false);
127
+ });
128
+
129
+ test("visibility gate: config-only check mirrors full gate minus context", () => {
130
+ assert.equal(isSessionDeltaSurfaceVisible(DEFAULT_CONFIG), true);
131
+ assert.equal(isSessionDeltaSurfaceVisible({ ...DEFAULT_CONFIG, enabled: false }), false);
132
+ assert.equal(isSessionDeltaSurfaceVisible({ ...DEFAULT_CONFIG, sessionDelta: false }), false);
133
+ });
134
+
135
+ // ──────────────────────────────────────────────────────────────────────────
136
+ // Handler — gate enforcement
137
+ // ──────────────────────────────────────────────────────────────────────────
138
+
139
+ test("handler: throws when gate fails (no coding context)", async () => {
140
+ const ctx = makeContext({ getCodingContext: () => null });
141
+ await assert.rejects(
142
+ () => handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx),
143
+ /coding_delta requires/,
144
+ );
145
+ });
146
+
147
+ test("handler: throws when gate fails (master gate off)", async () => {
148
+ const ctx = makeContext({
149
+ codingKnowledge: { ...DEFAULT_CONFIG, enabled: false },
150
+ });
151
+ await assert.rejects(
152
+ () => handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx),
153
+ /coding_delta requires/,
154
+ );
155
+ });
156
+
157
+ test("handler: throws when gate fails (sessionDelta off)", async () => {
158
+ const ctx = makeContext({
159
+ codingKnowledge: { ...DEFAULT_CONFIG, sessionDelta: false },
160
+ });
161
+ await assert.rejects(
162
+ () => handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx),
163
+ /coding_delta requires/,
164
+ );
165
+ });
166
+
167
+ // ──────────────────────────────────────────────────────────────────────────
168
+ // Handler — three-state matrix via the surface
169
+ // ──────────────────────────────────────────────────────────────────────────
170
+
171
+ test("handler/get: first run → first_run, nextState initialized", async () => {
172
+ const { storage, cleanup } = await makeTempStorage();
173
+ try {
174
+ const invoker = makeInvoker([
175
+ { args: ["rev-parse", "HEAD"], stdout: "newhead\n" },
176
+ ]);
177
+ const ctx = makeContext({
178
+ resolveStorage: async () => storage,
179
+ gitInvoker: invoker,
180
+ });
181
+ const result = await handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx);
182
+ assert.equal(result.subcommand, "get");
183
+ if (!("ok" in result) || !result.ok || result.kind !== "first_run") {
184
+ assert.fail(`expected first_run, got ${JSON.stringify(result)}`);
185
+ return;
186
+ }
187
+ assert.equal(result.nextState.head, "newhead");
188
+ assert.ok(result.nextState.at);
189
+ } finally {
190
+ await cleanup();
191
+ }
192
+ });
193
+
194
+ test("handler/get: unchanged head → suppressed", async () => {
195
+ const { storage, cleanup } = await makeTempStorage();
196
+ try {
197
+ // Pre-seed the state file by running once.
198
+ const invoker1 = makeInvoker([
199
+ { args: ["rev-parse", "HEAD"], stdout: "stable\n" },
200
+ ]);
201
+ const ctx1 = makeContext({ resolveStorage: async () => storage, gitInvoker: invoker1 });
202
+ await handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx1);
203
+
204
+ // Second call with the same head.
205
+ const invoker2 = makeInvoker([
206
+ { args: ["rev-parse", "HEAD"], stdout: "stable\n" },
207
+ ]);
208
+ const ctx2 = makeContext({ resolveStorage: async () => storage, gitInvoker: invoker2 });
209
+ const result = await handleCodingDelta({ subcommand: "get", sessionKey: "s1" }, ctx2);
210
+ if (!("ok" in result) || !result.ok || result.kind !== "unchanged") {
211
+ assert.fail(`expected unchanged, got ${JSON.stringify(result)}`);
212
+ return;
213
+ }
214
+ } finally {
215
+ await cleanup();
216
+ }
217
+ });
218
+
219
+ test("handler/get: changed head → real delta", async () => {
220
+ const { storage, cleanup } = await makeTempStorage();
221
+ try {
222
+ // Seed: first run on headA.
223
+ const invoker1 = makeInvoker([
224
+ { args: ["rev-parse", "HEAD"], stdout: "headA\n" },
225
+ ]);
226
+ await handleCodingDelta(
227
+ { subcommand: "get", sessionKey: "s1" },
228
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker1 }),
229
+ );
230
+
231
+ // Now head moved to headB with two commits.
232
+ const sep = "\x1f";
233
+ const log = [
234
+ `sha1${sep}feat: a`,
235
+ "",
236
+ "src/a.ts",
237
+ "",
238
+ `sha2${sep}fix: b`,
239
+ "",
240
+ "src/b.ts",
241
+ "",
242
+ ].join("\n");
243
+ const invoker2 = makeInvoker([
244
+ { args: ["rev-parse", "HEAD"], stdout: "headB\n" },
245
+ { args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "headA..headB"], stdout: log },
246
+ ]);
247
+ const result = await handleCodingDelta(
248
+ { subcommand: "get", sessionKey: "s1" },
249
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker2 }),
250
+ );
251
+ if (!("ok" in result) || !result.ok || result.kind !== "changed") {
252
+ assert.fail(`expected changed, got ${JSON.stringify(result)}`);
253
+ return;
254
+ }
255
+ assert.equal(result.delta.commits.length, 2);
256
+ assert.equal(result.delta.touchedFiles.length, 2);
257
+ assert.match(result.delta.summaryLine, /2 commits, 2 files touched/);
258
+ } finally {
259
+ await cleanup();
260
+ }
261
+ });
262
+
263
+ test("handler/get: unreachable prior head → tagged failure, state advances", async () => {
264
+ const { storage, cleanup } = await makeTempStorage();
265
+ try {
266
+ // Seed headA.
267
+ const invoker1 = makeInvoker([
268
+ { args: ["rev-parse", "HEAD"], stdout: "headA\n" },
269
+ ]);
270
+ await handleCodingDelta(
271
+ { subcommand: "get", sessionKey: "s1" },
272
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker1 }),
273
+ );
274
+
275
+ // Now head is headB but `git log headA..headB` fails (force-push).
276
+ const invoker2 = makeInvoker([
277
+ { args: ["rev-parse", "HEAD"], stdout: "headB\n" },
278
+ { args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "headA..headB"], stdout: "", exitCode: 128 },
279
+ ]);
280
+ const result = await handleCodingDelta(
281
+ { subcommand: "get", sessionKey: "s1" },
282
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker2 }),
283
+ );
284
+ if (!("ok" in result) || result.ok || result.code !== "unreachable_head") {
285
+ assert.fail(`expected unreachable_head, got ${JSON.stringify(result)}`);
286
+ return;
287
+ }
288
+ // nextState advanced so the NEXT call sees headB as the baseline.
289
+ assert.equal(result.nextState?.head, "headB");
290
+ } finally {
291
+ await cleanup();
292
+ }
293
+ });
294
+
295
+ test("handler/get: git rev-parse failure → git_failed, no state advance", async () => {
296
+ const { storage, cleanup } = await makeTempStorage();
297
+ try {
298
+ const invoker = makeInvoker([
299
+ { args: ["rev-parse", "HEAD"], stdout: "", exitCode: 128 },
300
+ ]);
301
+ const result = await handleCodingDelta(
302
+ { subcommand: "get", sessionKey: "s1" },
303
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker }),
304
+ );
305
+ if (!("ok" in result) || result.ok || result.code !== "git_failed") {
306
+ assert.fail(`expected git_failed, got ${JSON.stringify(result)}`);
307
+ return;
308
+ }
309
+ // No nextState when the head itself was unreadable.
310
+ assert.equal(result.nextState, undefined);
311
+ } finally {
312
+ await cleanup();
313
+ }
314
+ });
315
+
316
+ test("handler/get: transient git log failure (exit 127) → git_failed, state NOT advanced", async () => {
317
+ const { storage, cleanup } = await makeTempStorage();
318
+ try {
319
+ // Seed headA.
320
+ const invoker1 = makeInvoker([
321
+ { args: ["rev-parse", "HEAD"], stdout: "headA\n" },
322
+ ]);
323
+ await handleCodingDelta(
324
+ { subcommand: "get", sessionKey: "s1" },
325
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker1 }),
326
+ );
327
+
328
+ // Head is headB but git log times out (exit 127 = spawn/timeout).
329
+ const invoker2 = makeInvoker([
330
+ { args: ["rev-parse", "HEAD"], stdout: "headB\n" },
331
+ { args: ["log", "--reverse", "--pretty=format:%H\x1f%s", "--name-only", "headA..headB"], stdout: "", exitCode: 127 },
332
+ ]);
333
+ const result = await handleCodingDelta(
334
+ { subcommand: "get", sessionKey: "s1" },
335
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker2 }),
336
+ );
337
+ if (!("ok" in result) || result.ok || result.code !== "git_failed") {
338
+ assert.fail(`expected git_failed, got ${JSON.stringify(result)}`);
339
+ return;
340
+ }
341
+ // State marker must NOT advance — headA preserved for retry on next call.
342
+ // Read the state file back to verify.
343
+ const { readFile } = await import("node:fs/promises");
344
+ const { sessionDeltaStatePath } = await import("./session-delta.js");
345
+ const raw = await readFile(
346
+ sessionDeltaStatePath(storage.memoryDir, storage.namespace),
347
+ "utf8",
348
+ );
349
+ const persisted = JSON.parse(raw);
350
+ assert.equal(persisted.head, "headA", "transient failure must not advance the state marker");
351
+ } finally {
352
+ await cleanup();
353
+ }
354
+ });
355
+
356
+ // ──────────────────────────────────────────────────────────────────────────
357
+ // Rule 25 — state write happens AFTER compute
358
+ // ──────────────────────────────────────────────────────────────────────────
359
+
360
+ test("handler/get: state file is created after a successful first_run", async () => {
361
+ const { storage, cleanup } = await makeTempStorage();
362
+ try {
363
+ const invoker = makeInvoker([
364
+ { args: ["rev-parse", "HEAD"], stdout: "head1\n" },
365
+ ]);
366
+ await handleCodingDelta(
367
+ { subcommand: "get", sessionKey: "s1" },
368
+ makeContext({ resolveStorage: async () => storage, gitInvoker: invoker }),
369
+ );
370
+ const { readFile } = await import("node:fs/promises");
371
+ const raw = await readFile(
372
+ // The handler uses sessionDeltaStatePath(memoryDir, namespace).
373
+ // We import and call the same helper for parity.
374
+ (await import("./session-delta.js")).sessionDeltaStatePath(storage.memoryDir, storage.namespace),
375
+ "utf8",
376
+ );
377
+ const parsed = JSON.parse(raw);
378
+ assert.equal(parsed.head, "head1");
379
+ assert.ok(parsed.at);
380
+ } finally {
381
+ await cleanup();
382
+ }
383
+ });