gsd-pi 2.81.0-dev.72a81bdf3 → 2.82.0-dev.725028083

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 (135) hide show
  1. package/README.md +46 -28
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/browser-tools/tools/screenshot.js +1 -0
  4. package/dist/resources/extensions/browser-tools/tools/zoom.js +1 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +9 -0
  6. package/dist/resources/extensions/gsd/auto-prompts.js +11 -3
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +5 -0
  8. package/dist/resources/extensions/gsd/bootstrap/system-context.js +55 -12
  9. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
  10. package/dist/resources/extensions/gsd/commands-handlers.js +15 -2
  11. package/dist/resources/extensions/gsd/context-store.js +112 -0
  12. package/dist/resources/extensions/gsd/db-writer.js +150 -84
  13. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  14. package/dist/resources/extensions/gsd/doctor-git-checks.js +41 -6
  15. package/dist/resources/extensions/gsd/knowledge-backfill.js +144 -0
  16. package/dist/resources/extensions/gsd/knowledge-capture.js +136 -0
  17. package/dist/resources/extensions/gsd/knowledge-parser.js +154 -0
  18. package/dist/resources/extensions/gsd/knowledge-projection.js +210 -0
  19. package/dist/resources/extensions/gsd/markdown-renderer.js +6 -1
  20. package/dist/resources/extensions/gsd/memory-backfill.js +73 -17
  21. package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +222 -0
  22. package/dist/resources/extensions/gsd/prompts/system.md +2 -2
  23. package/dist/resources/extensions/gsd/provider-switch-observer.js +146 -0
  24. package/dist/resources/extensions/gsd/templates/knowledge.md +2 -2
  25. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.html +1 -1
  47. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  58. package/package.json +3 -2
  59. package/packages/daemon/package.json +2 -2
  60. package/packages/mcp-server/README.md +2 -0
  61. package/packages/mcp-server/package.json +2 -2
  62. package/packages/mcp-server/src/workflow-tools-parity.test.ts +244 -0
  63. package/packages/native/package.json +1 -1
  64. package/packages/pi-agent-core/package.json +1 -1
  65. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  66. package/packages/pi-ai/dist/index.d.ts +2 -2
  67. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  68. package/packages/pi-ai/dist/index.js +1 -1
  69. package/packages/pi-ai/dist/index.js.map +1 -1
  70. package/packages/pi-ai/dist/providers/transform-messages.d.ts +11 -0
  71. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  72. package/packages/pi-ai/dist/providers/transform-messages.js +20 -0
  73. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  74. package/packages/pi-ai/package.json +1 -1
  75. package/packages/pi-ai/src/index.ts +7 -2
  76. package/packages/pi-ai/src/providers/transform-messages.ts +24 -0
  77. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  78. package/packages/pi-coding-agent/dist/core/system-prompt.js +4 -4
  79. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts +2 -0
  81. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js +17 -0
  83. package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js.map +1 -0
  84. package/packages/pi-coding-agent/package.json +1 -1
  85. package/packages/pi-coding-agent/src/core/system-prompt.ts +4 -4
  86. package/packages/pi-coding-agent/src/tests/system-prompt-file-safety.test.ts +22 -0
  87. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-tui/package.json +1 -1
  89. package/packages/rpc-client/package.json +1 -1
  90. package/pkg/package.json +1 -1
  91. package/src/resources/extensions/browser-tools/tools/screenshot.ts +1 -0
  92. package/src/resources/extensions/browser-tools/tools/zoom.ts +1 -0
  93. package/src/resources/extensions/gsd/auto/phases.ts +14 -0
  94. package/src/resources/extensions/gsd/auto-prompts.ts +11 -3
  95. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
  96. package/src/resources/extensions/gsd/bootstrap/system-context.ts +58 -15
  97. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
  98. package/src/resources/extensions/gsd/commands-handlers.ts +19 -2
  99. package/src/resources/extensions/gsd/context-store.ts +120 -1
  100. package/src/resources/extensions/gsd/db-writer.ts +167 -84
  101. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  102. package/src/resources/extensions/gsd/doctor-git-checks.ts +44 -6
  103. package/src/resources/extensions/gsd/doctor-types.ts +2 -0
  104. package/src/resources/extensions/gsd/knowledge-backfill.ts +164 -0
  105. package/src/resources/extensions/gsd/knowledge-capture.ts +160 -0
  106. package/src/resources/extensions/gsd/knowledge-parser.ts +174 -0
  107. package/src/resources/extensions/gsd/knowledge-projection.ts +241 -0
  108. package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
  109. package/src/resources/extensions/gsd/memory-backfill.ts +89 -17
  110. package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +277 -0
  111. package/src/resources/extensions/gsd/prompts/system.md +2 -2
  112. package/src/resources/extensions/gsd/provider-switch-observer.ts +185 -0
  113. package/src/resources/extensions/gsd/templates/knowledge.md +2 -2
  114. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +75 -0
  115. package/src/resources/extensions/gsd/tests/browser-tools-compatibility-declarations.test.ts +62 -0
  116. package/src/resources/extensions/gsd/tests/context-store-decisions-from-memories.test.ts +312 -0
  117. package/src/resources/extensions/gsd/tests/db-writer.test.ts +13 -8
  118. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +453 -0
  119. package/src/resources/extensions/gsd/tests/decisions-stop-table-writes.test.ts +348 -0
  120. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +8 -4
  121. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +11 -7
  122. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +44 -0
  123. package/src/resources/extensions/gsd/tests/integration/integration-lifecycle.test.ts +13 -5
  124. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +323 -0
  125. package/src/resources/extensions/gsd/tests/knowledge-capture.test.ts +242 -0
  126. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -2
  127. package/src/resources/extensions/gsd/tests/load-knowledge-block-rules-only.test.ts +209 -0
  128. package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +316 -0
  129. package/src/resources/extensions/gsd/tests/plan-milestone-sketch-render.test.ts +157 -0
  130. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -0
  131. package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +252 -0
  132. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +16 -4
  133. package/src/resources/extensions/gsd/workflow-logger.ts +4 -0
  134. /package/dist/web/standalone/.next/static/{rIkMv4YSNlfSeqmGqWVns → KDRTXR-22LPCsa80X9dey}/_buildManifest.js +0 -0
  135. /package/dist/web/standalone/.next/static/{rIkMv4YSNlfSeqmGqWVns → KDRTXR-22LPCsa80X9dey}/_ssgManifest.js +0 -0
@@ -2548,6 +2548,81 @@ test("resolveAgentEndCancelled with errorContext passes it through to resolved p
2548
2548
  assert.equal(resolved.errorContext!.isTransient, true);
2549
2549
  });
2550
2550
 
2551
+ test("runUnitPhase pauses transient aborted cancellations instead of hard-stopping", async (t) => {
2552
+ _resetPendingResolve();
2553
+
2554
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-aborted-cancel-"));
2555
+ t.after(() => {
2556
+ rmSync(basePath, { recursive: true, force: true });
2557
+ });
2558
+
2559
+ const ctx = {
2560
+ ...makeMockCtx(),
2561
+ ui: {
2562
+ notify: () => {},
2563
+ setStatus: () => {},
2564
+ setWorkingMessage: () => {},
2565
+ },
2566
+ sessionManager: {
2567
+ getEntries: () => [],
2568
+ },
2569
+ modelRegistry: {
2570
+ getProviderAuthMode: () => undefined,
2571
+ isProviderRequestReady: () => true,
2572
+ },
2573
+ } as any;
2574
+ const pi = {
2575
+ ...makeMockPi(),
2576
+ sendMessage: () => {
2577
+ queueMicrotask(() => resolveAgentEndCancelled({
2578
+ message: "Claude Code process aborted by user",
2579
+ category: "aborted",
2580
+ isTransient: true,
2581
+ }));
2582
+ },
2583
+ } as any;
2584
+ const s = makeLoopSession({
2585
+ basePath,
2586
+ canonicalProjectRoot: basePath,
2587
+ originalBasePath: basePath,
2588
+ });
2589
+ const deps = makeMockDeps();
2590
+ let seq = 0;
2591
+
2592
+ const result = await runUnitPhase(
2593
+ { ctx, pi, s, deps, prefs: undefined, iteration: 1, flowId: "flow-aborted", nextSeq: () => ++seq },
2594
+ {
2595
+ unitType: "execute-task",
2596
+ unitId: "M001/S01/T01",
2597
+ prompt: "do work",
2598
+ finalPrompt: "do work",
2599
+ pauseAfterUatDispatch: false,
2600
+ state: {
2601
+ phase: "executing",
2602
+ activeMilestone: { id: "M001", title: "Milestone" },
2603
+ activeSlice: { id: "S01", title: "Slice" },
2604
+ activeTask: { id: "T01", title: "Task" },
2605
+ registry: [{ id: "M001", title: "Milestone", status: "active" }],
2606
+ recentDecisions: [],
2607
+ blockers: [],
2608
+ nextAction: "",
2609
+ progress: { milestones: { done: 0, total: 1 } },
2610
+ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
2611
+ } as any,
2612
+ mid: "M001",
2613
+ midTitle: "Milestone",
2614
+ isRetry: false,
2615
+ previousTier: undefined,
2616
+ },
2617
+ { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 },
2618
+ );
2619
+
2620
+ assert.equal(result.action, "break");
2621
+ assert.equal((result as any).reason, "unit-aborted-pause");
2622
+ assert.equal(deps.callLog.includes("pauseAuto"), true);
2623
+ assert.equal(deps.callLog.includes("stopAuto"), false);
2624
+ });
2625
+
2551
2626
  test("runUnitPhase pauses ghost completions before closeout and finalize side effects", async (t) => {
2552
2627
  _resetPendingResolve();
2553
2628
 
@@ -0,0 +1,62 @@
1
+ // GSD-2 — ADR-005 Phase 2: Verify browser tool compatibility declarations.
2
+ //
3
+ // Locks in the declarations that always-image-producing browser tools
4
+ // (browser_screenshot, browser_zoom_region) carry `producesImages: true` so
5
+ // the model-router filters them out on providers without imageToolResults
6
+ // (OpenAI completions/responses, Azure, Mistral, Ollama). Conditional-image
7
+ // browser tools (navigation, forms, refs, intent, interaction) must NOT
8
+ // declare producesImages — they only attach error screenshots and filtering
9
+ // them would lose the whole tool surface for OpenAI users.
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+
14
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
15
+
16
+ import { registerScreenshotTools } from "../../browser-tools/tools/screenshot.ts";
17
+ import { registerZoomTools } from "../../browser-tools/tools/zoom.ts";
18
+
19
+ interface CapturedToolDef {
20
+ name: string;
21
+ compatibility?: { producesImages?: boolean; schemaFeatures?: string[] };
22
+ }
23
+
24
+ function makeCapturingPi(): { pi: ExtensionAPI; tools: CapturedToolDef[] } {
25
+ const tools: CapturedToolDef[] = [];
26
+ const pi = {
27
+ registerTool(def: CapturedToolDef): void {
28
+ tools.push({ name: def.name, compatibility: def.compatibility });
29
+ },
30
+ } as unknown as ExtensionAPI;
31
+ return { pi, tools };
32
+ }
33
+
34
+ // Browser tool registration functions accept a `deps` object. None of the
35
+ // declarations under test reach for these deps at registration time — they
36
+ // only run inside execute(), which we never invoke. A bare object satisfies
37
+ // the type signature.
38
+ const stubDeps = {} as Parameters<typeof registerScreenshotTools>[1];
39
+
40
+ test("browser_screenshot declares producesImages: true", () => {
41
+ const { pi, tools } = makeCapturingPi();
42
+ registerScreenshotTools(pi, stubDeps);
43
+ const screenshot = tools.find((t) => t.name === "browser_screenshot");
44
+ assert.ok(screenshot, "browser_screenshot should be registered");
45
+ assert.equal(
46
+ screenshot.compatibility?.producesImages,
47
+ true,
48
+ "browser_screenshot must declare producesImages so it is filtered on providers without imageToolResults",
49
+ );
50
+ });
51
+
52
+ test("browser_zoom_region declares producesImages: true", () => {
53
+ const { pi, tools } = makeCapturingPi();
54
+ registerZoomTools(pi, stubDeps);
55
+ const zoom = tools.find((t) => t.name === "browser_zoom_region");
56
+ assert.ok(zoom, "browser_zoom_region should be registered");
57
+ assert.equal(
58
+ zoom.compatibility?.producesImages,
59
+ true,
60
+ "browser_zoom_region must declare producesImages so it is filtered on providers without imageToolResults",
61
+ );
62
+ });
@@ -0,0 +1,312 @@
1
+ // ADR-013 Phase 6 cutover (Stage 1) — queryDecisionsFromMemories parity test.
2
+ //
3
+ // Verifies that reading active decisions from the `memories` table returns
4
+ // the same Decision[] shape and content as the legacy `queryDecisions` read
5
+ // from the `decisions` table, once Phase 5 dual-write has populated both
6
+ // surfaces. Lock-in regression for the prompt-inline read path which was
7
+ // switched to the memories source in auto-prompts.ts:inlineDecisionsFromDb.
8
+ //
9
+ // Scope of parity: ACTIVE decisions only. Superseded rows are intentionally
10
+ // skipped by the existing backfill, so this test does not assert parity for
11
+ // the supersedes-chain — that gap is acknowledged in
12
+ // queryDecisionsFromMemories' contract and tracked for Stage 2/3.
13
+
14
+ import test from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { tmpdir } from "node:os";
19
+
20
+ import {
21
+ closeDatabase,
22
+ insertDecision,
23
+ openDatabase,
24
+ } from "../gsd-db.ts";
25
+ import { saveDecisionToDb } from "../db-writer.ts";
26
+ import {
27
+ queryDecisions,
28
+ queryDecisionsFromMemories,
29
+ } from "../context-store.ts";
30
+
31
+ function makeTmpBase(): string {
32
+ const base = mkdtempSync(join(tmpdir(), "gsd-decisions-memories-"));
33
+ mkdirSync(join(base, ".gsd"), { recursive: true });
34
+ openDatabase(join(base, ".gsd", "gsd.db"));
35
+ return base;
36
+ }
37
+
38
+ function cleanup(base: string): void {
39
+ try {
40
+ closeDatabase();
41
+ } catch {
42
+ /* noop */
43
+ }
44
+ try {
45
+ rmSync(base, { recursive: true, force: true });
46
+ } catch {
47
+ /* noop */
48
+ }
49
+ }
50
+
51
+ async function seedDecision(
52
+ base: string,
53
+ fields: {
54
+ when_context: string;
55
+ scope: string;
56
+ decision: string;
57
+ choice: string;
58
+ rationale: string;
59
+ revisable?: string;
60
+ made_by?: "human" | "agent" | "collaborative";
61
+ },
62
+ ): Promise<string> {
63
+ // saveDecisionToDb writes ONLY to memories post-Stage-3. For parity tests
64
+ // comparing the legacy `queryDecisions` against `queryDecisionsFromMemories`,
65
+ // mirror the same row into the legacy decisions table directly so both
66
+ // surfaces hold the same data and the parity assertion is well-defined.
67
+ const result = await saveDecisionToDb(
68
+ {
69
+ when_context: fields.when_context,
70
+ scope: fields.scope,
71
+ decision: fields.decision,
72
+ choice: fields.choice,
73
+ rationale: fields.rationale,
74
+ revisable: fields.revisable ?? "Yes",
75
+ made_by: fields.made_by ?? "agent",
76
+ },
77
+ base,
78
+ );
79
+ insertDecision({
80
+ id: result.id,
81
+ when_context: fields.when_context,
82
+ scope: fields.scope,
83
+ decision: fields.decision,
84
+ choice: fields.choice,
85
+ rationale: fields.rationale,
86
+ revisable: fields.revisable ?? "Yes",
87
+ made_by: fields.made_by ?? "agent",
88
+ superseded_by: null,
89
+ });
90
+ return result.id;
91
+ }
92
+
93
+ test("queryDecisionsFromMemories returns empty when no decisions exist", () => {
94
+ const base = makeTmpBase();
95
+ try {
96
+ assert.deepEqual(queryDecisionsFromMemories(), []);
97
+ assert.deepEqual(queryDecisionsFromMemories({ milestoneId: "M001" }), []);
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
103
+ test("queryDecisionsFromMemories matches queryDecisions for a single active decision", async () => {
104
+ const base = makeTmpBase();
105
+ try {
106
+ await seedDecision(base, {
107
+ when_context: "M001 discuss phase",
108
+ scope: "M001",
109
+ decision: "Adopt SQLite for persistence",
110
+ choice: "better-sqlite3",
111
+ rationale: "Native, synchronous, well-supported",
112
+ });
113
+
114
+ const fromDecisions = queryDecisions();
115
+ const fromMemories = queryDecisionsFromMemories();
116
+
117
+ assert.equal(fromDecisions.length, 1);
118
+ assert.equal(fromMemories.length, 1);
119
+
120
+ // Compare the user-visible Decision fields. seq differs across tables
121
+ // (memories.seq vs decisions.seq) so it's intentionally excluded.
122
+ const { seq: _seq1, ...d1 } = fromDecisions[0]!;
123
+ const { seq: _seq2, ...d2 } = fromMemories[0]!;
124
+ assert.deepEqual(d1, d2);
125
+ } finally {
126
+ cleanup(base);
127
+ }
128
+ });
129
+
130
+ test("queryDecisionsFromMemories preserves decision order across multiple writes", async () => {
131
+ const base = makeTmpBase();
132
+ try {
133
+ const id1 = await seedDecision(base, {
134
+ when_context: "M001 discuss",
135
+ scope: "M001",
136
+ decision: "First decision",
137
+ choice: "A",
138
+ rationale: "first",
139
+ });
140
+ const id2 = await seedDecision(base, {
141
+ when_context: "M001 plan",
142
+ scope: "M001",
143
+ decision: "Second decision",
144
+ choice: "B",
145
+ rationale: "second",
146
+ });
147
+ const id3 = await seedDecision(base, {
148
+ when_context: "M002 discuss",
149
+ scope: "M002",
150
+ decision: "Third decision",
151
+ choice: "C",
152
+ rationale: "third",
153
+ });
154
+
155
+ const fromMemories = queryDecisionsFromMemories();
156
+ assert.equal(fromMemories.length, 3);
157
+ assert.deepEqual(
158
+ fromMemories.map((d) => d.id),
159
+ [id1, id2, id3],
160
+ );
161
+ } finally {
162
+ cleanup(base);
163
+ }
164
+ });
165
+
166
+ test("queryDecisionsFromMemories filters by milestoneId (substring match on when_context)", async () => {
167
+ const base = makeTmpBase();
168
+ try {
169
+ await seedDecision(base, {
170
+ when_context: "M001 discuss",
171
+ scope: "M001",
172
+ decision: "M001 decision",
173
+ choice: "A",
174
+ rationale: "x",
175
+ });
176
+ await seedDecision(base, {
177
+ when_context: "M002 plan",
178
+ scope: "M002",
179
+ decision: "M002 decision",
180
+ choice: "B",
181
+ rationale: "y",
182
+ });
183
+ await seedDecision(base, {
184
+ when_context: "M001 execute",
185
+ scope: "M001-S01",
186
+ decision: "M001 follow-up",
187
+ choice: "C",
188
+ rationale: "z",
189
+ });
190
+ await seedDecision(base, {
191
+ when_context: "M003 plan",
192
+ scope: "M003",
193
+ decision: "Use M001 as precedent",
194
+ choice: "D",
195
+ rationale: "Mentions M001 outside when_context",
196
+ });
197
+
198
+ const m001 = queryDecisionsFromMemories({ milestoneId: "M001" });
199
+ assert.equal(m001.length, 2, "two decisions reference M001 in when_context");
200
+ assert.ok(m001.every((d) => d.when_context.includes("M001")));
201
+
202
+ const m002 = queryDecisionsFromMemories({ milestoneId: "M002" });
203
+ assert.equal(m002.length, 1);
204
+ assert.equal(m002[0]?.decision, "M002 decision");
205
+ } finally {
206
+ cleanup(base);
207
+ }
208
+ });
209
+
210
+ test("queryDecisionsFromMemories filters by scope (exact match, no prefix collisions)", async () => {
211
+ const base = makeTmpBase();
212
+ try {
213
+ await seedDecision(base, {
214
+ when_context: "M001 discuss",
215
+ scope: "M001",
216
+ decision: "Milestone-level",
217
+ choice: "A",
218
+ rationale: "x",
219
+ });
220
+ await seedDecision(base, {
221
+ when_context: "M001 plan",
222
+ scope: "M001-S01",
223
+ decision: "Slice-level",
224
+ choice: "B",
225
+ rationale: "y",
226
+ });
227
+ await seedDecision(base, {
228
+ when_context: "M001 plan",
229
+ scope: "M001-S02",
230
+ decision: "Different slice",
231
+ choice: "C",
232
+ rationale: "z",
233
+ });
234
+
235
+ // Exact-scope filter must not match prefix-similar values.
236
+ const milestoneScope = queryDecisionsFromMemories({ scope: "M001" });
237
+ assert.equal(milestoneScope.length, 1, "scope=M001 must not match M001-S01 / M001-S02");
238
+ assert.equal(milestoneScope[0]?.scope, "M001");
239
+
240
+ const sliceScope = queryDecisionsFromMemories({ scope: "M001-S01" });
241
+ assert.equal(sliceScope.length, 1);
242
+ assert.equal(sliceScope[0]?.scope, "M001-S01");
243
+ } finally {
244
+ cleanup(base);
245
+ }
246
+ });
247
+
248
+ test("queryDecisionsFromMemories matches queryDecisions for combined milestoneId + scope filters", async () => {
249
+ const base = makeTmpBase();
250
+ try {
251
+ await seedDecision(base, {
252
+ when_context: "M001 discuss",
253
+ scope: "M001",
254
+ decision: "A",
255
+ choice: "1",
256
+ rationale: "x",
257
+ });
258
+ await seedDecision(base, {
259
+ when_context: "M001 plan",
260
+ scope: "M001-S01",
261
+ decision: "B",
262
+ choice: "2",
263
+ rationale: "y",
264
+ });
265
+ await seedDecision(base, {
266
+ when_context: "M002 discuss",
267
+ scope: "M002",
268
+ decision: "C",
269
+ choice: "3",
270
+ rationale: "z",
271
+ });
272
+
273
+ const opts = { milestoneId: "M001", scope: "M001-S01" };
274
+ const fromDecisions = queryDecisions(opts);
275
+ const fromMemories = queryDecisionsFromMemories(opts);
276
+
277
+ assert.equal(fromDecisions.length, fromMemories.length);
278
+ assert.equal(fromMemories.length, 1);
279
+ assert.equal(fromMemories[0]?.id, fromDecisions[0]?.id);
280
+ assert.equal(fromMemories[0]?.scope, "M001-S01");
281
+ } finally {
282
+ cleanup(base);
283
+ }
284
+ });
285
+
286
+ test("queryDecisionsFromMemories ignores memories without a sourceDecisionId marker", async () => {
287
+ const base = makeTmpBase();
288
+ try {
289
+ // Insert a user-authored memory (no sourceDecisionId) — must not appear in
290
+ // the decisions-from-memories projection.
291
+ const { createMemory } = await import("../memory-store.ts");
292
+ createMemory({
293
+ category: "architecture",
294
+ content: "User-authored architecture note, not derived from a decision",
295
+ scope: "project",
296
+ });
297
+
298
+ await seedDecision(base, {
299
+ when_context: "M001 discuss",
300
+ scope: "M001",
301
+ decision: "Real decision",
302
+ choice: "A",
303
+ rationale: "x",
304
+ });
305
+
306
+ const fromMemories = queryDecisionsFromMemories();
307
+ assert.equal(fromMemories.length, 1, "user-authored memory must not appear as a decision");
308
+ assert.equal(fromMemories[0]?.decision, "Real decision");
309
+ } finally {
310
+ cleanup(base);
311
+ }
312
+ });
@@ -27,6 +27,7 @@ import {
27
27
  saveArtifactToDb,
28
28
  extractDeferredSliceRef,
29
29
  } from '../db-writer.ts';
30
+ import { getAllDecisionsFromMemories } from '../context-store.ts';
30
31
  import type { Decision, Requirement } from '../types.ts';
31
32
 
32
33
  // ═══════════════════════════════════════════════════════════════════════════
@@ -326,11 +327,14 @@ describe('db-writer', () => {
326
327
 
327
328
  assert.deepStrictEqual(result.id, 'D001', 'saveDecisionToDb returns D001 as first ID');
328
329
 
329
- // Verify DB state
330
- const dbDecision = getDecisionById('D001');
331
- assert.ok(!!dbDecision, 'decision exists in DB after save');
332
- assert.deepStrictEqual(dbDecision?.scope, 'arch', 'DB decision has correct scope');
333
- assert.deepStrictEqual(dbDecision?.choice, 'Option A', 'DB decision has correct choice');
330
+ // ADR-013 Stage 3: decisions land in memories, not the legacy table.
331
+ const memoryDecisions = getAllDecisionsFromMemories();
332
+ assert.equal(memoryDecisions.length, 1, 'one memory row exists after save');
333
+ const memDecision = memoryDecisions[0];
334
+ assert.ok(memDecision, 'memory decision exists after save');
335
+ assert.equal(memDecision.id, 'D001');
336
+ assert.equal(memDecision.scope, 'arch', 'memory decision has correct scope');
337
+ assert.equal(memDecision.choice, 'Option A', 'memory decision has correct choice');
334
338
 
335
339
  // Verify markdown file was written
336
340
  const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
@@ -394,10 +398,11 @@ describe('db-writer', () => {
394
398
  assert.match(id, /^D\d{3}$/, `ID ${id} should match D### pattern`);
395
399
  }
396
400
 
397
- // Verify all 5 exist in DB
401
+ // ADR-013 Stage 3: verify all 5 exist in the memories table (decisions
402
+ // table receives no writes from saveDecisionToDb post-cutover).
403
+ const memoryIds = new Set(getAllDecisionsFromMemories().map((d) => d.id));
398
404
  for (const id of ids) {
399
- const row = getDecisionById(id);
400
- assert.ok(row, `Decision ${id} should exist in DB`);
405
+ assert.ok(memoryIds.has(id), `Decision ${id} should exist in memories`);
401
406
  }
402
407
  } finally {
403
408
  closeDatabase();