@opengsd/gsd-pi 1.7.0-dev.30c4ec2f → 1.7.0-dev.e0039c3c
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/doctor.js +653 -14
- package/dist/resources/extensions/gsd/md-importer.js +2 -7
- package/dist/resources/extensions/gsd/migrate/planning-writer.js +2 -8
- package/dist/resources/extensions/gsd/migration-auto-check.js +0 -4
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +14 -54
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +5 -48
- 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 +8 -8
- 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 +8 -8
- 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 +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/package.json +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/doctor.ts +673 -14
- package/src/resources/extensions/gsd/md-importer.ts +2 -7
- package/src/resources/extensions/gsd/migrate/planning-writer.ts +3 -9
- package/src/resources/extensions/gsd/migration-auto-check.ts +0 -3
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +20 -67
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +5 -47
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +1 -76
- package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +0 -53
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +0 -53
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -4
- package/src/resources/extensions/gsd/tests/planning-writer.test.ts +1 -18
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +3 -18
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +25 -177
- package/dist/resources/extensions/gsd/doctor-history.js +0 -65
- package/dist/resources/extensions/gsd/doctor-state-checks.js +0 -509
- package/dist/resources/extensions/gsd/doctor-workspace-checks.js +0 -91
- package/src/resources/extensions/gsd/doctor-history.ts +0 -81
- package/src/resources/extensions/gsd/doctor-state-checks.ts +0 -527
- package/src/resources/extensions/gsd/doctor-workspace-checks.ts +0 -97
- package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +0 -25
- /package/dist/web/standalone/.next/static/{ue2JDvA1wboxPBPeblD5N → QoErQJVEEGAIpfz1TMIHj}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{ue2JDvA1wboxPBPeblD5N → QoErQJVEEGAIpfz1TMIHj}/_ssgManifest.js +0 -0
|
@@ -107,11 +107,11 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
|
|
|
107
107
|
sketchScope: "limited",
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
// Simulate the post-crash scenario:
|
|
111
|
-
//
|
|
110
|
+
// Simulate the post-crash scenario: PLAN.md exists on disk but the
|
|
111
|
+
// is_sketch flag is still 1.
|
|
112
112
|
writeFileSync(
|
|
113
113
|
join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
|
|
114
|
-
|
|
114
|
+
"# S02 Plan\n",
|
|
115
115
|
);
|
|
116
116
|
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "pre: flagged as sketch");
|
|
117
117
|
|
|
@@ -131,120 +131,6 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
test("#1287: stub/placeholder PLAN does NOT clear the sketch flag", async (t) => {
|
|
135
|
-
const base = makeFixtureBase();
|
|
136
|
-
t.after(() => cleanup(base));
|
|
137
|
-
|
|
138
|
-
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
139
|
-
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
140
|
-
insertSlice({
|
|
141
|
-
id: "S02",
|
|
142
|
-
milestoneId: "M001",
|
|
143
|
-
title: "Feature",
|
|
144
|
-
status: "pending",
|
|
145
|
-
risk: "medium",
|
|
146
|
-
depends: [],
|
|
147
|
-
demo: "S02 demo.",
|
|
148
|
-
sequence: 1,
|
|
149
|
-
isSketch: true,
|
|
150
|
-
sketchScope: "limited",
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
|
|
154
|
-
|
|
155
|
-
// A bare stub PLAN (no decomposed tasks) must not clear the flag.
|
|
156
|
-
writeFileSync(planPath, "# S02 Plan\n");
|
|
157
|
-
clearRendererCaches();
|
|
158
|
-
let state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
159
|
-
let result = await reconcileBeforeDispatch(base, {
|
|
160
|
-
invalidateStateCache: () => {},
|
|
161
|
-
deriveState: async () => state,
|
|
162
|
-
});
|
|
163
|
-
assert.equal(result.ok, true);
|
|
164
|
-
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "stub PLAN: flag stays set");
|
|
165
|
-
assert.equal(result.repaired.length, 0, "stub PLAN: no repair");
|
|
166
|
-
|
|
167
|
-
// A projection round-trip stub whose only task is a synthetic "Plan NN"
|
|
168
|
-
// placeholder (migrate/transformer.buildTaskTitle) must also not clear it.
|
|
169
|
-
writeFileSync(
|
|
170
|
-
planPath,
|
|
171
|
-
makeStalePlanContent("S02", [{ id: "T01", title: "Plan 01", done: false }]),
|
|
172
|
-
);
|
|
173
|
-
clearRendererCaches();
|
|
174
|
-
state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
175
|
-
result = await reconcileBeforeDispatch(base, {
|
|
176
|
-
invalidateStateCache: () => {},
|
|
177
|
-
deriveState: async () => state,
|
|
178
|
-
});
|
|
179
|
-
assert.equal(result.ok, true);
|
|
180
|
-
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "placeholder task: flag stays set");
|
|
181
|
-
assert.equal(result.repaired.length, 0, "placeholder task: no repair");
|
|
182
|
-
|
|
183
|
-
// buildTaskTitle also emits `${phase} ${plan}` (e.g. "00 01") when the plan
|
|
184
|
-
// frontmatter carries phase/plan. This projected placeholder must not clear
|
|
185
|
-
// the flag either.
|
|
186
|
-
writeFileSync(
|
|
187
|
-
planPath,
|
|
188
|
-
makeStalePlanContent("S02", [{ id: "T01", title: "00 01", done: false }]),
|
|
189
|
-
);
|
|
190
|
-
clearRendererCaches();
|
|
191
|
-
state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
192
|
-
result = await reconcileBeforeDispatch(base, {
|
|
193
|
-
invalidateStateCache: () => {},
|
|
194
|
-
deriveState: async () => state,
|
|
195
|
-
});
|
|
196
|
-
assert.equal(result.ok, true);
|
|
197
|
-
assert.equal(
|
|
198
|
-
getSlice("M001", "S02")?.is_sketch,
|
|
199
|
-
1,
|
|
200
|
-
"phase/plan placeholder task: flag stays set",
|
|
201
|
-
);
|
|
202
|
-
assert.equal(result.repaired.length, 0, "phase/plan placeholder task: no repair");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("#1288: real tasks shaped like `word + number` still clear the sketch flag", async (t) => {
|
|
206
|
-
const base = makeFixtureBase();
|
|
207
|
-
t.after(() => cleanup(base));
|
|
208
|
-
|
|
209
|
-
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
210
|
-
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
211
|
-
insertSlice({
|
|
212
|
-
id: "S02",
|
|
213
|
-
milestoneId: "M001",
|
|
214
|
-
title: "Feature",
|
|
215
|
-
status: "pending",
|
|
216
|
-
risk: "medium",
|
|
217
|
-
depends: [],
|
|
218
|
-
demo: "S02 demo.",
|
|
219
|
-
sequence: 1,
|
|
220
|
-
isSketch: true,
|
|
221
|
-
sketchScope: "limited",
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
|
|
225
|
-
|
|
226
|
-
// `Step 1` / `RFC 1234` match the loose `word + number` shape but are genuine
|
|
227
|
-
// decomposed tasks. buildTaskTitle only emits `${phase} ${plan}` with a
|
|
228
|
-
// digit-led phase, so these must NOT be read as placeholders (#1288): a real
|
|
229
|
-
// plan-slice like this must still clear the stale is_sketch flag.
|
|
230
|
-
writeFileSync(
|
|
231
|
-
planPath,
|
|
232
|
-
makeStalePlanContent("S02", [
|
|
233
|
-
{ id: "T01", title: "Step 1", done: false },
|
|
234
|
-
{ id: "T02", title: "RFC 1234", done: false },
|
|
235
|
-
]),
|
|
236
|
-
);
|
|
237
|
-
clearRendererCaches();
|
|
238
|
-
const state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
239
|
-
const result = await reconcileBeforeDispatch(base, {
|
|
240
|
-
invalidateStateCache: () => {},
|
|
241
|
-
deriveState: async () => state,
|
|
242
|
-
});
|
|
243
|
-
assert.equal(result.ok, true);
|
|
244
|
-
assert.equal(getSlice("M001", "S02")?.is_sketch, 0, "real task titles: flag cleared");
|
|
245
|
-
assert.equal(result.repaired.length, 1, "real task titles: repaired once");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
134
|
test("ADR-017 (#5700): repair failure throws ReconciliationFailedError with shape", async () => {
|
|
249
135
|
const seenDrift: DriftRecord = { kind: "stale-sketch-flag", mid: "M001", sid: "S02" };
|
|
250
136
|
const handler: DriftHandler = {
|
|
@@ -1146,7 +1032,7 @@ test("ADR-017 (#5703): live worker lock is not cleared", async (t) => {
|
|
|
1146
1032
|
|
|
1147
1033
|
// ─── #5704: unregistered-milestone drift ────────────────────────────────────
|
|
1148
1034
|
|
|
1149
|
-
test("ADR-017 (#5704
|
|
1035
|
+
test("ADR-017 (#5704): unregistered-milestone drift fails closed without importing markdown", async (t) => {
|
|
1150
1036
|
const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-"));
|
|
1151
1037
|
const milestoneDir = join(base, ".gsd", "phases", "42-test");
|
|
1152
1038
|
mkdirSync(milestoneDir, { recursive: true });
|
|
@@ -1173,65 +1059,27 @@ test("ADR-017 (#5704/#1281): unregistered-milestone drift pauses with a hint ins
|
|
|
1173
1059
|
// Pre-condition: filesystem has the milestone, DB does NOT.
|
|
1174
1060
|
assert.equal(getMilestone("M042"), null, "pre: DB has no row for M042");
|
|
1175
1061
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
test("#1281: descriptive flat-phase dir registered under a suffixed id → no false-positive drift", async (t) => {
|
|
1197
|
-
const base = mkdtempSync(join(tmpdir(), "gsd-1281-suffix-"));
|
|
1198
|
-
// Descriptive slug the flat-phase extractor cannot recover the suffix from →
|
|
1199
|
-
// it derives the bare `M007`, which has no DB row.
|
|
1200
|
-
const milestoneDir = join(base, ".gsd", "phases", "07-v40fmq-m007-v40fmq-navigation-footer-system");
|
|
1201
|
-
mkdirSync(milestoneDir, { recursive: true });
|
|
1202
|
-
writeFileSync(
|
|
1203
|
-
join(milestoneDir, "M007-v40fmq-ROADMAP.md"),
|
|
1204
|
-
[
|
|
1205
|
-
"# M007: Navigation Footer System",
|
|
1206
|
-
"",
|
|
1207
|
-
"**Vision:** Verify no false-positive unregistered-milestone drift",
|
|
1208
|
-
"",
|
|
1209
|
-
"## Slices",
|
|
1210
|
-
"",
|
|
1211
|
-
"- [ ] **S01: Foundation** `risk:medium` `depends:[]`",
|
|
1212
|
-
"",
|
|
1213
|
-
].join("\n"),
|
|
1214
|
-
);
|
|
1215
|
-
t.after(() => {
|
|
1216
|
-
try { closeDatabase(); } catch { /* noop */ }
|
|
1217
|
-
rmSync(base, { recursive: true, force: true });
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1221
|
-
// The milestone IS registered — under the unique_milestone_ids suffixed id.
|
|
1222
|
-
insertMilestone({ id: "M007-v40fmq", title: "Navigation Footer System", status: "complete" });
|
|
1223
|
-
|
|
1224
|
-
const result = await reconcileBeforeDispatch(base, {
|
|
1225
|
-
invalidateStateCache: () => {},
|
|
1226
|
-
deriveState: async () => makeState(),
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
assert.equal(result.ok, true);
|
|
1230
|
-
assert.equal(
|
|
1231
|
-
result.blockers.some((b) => /unregistered/i.test(b) || /M007/.test(b)),
|
|
1232
|
-
false,
|
|
1233
|
-
"bare M007 derived from the slug must resolve to the registered M007-v40fmq row",
|
|
1062
|
+
await assert.rejects(
|
|
1063
|
+
reconcileBeforeDispatch(base, {
|
|
1064
|
+
invalidateStateCache: () => {},
|
|
1065
|
+
deriveState: async () => makeState(),
|
|
1066
|
+
}),
|
|
1067
|
+
(err: unknown) => {
|
|
1068
|
+
assert.ok(err instanceof ReconciliationFailedError);
|
|
1069
|
+
assert.match(String(err.message), /unregistered-milestone/);
|
|
1070
|
+
assert.equal(err.failures[0]?.drift.kind, "unregistered-milestone");
|
|
1071
|
+
assert.match(String(err.failures[0]?.cause), /M042/);
|
|
1072
|
+
assert.match(String(err.failures[0]?.cause), /markdown projection/);
|
|
1073
|
+
// Hint leads with targeted, non-destructive actions (rename/discard)...
|
|
1074
|
+
assert.match(String(err.failures[0]?.cause), /Rename/);
|
|
1075
|
+
assert.match(String(err.failures[0]?.cause), /Discard/);
|
|
1076
|
+
// ...and reframes recover as a destructive last resort, not the fix (#826).
|
|
1077
|
+
assert.match(String(err.failures[0]?.cause), /\/gsd recover/);
|
|
1078
|
+
assert.match(String(err.failures[0]?.cause), /last resort/i);
|
|
1079
|
+
return true;
|
|
1080
|
+
},
|
|
1234
1081
|
);
|
|
1082
|
+
assert.equal(getMilestone("M042"), null, "post: DB still has no row for M042");
|
|
1235
1083
|
});
|
|
1236
1084
|
|
|
1237
1085
|
test("ADR-017 (#5704): registered milestone (DB row present) → no drift", async (t) => {
|
|
@@ -2129,7 +1977,7 @@ test("deriveState is pure: stale sketch healed only via reconcileBeforeDispatch"
|
|
|
2129
1977
|
});
|
|
2130
1978
|
writeFileSync(
|
|
2131
1979
|
join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
|
|
2132
|
-
|
|
1980
|
+
"# S02 Plan\n",
|
|
2133
1981
|
);
|
|
2134
1982
|
|
|
2135
1983
|
const { deriveState } = await import("../state.ts");
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { saveFile } from "./files.js";
|
|
4
|
-
import { gsdRoot } from "./paths.js";
|
|
5
|
-
function buildDoctorHistorySummary(report, errorCount, warningCount, issueDetails) {
|
|
6
|
-
const summaryParts = [];
|
|
7
|
-
if (report.ok) {
|
|
8
|
-
summaryParts.push("Clean");
|
|
9
|
-
}
|
|
10
|
-
else {
|
|
11
|
-
const counts = [];
|
|
12
|
-
if (errorCount > 0)
|
|
13
|
-
counts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
14
|
-
if (warningCount > 0)
|
|
15
|
-
counts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
16
|
-
summaryParts.push(counts.join(", "));
|
|
17
|
-
}
|
|
18
|
-
if (report.fixesApplied.length > 0) {
|
|
19
|
-
summaryParts.push(`${report.fixesApplied.length} fixed`);
|
|
20
|
-
}
|
|
21
|
-
if (issueDetails && issueDetails.length > 0) {
|
|
22
|
-
const topIssue = issueDetails.find(i => i.severity === "error") ?? issueDetails[0];
|
|
23
|
-
summaryParts.push(topIssue.message);
|
|
24
|
-
}
|
|
25
|
-
return summaryParts.join(" · ");
|
|
26
|
-
}
|
|
27
|
-
export async function appendDoctorHistory(basePath, report) {
|
|
28
|
-
try {
|
|
29
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
30
|
-
const errorCount = report.issues.filter(i => i.severity === "error").length;
|
|
31
|
-
const warningCount = report.issues.filter(i => i.severity === "warning").length;
|
|
32
|
-
const issueDetails = report.issues
|
|
33
|
-
.filter(i => i.severity === "error" || i.severity === "warning")
|
|
34
|
-
.slice(0, 10) // cap to keep JSONL lines bounded
|
|
35
|
-
.map(i => ({ severity: i.severity, code: i.code, message: i.message, unitId: i.unitId }));
|
|
36
|
-
const entry = JSON.stringify({
|
|
37
|
-
ts: new Date().toISOString(),
|
|
38
|
-
ok: report.ok,
|
|
39
|
-
errors: errorCount,
|
|
40
|
-
warnings: warningCount,
|
|
41
|
-
fixes: report.fixesApplied.length,
|
|
42
|
-
codes: [...new Set(report.issues.map(i => i.code))],
|
|
43
|
-
issues: issueDetails.length > 0 ? issueDetails : undefined,
|
|
44
|
-
fixDescriptions: report.fixesApplied.length > 0 ? report.fixesApplied : undefined,
|
|
45
|
-
scope: report.scope,
|
|
46
|
-
summary: buildDoctorHistorySummary(report, errorCount, warningCount, issueDetails),
|
|
47
|
-
});
|
|
48
|
-
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
49
|
-
await saveFile(historyPath, existing + entry + "\n");
|
|
50
|
-
}
|
|
51
|
-
catch { /* non-fatal */ }
|
|
52
|
-
}
|
|
53
|
-
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
54
|
-
export async function readDoctorHistory(basePath, lastN = 50) {
|
|
55
|
-
try {
|
|
56
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
57
|
-
if (!existsSync(historyPath))
|
|
58
|
-
return [];
|
|
59
|
-
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
60
|
-
return lines.slice(-lastN).reverse().map(l => JSON.parse(l));
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
}
|