@davidbalzan/groundwork 0.3.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidbalzan/groundwork",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Groundwork — an installable AI development workflow (skills + doc methodology) you bolt onto any repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "author": "David Balzan",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@davidbalzan/groundwork-seam": "0.1.1"
31
+ "@davidbalzan/groundwork-seam": "0.1.2"
32
32
  },
33
33
  "scripts": {
34
34
  "groundwork": "node src/cli.mjs",
package/src/cli.mjs CHANGED
@@ -55,7 +55,7 @@ function help() {
55
55
  ` --json machine-readable output (doctor / status / list)`,
56
56
  "",
57
57
  bold("Examples:"),
58
- dim(" npx github:davidbalzan/groundwork init"),
58
+ dim(" npm i -g @davidbalzan/groundwork && groundwork init"),
59
59
  dim(" groundwork init . --minimal"),
60
60
  dim(" groundwork add data-layer"),
61
61
  dim(" groundwork status"),
@@ -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
- const phaseDir = path.join(docs, "phases");
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
- const factsFile = path.join(docs, "FACTS.md");
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;