@davidbalzan/groundwork 0.4.6 → 0.4.8
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 +2 -2
- package/src/commands/doctor.mjs +99 -9
- package/src/lib/artifacts.mjs +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidbalzan/groundwork",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Groundwork — an installable AI development workflow (skills + doc methodology) you bolt onto any repo.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"author": "David Balzan",
|
|
30
30
|
"license": "UNLICENSED",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@davidbalzan/groundwork-seam": "0.1.
|
|
32
|
+
"@davidbalzan/groundwork-seam": "^0.1.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"groundwork": "node src/cli.mjs",
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { countCheckboxes, progressBar } from "../lib/progress.mjs";
|
|
|
7
7
|
import { ARTIFACTS, workDocPaths } from "../lib/artifacts.mjs";
|
|
8
8
|
import { log, bold, green, yellow, dim, cyan } from "../lib/log.mjs";
|
|
9
9
|
import { adrTripwire } from "../lib/adr-tripwire.mjs";
|
|
10
|
-
import { phaseCitationsIn, parseFactsDoc, parseWorkDoc, workDocIssues, workDocLegacyWriteIssues, workDocIssuesDetailed, queueItemsOf, doneEntriesOf, workstreamsV1RowsOf, sweepTagOf, sweepTagCensus, refsIn, refIn } from "@davidbalzan/groundwork-seam";
|
|
10
|
+
import { phaseCitationsIn, parseFactsDoc, parseWorkDoc, workDocIssues, workDocLegacyWriteIssues, workDocIssuesDetailed, queueItemsOf, doneEntriesOf, workstreamsV1RowsOf, sweepTagOf, sweepTagCensus, refsIn, refIn, withoutCodeContext } from "@davidbalzan/groundwork-seam";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* `groundwork doctor` — flag drift between the docs and reality. Offline + deterministic.
|
|
@@ -299,13 +299,29 @@ guard("queue-done-loop", () => {
|
|
|
299
299
|
// Fourth matcher in one day to re-derive ref/tag matching and get it wrong. The
|
|
300
300
|
// primitive lives in the seam now so the fifth one written cannot.
|
|
301
301
|
const doneRefs = doneEntries.flatMap((e) => refsIn(String(e.ref ?? "")));
|
|
302
|
+
// THE REPO BEING CHECKED, so a BARE `#170` resolves instead of being unknown.
|
|
303
|
+
//
|
|
304
|
+
// The seam's primitive refuses a bare-vs-qualified tie by design, and doctor
|
|
305
|
+
// deliberately passed nothing on the premise that "the corpus writes
|
|
306
|
+
// `kit#94`/`console#38`, so the tie is recoverable". MEASURED ON THIS REPO:
|
|
307
|
+
// it does not. Queue items cite bare `#170, #173` while DONE.md qualifies
|
|
308
|
+
// them, and six correctly-logged closures were reported as untieable — a
|
|
309
|
+
// warning ON CORRECT CONTENT, which is the whole subject of this task and
|
|
310
|
+
// reached David twice as a suspected data problem.
|
|
311
|
+
//
|
|
312
|
+
// This NARROWS the check, so it ships with the case where it still fires:
|
|
313
|
+
// `console#39` still does not match `groundwork-kit#39`, because the primitive
|
|
314
|
+
// compares repository leaves rather than numbers. Unknown becomes known only
|
|
315
|
+
// where the repo actually answers it; a doc outside a git repo passes null and
|
|
316
|
+
// gets exactly today's behaviour.
|
|
317
|
+
const contextRepo = originRepoOf(docs);
|
|
302
318
|
const problems = [];
|
|
303
319
|
for (const item of closed) {
|
|
304
320
|
const cited = refsIn(String(item.text));
|
|
305
321
|
const short = String(item.text).replace(/\s+/g, " ").slice(0, 70);
|
|
306
322
|
if (cited.length === 0) {
|
|
307
323
|
problems.push(`${item.where}: [x] item cites no PR, so nothing can tie it to DONE.md — "${short}…"`);
|
|
308
|
-
} else if (!cited.some((r) => refIn(r, doneRefs))) {
|
|
324
|
+
} else if (!cited.some((r) => refIn(r, doneRefs, { contextRepo }))) {
|
|
309
325
|
// NOT "this closure is unlogged" — the check cannot know that. The citation
|
|
310
326
|
// is the only tie it has, and an item whose body never names the PR that
|
|
311
327
|
// closed it is UNTIEABLE, even when DONE.md records the closure perfectly.
|
|
@@ -526,7 +542,12 @@ guard("undeclared-work-docs", () => {
|
|
|
526
542
|
const rogue = [];
|
|
527
543
|
for (const rel of mdFiles) {
|
|
528
544
|
if (declared.has(rel) || isTemplate(rel)) continue;
|
|
529
|
-
|
|
545
|
+
// Same root cause as the wikilink resolver, and the same fix: an example
|
|
546
|
+
// item line inside a fence is a SPECIMEN, not work. A doc explaining
|
|
547
|
+
// `- [ ] (P1) …` must not be reported as an undeclared queue — declaring it
|
|
548
|
+
// to silence the warning would make every queue check read specimens as
|
|
549
|
+
// real items, which is the defect this check exists to find.
|
|
550
|
+
const text = withoutCodeContext(readText(path.join(docs, rel)));
|
|
530
551
|
const count = (text.match(/^- \[[ x]\] \((?:P1|P2|P3)\) /gm) ?? []).length;
|
|
531
552
|
if (count > 0 && ITEM.test(text)) rogue.push({ rel, count });
|
|
532
553
|
}
|
|
@@ -536,12 +557,38 @@ guard("undeclared-work-docs", () => {
|
|
|
536
557
|
// much it looked at.
|
|
537
558
|
const scanned = `${mdFiles.filter((r) => !isTemplate(r)).length} doc(s) scanned (templates excluded), ${declared.size} declared queue.v1`;
|
|
538
559
|
if (rogue.length) {
|
|
560
|
+
// AN ARCHIVE MUST NOT BE TOLD TO DECLARE `queue.v1`. Declaring it is what
|
|
561
|
+
// the remedy says, and it would make every archived row parse as OPEN work —
|
|
562
|
+
// the check would resurrect the rows someone archived on purpose. A consumer
|
|
563
|
+
// fleet already ships `QUEUE_PARKED_ARCHIVE` (58 rows) and
|
|
564
|
+
// `QUEUE_ITEMS_ARCHIVE` (48); following this advice there would revive 106
|
|
565
|
+
// closed items. The archive GRAMMAR itself is a separate item; what belongs
|
|
566
|
+
// here is not handing out a remedy that breaks the thing it is aimed at.
|
|
567
|
+
const isArchive = (rel) => /archive/i.test(rel);
|
|
568
|
+
const archives = rogue.filter((r) => isArchive(r.rel));
|
|
569
|
+
const live = rogue.filter((r) => !isArchive(r.rel));
|
|
570
|
+
// A REMEDY MUST NAME A PATH THAT EXISTS IN THE REPO BEING CHECKED.
|
|
571
|
+
// `src/lib/artifacts.mjs` is correct only inside the kit, and it survived
|
|
572
|
+
// review because its authors live in the one repo where it works. A consumer
|
|
573
|
+
// reading it goes looking for a file it does not have, which is worse than
|
|
574
|
+
// no remedy: it reads as "your repo is broken" rather than "this advice is
|
|
575
|
+
// not for you".
|
|
576
|
+
const manifestPath = path.join(root, "src/lib/artifacts.mjs");
|
|
577
|
+
const where = exists(manifestPath)
|
|
578
|
+
? "`src/lib/artifacts.mjs`"
|
|
579
|
+
: "your artifact manifest (this repo has no `src/lib/artifacts.mjs` — that path exists only in the groundwork kit itself)";
|
|
539
580
|
push("undeclared-work-docs", "warn", [
|
|
540
581
|
scanned,
|
|
541
|
-
...
|
|
582
|
+
...live.map(
|
|
542
583
|
(r) => `docs/${r.rel} holds ${r.count} queue item(s) and is NOT declared queue.v1 in the manifest — no queue check reads it`,
|
|
543
584
|
),
|
|
544
|
-
|
|
585
|
+
...archives.map(
|
|
586
|
+
(r) =>
|
|
587
|
+
`docs/${r.rel} looks like an ARCHIVE holding ${r.count} archived item(s) — it is NOT declared, and it must NOT be declared queue.v1: that would make every archived row parse as open work`,
|
|
588
|
+
),
|
|
589
|
+
...(live.length
|
|
590
|
+
? [`declare it in ${where} with \`grammar: "queue.v1"\` and re-run \`groundwork artifacts\` — a manifest-derived population cannot find what the manifest does not name`]
|
|
591
|
+
: []),
|
|
545
592
|
]);
|
|
546
593
|
} else {
|
|
547
594
|
push("undeclared-work-docs", "ok", [`${scanned}, no undeclared queue-shaped doc`]);
|
|
@@ -582,7 +629,10 @@ guard("doc-registration", () => {
|
|
|
582
629
|
if (unregistered.length) {
|
|
583
630
|
push("doc-registration", "warn", [
|
|
584
631
|
...unregistered.map((f) => `docs/${f} is not in the artifact manifest — no stated writer, reader, or purpose`),
|
|
585
|
-
|
|
632
|
+
// SAME REMEDY DEFECT AS undeclared-work-docs, and the same fix: name a
|
|
633
|
+
// path the CHECKED repo has. This line is the one a consumer sees most,
|
|
634
|
+
// because an unregistered doc is the most common finding in a fresh repo.
|
|
635
|
+
`add it to ${exists(path.join(root, "src/lib/artifacts.mjs")) ? "src/lib/artifacts.mjs" : "your artifact manifest (this repo has no src/lib/artifacts.mjs — that path exists only in the groundwork kit itself)"} and re-run \`groundwork artifacts\`, or delete it — an unregistered doc is ungoverned, not merely undocumented`,
|
|
586
636
|
]);
|
|
587
637
|
} else {
|
|
588
638
|
push("doc-registration", "ok", [`${onDisk.length} doc(s) under docs/, all registered in the manifest`]);
|
|
@@ -1123,8 +1173,21 @@ export function doctor(targetDir, opts = {}) {
|
|
|
1123
1173
|
|
|
1124
1174
|
// ---------- helpers ----------
|
|
1125
1175
|
function phaseNumberFromDir(label) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1176
|
+
// THE PHASE TOKEN CAN HAVE A MINOR PART, and truncating it is the b-clause
|
|
1177
|
+
// defect at a second granularity. There the ID token was too narrow — `3.3b`
|
|
1178
|
+
// matched as `3.3`. Here the PHASE token is: `phase5.1` matched as `5`, so
|
|
1179
|
+
// every box in phase 5.1 was keyed under phase 5 and the citation that names
|
|
1180
|
+
// the phase correctly — `Phase 5.1 Task 7.1` — parsed as nothing at all.
|
|
1181
|
+
//
|
|
1182
|
+
// The consequence was worse than a miss: the citation that LOOKS right is the
|
|
1183
|
+
// one that does not tie, so a correct-looking record reports unevidenced
|
|
1184
|
+
// ticks. Every 5.1 citation had to be written in a form contradicting the
|
|
1185
|
+
// phase's own name.
|
|
1186
|
+
//
|
|
1187
|
+
// Returned as a STRING, because "5.1" is not a number and Number("5.1")
|
|
1188
|
+
// silently collides `phase5.1` with any future `phase5.10`.
|
|
1189
|
+
const m = String(label).match(/^phase(\d+(?:\.\d+)?)/i);
|
|
1190
|
+
return m ? m[1] : null;
|
|
1128
1191
|
}
|
|
1129
1192
|
|
|
1130
1193
|
function roadmapPhaseComplete(roadmapText, n) {
|
|
@@ -1138,9 +1201,36 @@ function doneLineNamesPhase(doneText, n) {
|
|
|
1138
1201
|
return String(doneText).split("\n").some((line) => re.test(line));
|
|
1139
1202
|
}
|
|
1140
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* `owner/repo` for the repository being checked, or null.
|
|
1206
|
+
*
|
|
1207
|
+
* Read from the git remote rather than from package.json: the remote is what
|
|
1208
|
+
* the refs in these documents actually name, and a package can be published
|
|
1209
|
+
* under a name that differs from its repository.
|
|
1210
|
+
*/
|
|
1211
|
+
function originRepoOf(dir) {
|
|
1212
|
+
try {
|
|
1213
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1214
|
+
cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"],
|
|
1215
|
+
}).trim();
|
|
1216
|
+
const m = /[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(url);
|
|
1217
|
+
return m ? m[1] : null;
|
|
1218
|
+
} catch {
|
|
1219
|
+
// No remote, or not a repo. Null is today's behaviour exactly — an unknown
|
|
1220
|
+
// context must not become a guessed one.
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1141
1225
|
function wikilinks(text) {
|
|
1142
1226
|
const out = [];
|
|
1143
|
-
|
|
1227
|
+
// A WIKILINK IN A CODE SPAN IS NOT A LINK. Documentation contains examples of
|
|
1228
|
+
// what it documents, and this check flagged 15/15 quoted specimens — one of
|
|
1229
|
+
// them a deliberately planted `[[orphan]]` control, which means the check
|
|
1230
|
+
// reported a failure ON THE EVIDENCE THAT IT WORKS. Stripping preserves line
|
|
1231
|
+
// structure, so every location this function's callers report still points at
|
|
1232
|
+
// the same place (§withoutCodeContext).
|
|
1233
|
+
for (const m of withoutCodeContext(text).matchAll(/\[\[([^\]]+)\]\]/g)) {
|
|
1144
1234
|
let t = m[1].split("|")[0].split("#")[0].trim(); // strip alias + heading
|
|
1145
1235
|
if (!t) continue; // intra-doc heading link
|
|
1146
1236
|
out.push(t);
|
package/src/lib/artifacts.mjs
CHANGED
|
@@ -54,6 +54,7 @@ export const ARTIFACTS = [
|
|
|
54
54
|
// it as claimable work. The whole point of the file is that its contents
|
|
55
55
|
// are the items the fleet CANNOT pick up.
|
|
56
56
|
{ path: "docs/DAVID_TASKS.md", scope: "project", purpose: "Standing list of what is blocked on David — his hands, accounts, money, or ruling", writtenBy: "aide (coordinator may append)", readBy: "David, /start-session", rules: "Not a queue: no `- [ ] (Pn)` lines. Items the fleet can do belong in QUEUE.md" },
|
|
57
|
+
{ path: "docs/improvements.md", scope: "project", purpose: "Improvements found by USING the kit, not by reviewing it — each entry records how it was hit", writtenBy: "aide (any agent may append an encounter)", readBy: "David (walkthrough), /plan-phase", rules: "An entry requires an ENCOUNTER: how it was hit, in the tool, by whom. A proposal with no encounter belongs in QUEUE.md. Entries keep their line after the fix lands and gain a reference, so the record of what using it felt like survives the repair. Includes a do-not-improve section: surfaces measured as working, which an in-flight phase must not regress." },
|
|
57
58
|
{ path: "docs/WORKSTREAMS.md", grammar: "workstreams.v1", scope: "project", purpose: "Live state of parallel streams", writtenBy: "/update-workstreams + coordinator", readBy: "everyone, /start-session", rules: "One row per active stream" },
|
|
58
59
|
{ path: "docs/QUEUE.md", grammar: "queue.v1", scope: "project", purpose: "Inbound queue (phases + ad-hoc)", writtenBy: "/plan-phase + human", readBy: "/start-session, coordinator", rules: "Single writer per file: human/proxy only; executors never edit it" },
|
|
59
60
|
{ path: "docs/DONE.md", grammar: "done.v1", scope: "project", purpose: "Completion log", writtenBy: "executor (solo you or coordinator)", readBy: "/start-session, humans", rules: "Append-only; sole executor write in the queue seam; pinned em-dash+middot line format" },
|