gsd-pi 2.24.0 → 2.25.0
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 +2 -1
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +0 -9
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/dist/resources/extensions/gsd/auto.ts +184 -36
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/doctor.ts +2 -0
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
- package/dist/resources/extensions/gsd/index.ts +14 -1
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +39 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +38 -1
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/types.ts +19 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/src/resources/extensions/gsd/auto.ts +184 -36
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/doctor.ts +2 -0
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +34 -12
- package/src/resources/extensions/gsd/index.ts +14 -1
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +15 -5
|
@@ -153,6 +153,64 @@ async function main(): Promise<void> {
|
|
|
153
153
|
// After teardown, originalBase should be null
|
|
154
154
|
assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
|
|
155
155
|
|
|
156
|
+
// ─── #778: reconcile plan checkboxes on re-attach ─────────────────
|
|
157
|
+
console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
|
|
158
|
+
{
|
|
159
|
+
// Simulate: T01 [x] was committed to milestone branch, T02 [x] was
|
|
160
|
+
// written to project root by syncStateToProjectRoot() but the
|
|
161
|
+
// auto-commit crashed before it fired. On restart the worktree is
|
|
162
|
+
// re-created from the milestone branch HEAD (T02 still [ ]).
|
|
163
|
+
// reconcilePlanCheckboxes should forward-apply T02 [x] from the root.
|
|
164
|
+
|
|
165
|
+
const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
|
|
166
|
+
const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
|
|
167
|
+
const { mkdirSync: mkdir, writeFileSync: write, readFileSync: read } = await import("node:fs");
|
|
168
|
+
|
|
169
|
+
// Plan on integration branch (project root): T01 [x], T02 [x]
|
|
170
|
+
mkdir(planDir, { recursive: true });
|
|
171
|
+
write(
|
|
172
|
+
join(tempDir, planRelPath),
|
|
173
|
+
"# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Write integration-branch plan to git so milestone branch starts from it
|
|
177
|
+
run(`git add .`, tempDir);
|
|
178
|
+
run(`git commit -m "add plan with T01 and T02 checked" --allow-empty`, tempDir);
|
|
179
|
+
|
|
180
|
+
// Create milestone branch with only T01 [x] (simulating crash before T02 commit)
|
|
181
|
+
const milestoneBranch = "milestone/M004";
|
|
182
|
+
run(`git checkout -b ${milestoneBranch}`, tempDir);
|
|
183
|
+
mkdir(planDir, { recursive: true });
|
|
184
|
+
write(
|
|
185
|
+
join(tempDir, planRelPath),
|
|
186
|
+
"# S01 Plan\n- [x] **T01:** task one\n- [ ] **T02:** task two\n- [ ] **T03:** task three\n",
|
|
187
|
+
);
|
|
188
|
+
run(`git add .`, tempDir);
|
|
189
|
+
run(`git commit -m "milestone: only T01 checked"`, tempDir);
|
|
190
|
+
run(`git checkout main`, tempDir);
|
|
191
|
+
|
|
192
|
+
// Restore project root plan (T01+T02 [x]) — simulates syncStateToProjectRoot
|
|
193
|
+
write(
|
|
194
|
+
join(tempDir, planRelPath),
|
|
195
|
+
"# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
|
|
199
|
+
const wtPath = createAutoWorktree(tempDir, "M004");
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const wtPlanPath = join(wtPath, planRelPath);
|
|
203
|
+
assertTrue(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
|
|
204
|
+
|
|
205
|
+
const wtPlan = read(wtPlanPath, "utf-8");
|
|
206
|
+
assertTrue(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
|
|
207
|
+
assertTrue(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
|
|
208
|
+
assertTrue(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
|
|
209
|
+
} finally {
|
|
210
|
+
teardownAutoWorktree(tempDir, "M004");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
156
214
|
} finally {
|
|
157
215
|
// Always restore cwd and clean up
|
|
158
216
|
process.chdir(savedCwd);
|
|
@@ -5,6 +5,7 @@ import { execSync } from "node:child_process";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
inferCommitType,
|
|
8
|
+
buildTaskCommitMessage,
|
|
8
9
|
GitServiceImpl,
|
|
9
10
|
RUNTIME_EXCLUSION_PATHS,
|
|
10
11
|
VALID_BRANCH_NAME,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type GitPreferences,
|
|
15
16
|
type CommitOptions,
|
|
16
17
|
type PreMergeCheckResult,
|
|
18
|
+
type TaskCommitContext,
|
|
17
19
|
} from "../git-service.ts";
|
|
18
20
|
import { createTestContext } from './test-helpers.ts';
|
|
19
21
|
|
|
@@ -188,6 +190,58 @@ async function main(): Promise<void> {
|
|
|
188
190
|
"'prefix' does not match 'fix' — word boundary prevents partial match"
|
|
189
191
|
);
|
|
190
192
|
|
|
193
|
+
// ─── inferCommitType with oneLiner ──────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
console.log("\n=== inferCommitType with oneLiner ===");
|
|
196
|
+
|
|
197
|
+
assertEq(
|
|
198
|
+
inferCommitType("implement dashboard", "Fixed rendering bug in sidebar"),
|
|
199
|
+
"fix",
|
|
200
|
+
"one-liner with 'fixed' overrides generic title → fix"
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
assertEq(
|
|
204
|
+
inferCommitType("add search", "Optimized query performance with caching"),
|
|
205
|
+
"perf",
|
|
206
|
+
"one-liner with 'performance' and 'caching' → perf"
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ─── buildTaskCommitMessage ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
console.log("\n=== buildTaskCommitMessage ===");
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
const msg = buildTaskCommitMessage({
|
|
215
|
+
taskId: "S01/T02",
|
|
216
|
+
taskTitle: "implement user authentication",
|
|
217
|
+
oneLiner: "Added JWT-based auth with refresh token rotation",
|
|
218
|
+
keyFiles: ["src/auth.ts", "src/middleware/jwt.ts"],
|
|
219
|
+
});
|
|
220
|
+
assertTrue(msg.startsWith("feat(S01/T02):"), "message starts with type(scope)");
|
|
221
|
+
assertTrue(msg.includes("JWT-based auth"), "message includes one-liner content");
|
|
222
|
+
assertTrue(msg.includes("- src/auth.ts"), "message body includes key files");
|
|
223
|
+
assertTrue(msg.includes("- src/middleware/jwt.ts"), "message body includes second key file");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
{
|
|
227
|
+
const msg = buildTaskCommitMessage({
|
|
228
|
+
taskId: "S02/T01",
|
|
229
|
+
taskTitle: "fix login redirect bug",
|
|
230
|
+
});
|
|
231
|
+
assertTrue(msg.startsWith("fix(S02/T01):"), "infers fix type from title");
|
|
232
|
+
assertTrue(msg.includes("fix login redirect bug"), "uses task title when no one-liner");
|
|
233
|
+
assertTrue(!msg.includes("\n"), "no body when no key files");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
{
|
|
237
|
+
const msg = buildTaskCommitMessage({
|
|
238
|
+
taskId: "S01/T03",
|
|
239
|
+
taskTitle: "add tests",
|
|
240
|
+
oneLiner: "Unit tests for auth module with coverage",
|
|
241
|
+
});
|
|
242
|
+
assertTrue(msg.startsWith("test(S01/T03):"), "infers test type");
|
|
243
|
+
}
|
|
244
|
+
|
|
191
245
|
// ─── RUNTIME_EXCLUSION_PATHS ───────────────────────────────────────────
|
|
192
246
|
|
|
193
247
|
console.log("\n=== RUNTIME_EXCLUSION_PATHS ===");
|
|
@@ -430,13 +484,25 @@ async function main(): Promise<void> {
|
|
|
430
484
|
const svc = new GitServiceImpl(repo);
|
|
431
485
|
|
|
432
486
|
createFile(repo, "src/new-feature.ts", "export const x = 1;");
|
|
433
|
-
const msg = svc.autoCommit("task", "T01");
|
|
434
487
|
|
|
435
|
-
|
|
488
|
+
// Without task context, autoCommit uses generic chore message
|
|
489
|
+
const msg = svc.autoCommit("task", "T01");
|
|
490
|
+
assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns generic format without task context");
|
|
436
491
|
|
|
437
|
-
// Verify the commit exists
|
|
438
492
|
const log = run("git log --oneline -1", repo);
|
|
439
|
-
assertTrue(log.includes("chore(T01): auto-commit after task"), "commit message is in git log");
|
|
493
|
+
assertTrue(log.includes("chore(T01): auto-commit after task"), "generic commit message is in git log");
|
|
494
|
+
|
|
495
|
+
// With task context, autoCommit uses meaningful message
|
|
496
|
+
createFile(repo, "src/auth.ts", "export function login() {}");
|
|
497
|
+
const msg2 = svc.autoCommit("task", "S01/T02", [], {
|
|
498
|
+
taskId: "S01/T02",
|
|
499
|
+
taskTitle: "implement user authentication endpoint",
|
|
500
|
+
oneLiner: "Added JWT-based auth with refresh token rotation",
|
|
501
|
+
keyFiles: ["src/auth.ts"],
|
|
502
|
+
});
|
|
503
|
+
assertTrue(msg2 !== null, "autoCommit with task context returns a message");
|
|
504
|
+
assertTrue(msg2!.startsWith("feat(S01/T02):"), "meaningful commit uses feat type and scope");
|
|
505
|
+
assertTrue(msg2!.includes("JWT-based auth"), "meaningful commit includes one-liner content");
|
|
440
506
|
|
|
441
507
|
rmSync(repo, { recursive: true, force: true });
|
|
442
508
|
}
|
|
@@ -65,8 +65,8 @@ console.log('\n=== gsd-db: fresh DB schema init (memory) ===');
|
|
|
65
65
|
|
|
66
66
|
// Check schema_version table
|
|
67
67
|
const adapter = _getAdapter()!;
|
|
68
|
-
const version = adapter.prepare('SELECT version FROM schema_version').get();
|
|
69
|
-
assertEq(version?.['version'],
|
|
68
|
+
const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
69
|
+
assertEq(version?.['version'], 3, 'schema version should be 3');
|
|
70
70
|
|
|
71
71
|
// Check tables exist by querying them
|
|
72
72
|
const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
|
|
@@ -350,12 +350,11 @@ console.log('=== md-importer: missing file handling ===');
|
|
|
350
350
|
console.log('=== md-importer: schema v1→v2 migration ===');
|
|
351
351
|
|
|
352
352
|
{
|
|
353
|
-
// This test verifies that opening a
|
|
354
|
-
// (The actual migration is tested via the gsd-db.test.ts schema version assertion = 2)
|
|
353
|
+
// This test verifies that opening a fresh DB auto-migrates to current schema version
|
|
355
354
|
openDatabase(':memory:');
|
|
356
355
|
const adapter = _getAdapter();
|
|
357
356
|
const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
358
|
-
assertEq(version?.v,
|
|
357
|
+
assertEq(version?.v, 3, 'new DB should be at schema version 3');
|
|
359
358
|
|
|
360
359
|
// Artifacts table should exist
|
|
361
360
|
const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createTestContext } from './test-helpers.ts';
|
|
2
|
+
import { parseMemoryResponse, _resetExtractionState } from '../memory-extractor.ts';
|
|
3
|
+
import {
|
|
4
|
+
openDatabase,
|
|
5
|
+
closeDatabase,
|
|
6
|
+
} from '../gsd-db.ts';
|
|
7
|
+
import {
|
|
8
|
+
getActiveMemories,
|
|
9
|
+
applyMemoryActions,
|
|
10
|
+
getActiveMemoriesRanked,
|
|
11
|
+
} from '../memory-store.ts';
|
|
12
|
+
import type { MemoryAction } from '../memory-store.ts';
|
|
13
|
+
|
|
14
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
15
|
+
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
// memory-extractor: parse valid JSON response
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
console.log('\n=== memory-extractor: parse valid JSON ===');
|
|
21
|
+
{
|
|
22
|
+
const response = JSON.stringify([
|
|
23
|
+
{ action: 'CREATE', category: 'gotcha', content: 'esbuild drops binaries', confidence: 0.85 },
|
|
24
|
+
{ action: 'REINFORCE', id: 'MEM001' },
|
|
25
|
+
{ action: 'UPDATE', id: 'MEM002', content: 'revised content' },
|
|
26
|
+
{ action: 'SUPERSEDE', id: 'MEM003', superseded_by: 'MEM004' },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const actions = parseMemoryResponse(response);
|
|
30
|
+
assertEq(actions.length, 4, 'should parse 4 actions');
|
|
31
|
+
assertEq(actions[0].action, 'CREATE', 'first action should be CREATE');
|
|
32
|
+
assertEq((actions[0] as any).category, 'gotcha', 'CREATE category');
|
|
33
|
+
assertEq((actions[0] as any).confidence, 0.85, 'CREATE confidence');
|
|
34
|
+
assertEq(actions[1].action, 'REINFORCE', 'second action should be REINFORCE');
|
|
35
|
+
assertEq(actions[2].action, 'UPDATE', 'third action should be UPDATE');
|
|
36
|
+
assertEq(actions[3].action, 'SUPERSEDE', 'fourth action should be SUPERSEDE');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
// memory-extractor: parse fenced JSON response
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
console.log('\n=== memory-extractor: parse fenced JSON ===');
|
|
44
|
+
{
|
|
45
|
+
const response = '```json\n[\n {"action": "CREATE", "category": "convention", "content": "test memory"}\n]\n```';
|
|
46
|
+
|
|
47
|
+
const actions = parseMemoryResponse(response);
|
|
48
|
+
assertEq(actions.length, 1, 'should parse 1 action from fenced JSON');
|
|
49
|
+
assertEq(actions[0].action, 'CREATE', 'action should be CREATE');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
|
+
// memory-extractor: parse empty array response
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
console.log('\n=== memory-extractor: parse empty array ===');
|
|
57
|
+
{
|
|
58
|
+
const actions = parseMemoryResponse('[]');
|
|
59
|
+
assertEq(actions.length, 0, 'empty array should parse to empty actions');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
// memory-extractor: parse malformed response
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
console.log('\n=== memory-extractor: malformed responses ===');
|
|
67
|
+
{
|
|
68
|
+
assertEq(parseMemoryResponse('not json at all'), [], 'garbage text should return []');
|
|
69
|
+
assertEq(parseMemoryResponse('{"action": "CREATE"}'), [], 'non-array should return []');
|
|
70
|
+
assertEq(parseMemoryResponse(''), [], 'empty string should return []');
|
|
71
|
+
assertEq(parseMemoryResponse('```\nbroken\n```'), [], 'fenced non-JSON should return []');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
// memory-extractor: validation of required fields
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
console.log('\n=== memory-extractor: field validation ===');
|
|
79
|
+
{
|
|
80
|
+
const response = JSON.stringify([
|
|
81
|
+
// Valid CREATE
|
|
82
|
+
{ action: 'CREATE', category: 'gotcha', content: 'valid' },
|
|
83
|
+
// Invalid CREATE — missing content
|
|
84
|
+
{ action: 'CREATE', category: 'gotcha' },
|
|
85
|
+
// Invalid CREATE — missing category
|
|
86
|
+
{ action: 'CREATE', content: 'no category' },
|
|
87
|
+
// Valid REINFORCE
|
|
88
|
+
{ action: 'REINFORCE', id: 'MEM001' },
|
|
89
|
+
// Invalid REINFORCE — missing id
|
|
90
|
+
{ action: 'REINFORCE' },
|
|
91
|
+
// Valid UPDATE
|
|
92
|
+
{ action: 'UPDATE', id: 'MEM002', content: 'new content' },
|
|
93
|
+
// Invalid UPDATE — missing content
|
|
94
|
+
{ action: 'UPDATE', id: 'MEM002' },
|
|
95
|
+
// Valid SUPERSEDE
|
|
96
|
+
{ action: 'SUPERSEDE', id: 'MEM001', superseded_by: 'MEM002' },
|
|
97
|
+
// Invalid SUPERSEDE — missing superseded_by
|
|
98
|
+
{ action: 'SUPERSEDE', id: 'MEM001' },
|
|
99
|
+
// Unknown action
|
|
100
|
+
{ action: 'DELETE', id: 'MEM001' },
|
|
101
|
+
// Null entry
|
|
102
|
+
null,
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const actions = parseMemoryResponse(response);
|
|
106
|
+
assertEq(actions.length, 4, 'should only accept 4 valid actions');
|
|
107
|
+
assertEq(actions[0].action, 'CREATE', 'first valid is CREATE');
|
|
108
|
+
assertEq(actions[1].action, 'REINFORCE', 'second valid is REINFORCE');
|
|
109
|
+
assertEq(actions[2].action, 'UPDATE', 'third valid is UPDATE');
|
|
110
|
+
assertEq(actions[3].action, 'SUPERSEDE', 'fourth valid is SUPERSEDE');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// Integration: applyMemoryActions with mixed actions
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
console.log('\n=== integration: mixed action lifecycle ===');
|
|
118
|
+
{
|
|
119
|
+
openDatabase(':memory:');
|
|
120
|
+
|
|
121
|
+
// Phase 1: Create initial memories
|
|
122
|
+
applyMemoryActions([
|
|
123
|
+
{ action: 'CREATE', category: 'gotcha', content: 'npm run build needs tsc first', confidence: 0.7 },
|
|
124
|
+
{ action: 'CREATE', category: 'convention', content: 'all DB queries use named params', confidence: 0.8 },
|
|
125
|
+
{ action: 'CREATE', category: 'architecture', content: 'extensions loaded from two paths', confidence: 0.85 },
|
|
126
|
+
], 'plan-slice', 'M001/S01');
|
|
127
|
+
|
|
128
|
+
let active = getActiveMemoriesRanked(30);
|
|
129
|
+
assertEq(active.length, 3, 'phase 1: 3 active memories');
|
|
130
|
+
|
|
131
|
+
// Phase 2: Reinforce one, update another, create new
|
|
132
|
+
applyMemoryActions([
|
|
133
|
+
{ action: 'REINFORCE', id: 'MEM002' },
|
|
134
|
+
{ action: 'UPDATE', id: 'MEM001', content: 'npm run build requires tsc --noEmit first' },
|
|
135
|
+
{ action: 'CREATE', category: 'pattern', content: 'use INSERT OR IGNORE for idempotency', confidence: 0.75 },
|
|
136
|
+
], 'execute-task', 'M001/S01/T01');
|
|
137
|
+
|
|
138
|
+
active = getActiveMemoriesRanked(30);
|
|
139
|
+
assertEq(active.length, 4, 'phase 2: 4 active memories');
|
|
140
|
+
assertEq(
|
|
141
|
+
active.find(m => m.id === 'MEM001')?.content,
|
|
142
|
+
'npm run build requires tsc --noEmit first',
|
|
143
|
+
'MEM001 content should be updated',
|
|
144
|
+
);
|
|
145
|
+
assertEq(active.find(m => m.id === 'MEM002')?.hit_count, 1, 'MEM002 should be reinforced');
|
|
146
|
+
|
|
147
|
+
// Phase 3: Supersede MEM001 with MEM005
|
|
148
|
+
applyMemoryActions([
|
|
149
|
+
{ action: 'CREATE', category: 'gotcha', content: 'build script handles tsc automatically now', confidence: 0.9 },
|
|
150
|
+
{ action: 'SUPERSEDE', id: 'MEM001', superseded_by: 'MEM005' },
|
|
151
|
+
], 'execute-task', 'M001/S01/T02');
|
|
152
|
+
|
|
153
|
+
active = getActiveMemoriesRanked(30);
|
|
154
|
+
assertEq(active.length, 4, 'phase 3: 4 active (1 superseded, 1 created)');
|
|
155
|
+
assertTrue(!active.find(m => m.id === 'MEM001'), 'MEM001 should be superseded');
|
|
156
|
+
assertTrue(!!active.find(m => m.id === 'MEM005'), 'MEM005 should be active');
|
|
157
|
+
|
|
158
|
+
// Verify ranking: MEM003 (0.85) > MEM005 (0.9) but MEM002 has 1 hit
|
|
159
|
+
// MEM002: 0.8 * (1 + 1*0.1) = 0.88
|
|
160
|
+
// MEM003: 0.85 * 1.0 = 0.85
|
|
161
|
+
// MEM005: 0.9 * 1.0 = 0.9
|
|
162
|
+
// MEM004: 0.75 * 1.0 = 0.75
|
|
163
|
+
assertEq(active[0].id, 'MEM005', 'MEM005 should rank first (0.9)');
|
|
164
|
+
assertEq(active[1].id, 'MEM002', 'MEM002 should rank second (0.88)');
|
|
165
|
+
|
|
166
|
+
closeDatabase();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
+
// memory-extractor: _resetExtractionState
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
+
|
|
173
|
+
console.log('\n=== memory-extractor: reset extraction state ===');
|
|
174
|
+
{
|
|
175
|
+
// Just verify it doesn't throw
|
|
176
|
+
_resetExtractionState();
|
|
177
|
+
assertTrue(true, '_resetExtractionState should not throw');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
report();
|