@davidbalzan/groundwork 0.3.3 → 0.3.4
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/package.json +1 -1
- package/src/commands/doctor.mjs +47 -3
package/package.json
CHANGED
package/src/commands/doctor.mjs
CHANGED
|
@@ -26,8 +26,26 @@ export function collectDoctor(targetDir) {
|
|
|
26
26
|
|
|
27
27
|
const checks = [];
|
|
28
28
|
const push = (check, level, items) => checks.push({ check, level, items });
|
|
29
|
+
// A broken check degrades to a named skip instead of taking the whole doctor
|
|
30
|
+
// down (field crash 2026-08-21: a TypeError from one check aborted the run
|
|
31
|
+
// and every later check with it, including the pure-fs ones). Doctor never
|
|
32
|
+
// writes anything — a skipped check must not tempt anyone to "fix" a target
|
|
33
|
+
// into existence.
|
|
34
|
+
const skipped = [];
|
|
35
|
+
const guard = (name, fn) => {
|
|
36
|
+
try {
|
|
37
|
+
fn();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
skipped.push(name);
|
|
40
|
+
push(name, "info", [
|
|
41
|
+
`check skipped — ${String(e?.message ?? e).split("\n")[0]} (doctor kept going; other checks are unaffected)`,
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
29
45
|
const mdFiles = [...walk(docs)].filter((f) => f.endsWith(".md"));
|
|
46
|
+
const phaseDir = path.join(docs, "phases");
|
|
30
47
|
|
|
48
|
+
guard("orphan-wikilinks", () => {
|
|
31
49
|
const valid = validLinkTargets(docs, mdFiles);
|
|
32
50
|
const orphanItems = [];
|
|
33
51
|
const seen = new Set();
|
|
@@ -42,8 +60,9 @@ export function collectDoctor(targetDir) {
|
|
|
42
60
|
}
|
|
43
61
|
}
|
|
44
62
|
push("orphan-wikilinks", orphanItems.length ? "warn" : "ok", orphanItems.length ? orphanItems : ["all wikilinks resolve"]);
|
|
63
|
+
});
|
|
45
64
|
|
|
46
|
-
|
|
65
|
+
guard("phase-progress", () => {
|
|
47
66
|
const roadmapText = exists(path.join(docs, "PRODUCTION_ROADMAP.md"))
|
|
48
67
|
? readText(path.join(docs, "PRODUCTION_ROADMAP.md"))
|
|
49
68
|
: "";
|
|
@@ -71,7 +90,10 @@ export function collectDoctor(targetDir) {
|
|
|
71
90
|
}
|
|
72
91
|
if (!phaseItems.length) push("phase-progress", "ok", ["no phase task files yet (run /plan-phase)"]);
|
|
73
92
|
else push("phase-progress", phaseWarn ? "warn" : "ok", phaseItems);
|
|
93
|
+
});
|
|
94
|
+
|
|
74
95
|
|
|
96
|
+
guard("spec-coverage", () => {
|
|
75
97
|
const coverageItems = [];
|
|
76
98
|
if (exists(phaseDir)) {
|
|
77
99
|
for (const dir of listDirs(phaseDir)) {
|
|
@@ -100,7 +122,10 @@ export function collectDoctor(targetDir) {
|
|
|
100
122
|
const coverageInfoOnly = coverageItems.length === 1 && coverageItems[0].startsWith("no PRD");
|
|
101
123
|
if (!coverageItems.length) push("spec-coverage", "ok", ["phase structure + PRD sections look complete"]);
|
|
102
124
|
else push("spec-coverage", coverageInfoOnly ? "info" : "warn", coverageItems);
|
|
125
|
+
});
|
|
126
|
+
|
|
103
127
|
|
|
128
|
+
guard("version-marker", () => {
|
|
104
129
|
const marker = path.join(docs, ".groundwork", "VERSION");
|
|
105
130
|
const cliV = pkgVersion();
|
|
106
131
|
if (exists(marker)) {
|
|
@@ -109,7 +134,10 @@ export function collectDoctor(targetDir) {
|
|
|
109
134
|
push("version-marker", "warn", [`installed v${got}, CLI is v${cliV} — run \`groundwork update --all --docs\``]);
|
|
110
135
|
else push("version-marker", "ok", [`up to date (v${got})`]);
|
|
111
136
|
} else push("version-marker", "warn", ["no VERSION marker — re-run `groundwork init`/`update`"]);
|
|
137
|
+
});
|
|
112
138
|
|
|
139
|
+
|
|
140
|
+
guard("stack-map-freshness", () => {
|
|
113
141
|
const sm = path.join(docs, "STACK_MAP.md");
|
|
114
142
|
if (exists(sm)) {
|
|
115
143
|
const m = readText(sm).match(/Last audited[^\d]*(\d{4})-(\d{2})-(\d{2})/);
|
|
@@ -120,7 +148,10 @@ export function collectDoctor(targetDir) {
|
|
|
120
148
|
else push("stack-map-freshness", "ok", [`STACK_MAP audited ${days} days ago`]);
|
|
121
149
|
}
|
|
122
150
|
} else push("stack-map-freshness", "info", ["no STACK_MAP.md yet (run /kickstart)"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
123
153
|
|
|
154
|
+
guard("workstreams", () => {
|
|
124
155
|
const wsFile = path.join(docs, "WORKSTREAMS.md");
|
|
125
156
|
if (exists(wsFile)) {
|
|
126
157
|
const wsDoc = parseWorkDoc(readText(wsFile));
|
|
@@ -128,15 +159,21 @@ export function collectDoctor(targetDir) {
|
|
|
128
159
|
if (wsIssues.length) push("workstreams", "warn", wsIssues);
|
|
129
160
|
else push("workstreams", "ok", ["write grammar is workstreams.v1"]);
|
|
130
161
|
} else push("workstreams", "info", ["no WORKSTREAMS.md yet (run `groundwork update --docs`)"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
131
164
|
|
|
165
|
+
guard("queue", () => {
|
|
132
166
|
const qFile = path.join(docs, "QUEUE.md");
|
|
133
167
|
if (exists(qFile)) {
|
|
134
168
|
const qIssues = workDocIssues(parseWorkDoc(readText(qFile)));
|
|
135
169
|
if (qIssues.length) push("queue", "warn", qIssues);
|
|
136
170
|
else push("queue", "ok", ["queue.v1 — one line per item"]);
|
|
137
171
|
}
|
|
172
|
+
});
|
|
138
173
|
|
|
139
|
-
|
|
174
|
+
|
|
175
|
+
const factsFile = path.join(docs, "FACTS.md");
|
|
176
|
+
guard("facts-freshness", () => {
|
|
140
177
|
if (exists(factsFile)) {
|
|
141
178
|
const { entries, issues } = parseFactsDoc(readText(factsFile));
|
|
142
179
|
if (issues.length) push("facts-freshness", "warn", issues);
|
|
@@ -147,7 +184,10 @@ export function collectDoctor(targetDir) {
|
|
|
147
184
|
[entries.length ? `${entries.length} fact(s), all verified and fresh` : "no facts recorded yet"],
|
|
148
185
|
);
|
|
149
186
|
} else push("facts-freshness", "info", ["no FACTS.md yet (run `groundwork update --docs`)"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
150
189
|
|
|
190
|
+
guard("adr-tripwire", () => {
|
|
151
191
|
const decisions = path.join(docs, "DECISIONS.md");
|
|
152
192
|
if (exists(decisions)) {
|
|
153
193
|
const acceptedIds = new Set();
|
|
@@ -169,9 +209,11 @@ export function collectDoctor(targetDir) {
|
|
|
169
209
|
if (adrItems.length) push("adr-tripwire", "warn", adrItems);
|
|
170
210
|
else push("adr-tripwire", "ok", ["no Accepted ADR contradicted by dependencies/paths"]);
|
|
171
211
|
} else push("adr-tripwire", "info", ["no DECISIONS.md yet (run /kickstart)"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
172
214
|
|
|
173
215
|
const warnCount = checks.filter((c) => c.level === "warn").length;
|
|
174
|
-
return { ok: warnCount === 0, exit: warnCount === 0 ? 0 : 1, checks };
|
|
216
|
+
return { ok: warnCount === 0, exit: warnCount === 0 ? 0 : 1, checks, ...(skipped.length ? { skipped } : {}) };
|
|
175
217
|
}
|
|
176
218
|
|
|
177
219
|
export function doctor(targetDir, opts = {}) {
|
|
@@ -207,6 +249,8 @@ export function doctor(targetDir, opts = {}) {
|
|
|
207
249
|
}
|
|
208
250
|
}
|
|
209
251
|
log.heading("Summary");
|
|
252
|
+
if (report.skipped?.length)
|
|
253
|
+
console.log(` ${yellow("⚠")} ${report.skipped.length} check(s) skipped (${report.skipped.join(", ")}) — see above; fs-only checks still ran.`);
|
|
210
254
|
if (report.ok) log.ok("No drift detected — docs match reality.");
|
|
211
255
|
else console.log(` ${yellow(String(report.checks.filter((c) => c.level === "warn").length))} issue(s) to look at.`);
|
|
212
256
|
process.exitCode = report.exit;
|