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.
- package/README.md +46 -28
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/browser-tools/tools/screenshot.js +1 -0
- package/dist/resources/extensions/browser-tools/tools/zoom.js +1 -0
- package/dist/resources/extensions/gsd/auto/phases.js +9 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +11 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +5 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +55 -12
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +15 -2
- package/dist/resources/extensions/gsd/context-store.js +112 -0
- package/dist/resources/extensions/gsd/db-writer.js +150 -84
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/doctor-git-checks.js +41 -6
- package/dist/resources/extensions/gsd/knowledge-backfill.js +144 -0
- package/dist/resources/extensions/gsd/knowledge-capture.js +136 -0
- package/dist/resources/extensions/gsd/knowledge-parser.js +154 -0
- package/dist/resources/extensions/gsd/knowledge-projection.js +210 -0
- package/dist/resources/extensions/gsd/markdown-renderer.js +6 -1
- package/dist/resources/extensions/gsd/memory-backfill.js +73 -17
- package/dist/resources/extensions/gsd/memory-consolidation-scanner.js +222 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -2
- package/dist/resources/extensions/gsd/provider-switch-observer.js +146 -0
- package/dist/resources/extensions/gsd/templates/knowledge.md +2 -2
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +3 -2
- package/packages/daemon/package.json +2 -2
- package/packages/mcp-server/README.md +2 -0
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/workflow-tools-parity.test.ts +244 -0
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/index.d.ts +2 -2
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -1
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.d.ts +11 -0
- package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/transform-messages.js +20 -0
- package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/src/index.ts +7 -2
- package/packages/pi-ai/src/providers/transform-messages.ts +24 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +4 -4
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js +17 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-file-safety.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/system-prompt.ts +4 -4
- package/packages/pi-coding-agent/src/tests/system-prompt-file-safety.test.ts +22 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +1 -0
- package/src/resources/extensions/browser-tools/tools/zoom.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +11 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +58 -15
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +19 -2
- package/src/resources/extensions/gsd/context-store.ts +120 -1
- package/src/resources/extensions/gsd/db-writer.ts +167 -84
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/doctor-git-checks.ts +44 -6
- package/src/resources/extensions/gsd/doctor-types.ts +2 -0
- package/src/resources/extensions/gsd/knowledge-backfill.ts +164 -0
- package/src/resources/extensions/gsd/knowledge-capture.ts +160 -0
- package/src/resources/extensions/gsd/knowledge-parser.ts +174 -0
- package/src/resources/extensions/gsd/knowledge-projection.ts +241 -0
- package/src/resources/extensions/gsd/markdown-renderer.ts +6 -1
- package/src/resources/extensions/gsd/memory-backfill.ts +89 -17
- package/src/resources/extensions/gsd/memory-consolidation-scanner.ts +277 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -2
- package/src/resources/extensions/gsd/provider-switch-observer.ts +185 -0
- package/src/resources/extensions/gsd/templates/knowledge.md +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/browser-tools-compatibility-declarations.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/context-store-decisions-from-memories.test.ts +312 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +13 -8
- package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +453 -0
- package/src/resources/extensions/gsd/tests/decisions-stop-table-writes.test.ts +348 -0
- package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +11 -7
- package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/integration/integration-lifecycle.test.ts +13 -5
- package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +323 -0
- package/src/resources/extensions/gsd/tests/knowledge-capture.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -2
- package/src/resources/extensions/gsd/tests/load-knowledge-block-rules-only.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/memory-consolidation-scanner.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/plan-milestone-sketch-render.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/provider-switch-observer.test.ts +252 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +16 -4
- package/src/resources/extensions/gsd/workflow-logger.ts +4 -0
- /package/dist/web/standalone/.next/static/{rIkMv4YSNlfSeqmGqWVns → KDRTXR-22LPCsa80X9dey}/_buildManifest.js +0 -0
- /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
|
-
//
|
|
330
|
-
const
|
|
331
|
-
assert.
|
|
332
|
-
|
|
333
|
-
assert.
|
|
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
|
-
//
|
|
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
|
-
|
|
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();
|