gsd-pi 2.47.0-dev.8cfe772 → 2.47.0-dev.f2e721d
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/dist/resources/extensions/gsd/forensics.js +292 -1
- package/dist/resources/extensions/gsd/guided-flow.js +7 -1
- package/dist/resources/extensions/gsd/prompts/forensics.md +37 -5
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- 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 +2 -2
- 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/api/forensics/route.js +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 +17 -17
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/forensics.ts +329 -2
- package/src/resources/extensions/gsd/guided-flow.ts +9 -1
- package/src/resources/extensions/gsd/prompts/forensics.md +37 -5
- package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/stale-milestone-id-reservation.test.ts +79 -0
- /package/dist/web/standalone/.next/static/{DyrX2zX_4v7KZDbUNxE2q → O3E7X3EJ2lEKs_0hIUzGd}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DyrX2zX_4v7KZDbUNxE2q → O3E7X3EJ2lEKs_0hIUzGd}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const gsdDir = join(__dirname, "..");
|
|
9
|
+
|
|
10
|
+
describe("forensics journal & activity log awareness", () => {
|
|
11
|
+
const forensicsSrc = readFileSync(join(gsdDir, "forensics.ts"), "utf-8");
|
|
12
|
+
const promptSrc = readFileSync(join(gsdDir, "prompts", "forensics.md"), "utf-8");
|
|
13
|
+
|
|
14
|
+
it("scanJournalForForensics reads journal files directly (no full queryJournal load)", () => {
|
|
15
|
+
// Must NOT use queryJournal which loads ALL entries into memory
|
|
16
|
+
assert.ok(
|
|
17
|
+
!forensicsSrc.includes('queryJournal('),
|
|
18
|
+
"forensics.ts must NOT call queryJournal() which loads all entries at once",
|
|
19
|
+
);
|
|
20
|
+
// Must have its own journal scanning with file-level limits
|
|
21
|
+
assert.ok(
|
|
22
|
+
forensicsSrc.includes("scanJournalForForensics"),
|
|
23
|
+
"forensics.ts must have scanJournalForForensics function",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("journal scanning limits files parsed to avoid memory bloat", () => {
|
|
28
|
+
assert.ok(
|
|
29
|
+
forensicsSrc.includes("MAX_JOURNAL_RECENT_FILES"),
|
|
30
|
+
"must have MAX_JOURNAL_RECENT_FILES constant to limit parsed files",
|
|
31
|
+
);
|
|
32
|
+
assert.ok(
|
|
33
|
+
forensicsSrc.includes("MAX_JOURNAL_RECENT_EVENTS"),
|
|
34
|
+
"must have MAX_JOURNAL_RECENT_EVENTS constant to limit events extracted",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("older journal files are line-counted without full JSON parse", () => {
|
|
39
|
+
assert.ok(
|
|
40
|
+
forensicsSrc.includes("olderEntryCount") || forensicsSrc.includes("olderFiles"),
|
|
41
|
+
"must handle older files separately from recent files",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("ForensicReport includes journalSummary field", () => {
|
|
46
|
+
assert.ok(
|
|
47
|
+
forensicsSrc.includes("journalSummary"),
|
|
48
|
+
"ForensicReport must include journalSummary field",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("ForensicReport includes activityLogMeta field", () => {
|
|
53
|
+
assert.ok(
|
|
54
|
+
forensicsSrc.includes("activityLogMeta"),
|
|
55
|
+
"ForensicReport must include activityLogMeta field",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("buildForensicReport calls scanJournalForForensics", () => {
|
|
60
|
+
assert.ok(
|
|
61
|
+
forensicsSrc.includes("scanJournalForForensics"),
|
|
62
|
+
"buildForensicReport must call scanJournalForForensics",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("buildForensicReport calls gatherActivityLogMeta", () => {
|
|
67
|
+
assert.ok(
|
|
68
|
+
forensicsSrc.includes("gatherActivityLogMeta"),
|
|
69
|
+
"buildForensicReport must call gatherActivityLogMeta",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("forensics detects journal-based anomalies", () => {
|
|
74
|
+
assert.ok(
|
|
75
|
+
forensicsSrc.includes("detectJournalAnomalies"),
|
|
76
|
+
"forensics.ts must have detectJournalAnomalies function",
|
|
77
|
+
);
|
|
78
|
+
// Check for specific journal anomaly types
|
|
79
|
+
assert.ok(forensicsSrc.includes('"journal-stuck"'), "must detect journal-stuck anomalies");
|
|
80
|
+
assert.ok(forensicsSrc.includes('"journal-guard-block"'), "must detect journal-guard-block anomalies");
|
|
81
|
+
assert.ok(forensicsSrc.includes('"journal-rapid-iterations"'), "must detect journal-rapid-iterations anomalies");
|
|
82
|
+
assert.ok(forensicsSrc.includes('"journal-worktree-failure"'), "must detect journal-worktree-failure anomalies");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("formatReportForPrompt includes journal summary section", () => {
|
|
86
|
+
assert.ok(
|
|
87
|
+
forensicsSrc.includes("Journal Summary"),
|
|
88
|
+
"prompt formatter must include a Journal Summary section",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("formatReportForPrompt includes activity log overview section", () => {
|
|
93
|
+
assert.ok(
|
|
94
|
+
forensicsSrc.includes("Activity Log Overview"),
|
|
95
|
+
"prompt formatter must include an Activity Log Overview section",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("activity log scanning uses tail-read with byte cap (not full file load)", () => {
|
|
100
|
+
// scanActivityLogs uses nativeParseJsonlTail + MAX_JSONL_BYTES for efficient reading
|
|
101
|
+
assert.ok(
|
|
102
|
+
forensicsSrc.includes("nativeParseJsonlTail"),
|
|
103
|
+
"activity log scanning must use nativeParseJsonlTail for tail-reading",
|
|
104
|
+
);
|
|
105
|
+
assert.ok(
|
|
106
|
+
forensicsSrc.includes("MAX_JSONL_BYTES"),
|
|
107
|
+
"activity log scanning must respect MAX_JSONL_BYTES cap",
|
|
108
|
+
);
|
|
109
|
+
// Only reads last 5 files
|
|
110
|
+
assert.ok(
|
|
111
|
+
forensicsSrc.includes("slice(-5)"),
|
|
112
|
+
"activity log scanning must limit to last 5 files",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("activity log entries are distilled through extractTrace, not sent raw", () => {
|
|
117
|
+
assert.ok(
|
|
118
|
+
forensicsSrc.includes("extractTrace("),
|
|
119
|
+
"activity log entries must be distilled through extractTrace before reporting",
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("prompt output is hard-capped at 30KB", () => {
|
|
124
|
+
assert.ok(
|
|
125
|
+
forensicsSrc.includes("MAX_BYTES") && forensicsSrc.includes("30 * 1024"),
|
|
126
|
+
"formatReportForPrompt must have a 30KB hard cap",
|
|
127
|
+
);
|
|
128
|
+
assert.ok(
|
|
129
|
+
forensicsSrc.includes("truncated at 30KB"),
|
|
130
|
+
"prompt must show truncation message when capped",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("forensics prompt documents journal format", () => {
|
|
135
|
+
assert.ok(
|
|
136
|
+
promptSrc.includes("### Journal Format"),
|
|
137
|
+
"forensics.md must document the journal format",
|
|
138
|
+
);
|
|
139
|
+
assert.ok(
|
|
140
|
+
promptSrc.includes("flowId"),
|
|
141
|
+
"forensics.md must reference flowId concept",
|
|
142
|
+
);
|
|
143
|
+
assert.ok(
|
|
144
|
+
promptSrc.includes("causedBy"),
|
|
145
|
+
"forensics.md must reference causedBy for causal chains",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("forensics prompt includes journal directory in runtime path reference", () => {
|
|
150
|
+
assert.ok(
|
|
151
|
+
promptSrc.includes("journal/"),
|
|
152
|
+
"forensics.md runtime path reference must include journal/",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("investigation protocol references journal data", () => {
|
|
157
|
+
assert.ok(
|
|
158
|
+
promptSrc.includes("journal timeline") || promptSrc.includes("journal events"),
|
|
159
|
+
"investigation protocol must reference journal data for tracing",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #2488: Stale milestone ID reservations inflate next ID
|
|
3
|
+
* after cancelled /gsd sessions.
|
|
4
|
+
*
|
|
5
|
+
* The module-level `reservedMilestoneIds` Set persists across /gsd invocations
|
|
6
|
+
* within the same Node process. Without clearReservedMilestoneIds() at session
|
|
7
|
+
* start, each cancelled session permanently bumps the counter by 1.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, test, beforeEach } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
nextMilestoneId,
|
|
14
|
+
reserveMilestoneId,
|
|
15
|
+
getReservedMilestoneIds,
|
|
16
|
+
clearReservedMilestoneIds,
|
|
17
|
+
} from "../milestone-ids.ts";
|
|
18
|
+
|
|
19
|
+
describe("stale milestone ID reservation cleanup (#2488)", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
clearReservedMilestoneIds();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("without cleanup, cancelled sessions inflate the next ID", () => {
|
|
25
|
+
const diskIds = ["M001", "M002", "M003"];
|
|
26
|
+
|
|
27
|
+
// Session 1: user starts /gsd, ID is previewed and reserved, then cancelled
|
|
28
|
+
const allIds1 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
29
|
+
const preview1 = nextMilestoneId(allIds1);
|
|
30
|
+
reserveMilestoneId(preview1);
|
|
31
|
+
assert.equal(preview1, "M004");
|
|
32
|
+
|
|
33
|
+
// Session 2: user starts /gsd again — stale reservation still in Set
|
|
34
|
+
// WITHOUT clearing, the next ID skips M004 (reserved) and goes to M005
|
|
35
|
+
const allIds2 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
36
|
+
const preview2 = nextMilestoneId(allIds2);
|
|
37
|
+
assert.equal(preview2, "M005", "without cleanup, ID inflates to M005");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("with cleanup at session start, next ID is correct", () => {
|
|
41
|
+
const diskIds = ["M001", "M002", "M003"];
|
|
42
|
+
|
|
43
|
+
// Session 1: user starts /gsd, ID is previewed and reserved, then cancelled
|
|
44
|
+
const allIds1 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
45
|
+
const preview1 = nextMilestoneId(allIds1);
|
|
46
|
+
reserveMilestoneId(preview1);
|
|
47
|
+
assert.equal(preview1, "M004");
|
|
48
|
+
|
|
49
|
+
// Session 2: clear stale reservations first (the fix)
|
|
50
|
+
clearReservedMilestoneIds();
|
|
51
|
+
|
|
52
|
+
// Now the next ID correctly returns M004 again
|
|
53
|
+
const allIds2 = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
54
|
+
const preview2 = nextMilestoneId(allIds2);
|
|
55
|
+
assert.equal(preview2, "M004", "after cleanup, ID is correctly M004");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("multiple cancelled sessions compound the inflation without cleanup", () => {
|
|
59
|
+
const diskIds = ["M001", "M002", "M003"];
|
|
60
|
+
|
|
61
|
+
// 3 cancelled sessions without cleanup
|
|
62
|
+
for (let i = 0; i < 3; i++) {
|
|
63
|
+
const allIds = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
64
|
+
const preview = nextMilestoneId(allIds);
|
|
65
|
+
reserveMilestoneId(preview);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Without cleanup, we're now at M007 instead of M004
|
|
69
|
+
const allIds = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
70
|
+
const next = nextMilestoneId(allIds);
|
|
71
|
+
assert.equal(next, "M007", "3 cancelled sessions inflate ID by 3");
|
|
72
|
+
|
|
73
|
+
// With cleanup, we're back to M004
|
|
74
|
+
clearReservedMilestoneIds();
|
|
75
|
+
const allIdsClean = [...new Set([...diskIds, ...getReservedMilestoneIds()])];
|
|
76
|
+
const nextClean = nextMilestoneId(allIdsClean);
|
|
77
|
+
assert.equal(nextClean, "M004", "cleanup restores correct next ID");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
File without changes
|
|
File without changes
|