@davidbalzan/groundwork 0.4.4 → 0.4.6
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.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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.9"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"groundwork": "node src/cli.mjs",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Needs David
|
|
2
|
+
|
|
3
|
+
Work the fleet **cannot do for itself**: things requiring David's hands, his
|
|
4
|
+
accounts, his money, or his ruling.
|
|
5
|
+
|
|
6
|
+
> **This file is not a queue, and the distinction is the reason it exists.**
|
|
7
|
+
> Without it, David-blocked items sit in `QUEUE.md` at P1 looking exactly like
|
|
8
|
+
> work the fleet can pick up — which is how a licence item read as actionable
|
|
9
|
+
> for two days while nobody could have actioned it.
|
|
10
|
+
>
|
|
11
|
+
> So entries here carry **no `- [ ] (Pn)` checkbox lines**. That is deliberate
|
|
12
|
+
> and load-bearing: a David-blocked list that parses as a queue *is* a queue,
|
|
13
|
+
> and every queue check would start reading it as available work. If you find
|
|
14
|
+
> yourself wanting a priority marker here, the item probably belongs in
|
|
15
|
+
> `QUEUE.md` with a note that it is blocked.
|
|
16
|
+
|
|
17
|
+
## How this differs from its neighbours
|
|
18
|
+
|
|
19
|
+
- **`QUEUE.md`** — work the fleet can do. Prioritised, claimable, closeable.
|
|
20
|
+
- **`WORKSTREAMS.md` § Needs David** — decisions in flight *right now*, with a
|
|
21
|
+
recommendation attached. Cleared within a session.
|
|
22
|
+
- **this file** — the standing list of what is blocked ON DAVID and stays
|
|
23
|
+
blocked until he acts. It outlives any one session.
|
|
24
|
+
|
|
25
|
+
## Blocked on David
|
|
26
|
+
|
|
27
|
+
*One `###` block per item. Delete this heading's examples when the first real
|
|
28
|
+
one lands.*
|
|
29
|
+
|
|
30
|
+
### <short title>
|
|
31
|
+
|
|
32
|
+
- **Needs:** what only David can do (publish · pay · approve · decide · access)
|
|
33
|
+
- **Why it cannot be delegated:** the specific thing an agent lacks — a
|
|
34
|
+
credential, an account, an authority. "It is important" is not a reason.
|
|
35
|
+
- **Blocked since:** YYYY-MM-DD
|
|
36
|
+
- **Blocking:** what is waiting on it, or `nothing — it is just owed`
|
|
37
|
+
- **If David does nothing:** the actual consequence, written plainly. An item
|
|
38
|
+
with no consequence for inaction is a preference, not a blocker.
|
|
39
|
+
|
|
40
|
+
## Cleared
|
|
41
|
+
|
|
42
|
+
*Move items here with the date and what David decided. Kept because "we asked
|
|
43
|
+
and he said no" is a fact worth not re-litigating in three weeks.*
|
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 { parseFactsDoc, parseWorkDoc, workDocIssues, workDocLegacyWriteIssues, workDocIssuesDetailed, queueItemsOf, doneEntriesOf, workstreamsV1RowsOf, sweepTagOf, sweepTagCensus } from "@davidbalzan/groundwork-seam";
|
|
10
|
+
import { phaseCitationsIn, parseFactsDoc, parseWorkDoc, workDocIssues, workDocLegacyWriteIssues, workDocIssuesDetailed, queueItemsOf, doneEntriesOf, workstreamsV1RowsOf, sweepTagOf, sweepTagCensus, refsIn, refIn } from "@davidbalzan/groundwork-seam";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* `groundwork doctor` — flag drift between the docs and reality. Offline + deterministic.
|
|
@@ -288,23 +288,40 @@ guard("queue-done-loop", () => {
|
|
|
288
288
|
]);
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
// REFS ARE COMPARED WITH THE SHARED PRIMITIVE, not by number.
|
|
292
|
+
//
|
|
293
|
+
// This matched on the bare NUMBER, so an item whose body cited `console#39`
|
|
294
|
+
// tied to a `DONE.md` entry for `groundwork-kit#39` — two repositories, one
|
|
295
|
+
// number — and the check reported GREEN over the exact gap it exists to find.
|
|
296
|
+
// Measured live: `q-142d95da` was closed by #119 with no #119 entry in DONE.md,
|
|
297
|
+
// and this printed "1 closed item(s), each matched to a DONE.md entry".
|
|
298
|
+
//
|
|
299
|
+
// Fourth matcher in one day to re-derive ref/tag matching and get it wrong. The
|
|
300
|
+
// primitive lives in the seam now so the fifth one written cannot.
|
|
301
|
+
const doneRefs = doneEntries.flatMap((e) => refsIn(String(e.ref ?? "")));
|
|
294
302
|
const problems = [];
|
|
295
303
|
for (const item of closed) {
|
|
296
|
-
const cited =
|
|
304
|
+
const cited = refsIn(String(item.text));
|
|
297
305
|
const short = String(item.text).replace(/\s+/g, " ").slice(0, 70);
|
|
298
306
|
if (cited.length === 0) {
|
|
299
307
|
problems.push(`${item.where}: [x] item cites no PR, so nothing can tie it to DONE.md — "${short}…"`);
|
|
300
|
-
} else if (!cited.some((
|
|
301
|
-
|
|
308
|
+
} else if (!cited.some((r) => refIn(r, doneRefs))) {
|
|
309
|
+
// NOT "this closure is unlogged" — the check cannot know that. The citation
|
|
310
|
+
// is the only tie it has, and an item whose body never names the PR that
|
|
311
|
+
// closed it is UNTIEABLE, even when DONE.md records the closure perfectly.
|
|
312
|
+
// Measured live: an item citing `kit#84` and `console#39` was closed by
|
|
313
|
+
// #119, and #119 IS in DONE.md — the item simply never mentions it.
|
|
314
|
+
// Reporting that as "no entry" would send someone to add a duplicate.
|
|
315
|
+
problems.push(
|
|
316
|
+
`${item.where}: [x] item cites ${cited.map((r) => r.raw).join(", ")}, none of which DONE.md records — so this closure CANNOT BE TIED by citation. ` +
|
|
317
|
+
`It may still be logged under a ref the item does not name; the tie, not the log, is what is missing — "${short}…"`,
|
|
318
|
+
);
|
|
302
319
|
}
|
|
303
320
|
}
|
|
304
321
|
if (problems.length) {
|
|
305
322
|
push("queue-done-loop", "warn", [
|
|
306
323
|
...problems,
|
|
307
|
-
"
|
|
324
|
+
"either DONE.md is missing the entry, or the item does not cite the PR that closed it — those are different fixes, and the citation is the only thing that distinguishes them",
|
|
308
325
|
]);
|
|
309
326
|
} else {
|
|
310
327
|
push("queue-done-loop", "ok", [`${closed.length} closed item(s), each matched to a DONE.md entry`]);
|
|
@@ -595,7 +612,9 @@ guard("phase-checkbox", () => {
|
|
|
595
612
|
if (!exists(phaseDir)) return;
|
|
596
613
|
|
|
597
614
|
// `- [x] 1.1 <text>` inside docs/phases/phase<N>/PHASE*_TASKS.md.
|
|
598
|
-
|
|
615
|
+
// A TASK ID MAY CARRY A LETTER SUFFIX — `3.3b` is a disposition clause, and this
|
|
616
|
+
// convention adds them constantly.
|
|
617
|
+
const TASK_LINE = /^\s*-\s*\[( |x)\]\s*\*{0,2}(\d+\.\d+[a-z]?)\b/i;
|
|
599
618
|
const boxes = new Map(); // "4:1.1" -> { ticked, phase, id, file }
|
|
600
619
|
for (const rel of walk(phaseDir)) {
|
|
601
620
|
if (!/PHASE.*TASKS\.md$/i.test(rel)) continue;
|
|
@@ -613,14 +632,33 @@ guard("phase-checkbox", () => {
|
|
|
613
632
|
}
|
|
614
633
|
|
|
615
634
|
const claims = new Map(); // "4:1.1" -> where it was claimed
|
|
616
|
-
|
|
635
|
+
// B-CLAUSES BROKE THIS TWO WAYS, AND THE SECOND IS WORSE THAN THE TRUNCATION.
|
|
636
|
+
//
|
|
637
|
+
// The id pattern was `\d+\.\d+`, so `3.3b` matched as `3.3`:
|
|
638
|
+
// `Phase 5 Task 3.1, 3.2, 3.3b, 3.4, 3.5` captured 3.1, 3.2, 3.3 — the list
|
|
639
|
+
// SILENTLY TRUNCATED at the b-clause and 3.4/3.5 went from cited to
|
|
640
|
+
// unevidenced with no warning.
|
|
641
|
+
// `Phase 5 Task 3.3b` credited `3.3` — a MISATTRIBUTION, ticking a different
|
|
642
|
+
// box than the one cited. The right answer arrived for the wrong reason once
|
|
643
|
+
// today and would have credited a false claim just as readily.
|
|
644
|
+
//
|
|
645
|
+
// A GRAMMAR THAT DEGRADES IN PROPORTION TO BEING USED is worse than one that
|
|
646
|
+
// fails randomly, because adoption looks like decay — and every disposition
|
|
647
|
+
// clause this convention adds is a b-clause.
|
|
648
|
+
//
|
|
649
|
+
// GRANULARITY THIS FIX COVERS: the ID TOKEN — `3.3` and `3.3b` are now distinct
|
|
650
|
+
// ids, in both the citation and the checkbox.
|
|
651
|
+
// THE NEXT ONE DOWN, STATED RATHER THAN DISCOVERED: a deeper id (`3.3b.i`, or a
|
|
652
|
+
// two-letter `3.3ab`) still truncates the list at that point. Nothing in the
|
|
653
|
+
// corpus uses one today; if that changes, this is the line that needs widening,
|
|
654
|
+
// and the same silent-truncation failure returns until it is.
|
|
655
|
+
// ONE GRAMMAR, SHARED THROUGH SEAM. This was a local regex until the bus's
|
|
656
|
+
// `merge` verb needed the same answer at merge time; ADR-004 forbids a
|
|
657
|
+
// groundwork↔mcp edge, so a copy in each package was the obvious route — and
|
|
658
|
+
// a copied grammar is two grammars the moment one is fixed. This very token
|
|
659
|
+
// was widened for b-clauses today, which a second copy would have missed.
|
|
617
660
|
const harvest = (text, where) => {
|
|
618
|
-
for (const
|
|
619
|
-
for (const id of String(m[2]).match(/\d+\.\d+/g) ?? []) {
|
|
620
|
-
const k = `${Number(m[1])}:${id}`;
|
|
621
|
-
if (!claims.has(k)) claims.set(k, where);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
661
|
+
for (const k of phaseCitationsIn(text)) if (!claims.has(k)) claims.set(k, where);
|
|
624
662
|
};
|
|
625
663
|
const dFile = path.join(docs, "DONE.md");
|
|
626
664
|
if (exists(dFile)) harvest(readText(dFile), "DONE.md");
|
|
@@ -860,7 +898,48 @@ guard("version-truth", () => {
|
|
|
860
898
|
}
|
|
861
899
|
});
|
|
862
900
|
}
|
|
863
|
-
|
|
901
|
+
// THE PUBLISHED COLUMN IS A CLAIM TOO, and it is checked BY COLUMN.
|
|
902
|
+
//
|
|
903
|
+
// My first attempt at this compared every version in the ROW and passed as soon
|
|
904
|
+
// as one matched — which is the occurrence-not-position error at column
|
|
905
|
+
// granularity, in the check I wrote after landing that rule. It false-positived
|
|
906
|
+
// on a workspace-deps table and MISSED the live case: `STACK_MAP` stated
|
|
907
|
+
// playbook pinned 0.5.17 and published 0.5.16, and the row contained 0.5.17 so
|
|
908
|
+
// it passed. Second time in one day a bump left that table behind.
|
|
909
|
+
//
|
|
910
|
+
// So the header decides which cell is which. A table without a `Latest
|
|
911
|
+
// published` column makes no published claim and is not checked — absence of a
|
|
912
|
+
// claim is not a wrong claim.
|
|
913
|
+
for (const rel of mdFiles) {
|
|
914
|
+
const text = readText(path.join(docs, rel));
|
|
915
|
+
if (!/Last audited|verified:/i.test(text.split("\n").slice(0, 15).join("\n"))) continue;
|
|
916
|
+
const lines = text.split("\n");
|
|
917
|
+
let col = -1;
|
|
918
|
+
lines.forEach((line, i) => {
|
|
919
|
+
if (!line.startsWith("|")) {
|
|
920
|
+
col = -1;
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const cells = line.split("|").slice(1, -1).map((c) => c.trim());
|
|
924
|
+
if (/latest published/i.test(line)) {
|
|
925
|
+
col = cells.findIndex((c) => /latest published/i.test(c));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (col < 0 || col >= cells.length) return;
|
|
929
|
+
const named = pkgs.find((p) =>
|
|
930
|
+
p.published && new RegExp(`${p.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w./-])`).test(line),
|
|
931
|
+
);
|
|
932
|
+
if (!named) return;
|
|
933
|
+
const claim = (cells[col].match(/\b\d+\.\d+\.\d+\b/) ?? [])[0];
|
|
934
|
+
if (!claim || claim === named.published) return;
|
|
935
|
+
stale.push(
|
|
936
|
+
`docs/${rel}:${i + 1} states ${named.name} PUBLISHED at ${claim} — kit.json records ${named.published}. ` +
|
|
937
|
+
`Checked by COLUMN: a row can carry the right pinned version and a stale published one, and comparing the whole row passes on the pinned half.`,
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const tally = `${rows} version-asserting row(s) in ${scanned} doc(s) that claim currency (a dated snapshot is not checked — it was true when written), against kit.json's version AND published records`;
|
|
864
943
|
if (stale.length) {
|
|
865
944
|
push("version-truth", "warn", [
|
|
866
945
|
...stale,
|
package/src/lib/artifacts.mjs
CHANGED
|
@@ -48,6 +48,12 @@ export const ARTIFACTS = [
|
|
|
48
48
|
{ path: "docs/phases/phaseN/README.md", scope: "project", purpose: "Phase overview", writtenBy: "/plan-phase", readBy: "/start-session", rules: "—" },
|
|
49
49
|
{ path: "docs/phases/phaseN/PHASEN_TASKS.md", scope: "project", purpose: "Detailed checkbox tasks", writtenBy: "/plan-phase", readBy: "/check-task, /start-session, groundwork status", rules: "Progress recomputed by the helper script" },
|
|
50
50
|
{ path: "docs/phases/phaseN/ITEMS.md", grammar: "queue.v1", scope: "project", purpose: "Queue items pulled into a phase, verbatim (the phase's working set)", writtenBy: "aide curates; coordinator sets status", readBy: "groundwork doctor, whoever executes the phase", rules: "queue.v1 — items live under a `## Queue` h2; `### Task N` groups them. Watched by the same checks as QUEUE.md" },
|
|
51
|
+
// DELIBERATELY NO `grammar`. This doc is registered so `doc-registration`
|
|
52
|
+
// can see it, but it is NOT a work-doc grammar: a David-blocked list that
|
|
53
|
+
// parses as a queue IS a queue, and every queue check would begin reading
|
|
54
|
+
// it as claimable work. The whole point of the file is that its contents
|
|
55
|
+
// are the items the fleet CANNOT pick up.
|
|
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" },
|
|
51
57
|
{ 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" },
|
|
52
58
|
{ 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" },
|
|
53
59
|
{ 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" },
|