@davidbalzan/groundwork-seam 0.1.14 → 0.1.16

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/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@ Started at 0.1.6 because a contract change landed in the tree with nowhere to re
6
6
  it — which is the same class of gap as the change itself: a fact that exists and has
7
7
  no home.
8
8
 
9
+ ## [0.1.16] — 2026-09-11
10
+
11
+ ### Fixed
12
+ - `zeroIsUnparsed` no longer reports a pruned-but-healthy queue as unparsed. It measured CONTENT (`substantiveLines`) on every axis — the right question for the done axis, the wrong one for a queue, which keeps its headings and explanatory prose by design. Measured on this repo's own queue: 793 substantive lines, zero rows, `unparsed: true`. Red-when-fine, one axis over from the failure the predicate was written to avoid.
13
+
14
+ ### Added
15
+ - `queueRowAttempts` and an optional `axis` argument to `zeroIsUnparsed`. For `"queue"` the discriminator is checkbox ROW ATTEMPTS — lines somebody wrote as rows — not authored content. **Omitting the axis preserves the previous behaviour exactly**, so the done and board axes are unchanged. Not "any list item": pruning the real queue leaves 0 checkbox lines and 9 plain bullets of template prose, so a bullet-counting predicate stays red on the very case being fixed.
16
+
9
17
  ## [0.1.8] — 2026-08-28
10
18
 
11
19
  ### Added
package/README.md CHANGED
@@ -4,4 +4,51 @@ Schema owner of `docs/QUEUE.md`, `docs/DONE.md`, `docs/WORKSTREAMS.md`, `docs/FA
4
4
 
5
5
  Markdown stays authoritative. This package is a library, not a daemon.
6
6
 
7
+ ## Find the answer without knowing the name
8
+
9
+ **This section is indexed by the QUESTION, not by the export.** If you are about to
10
+ write a regex against one of these documents, the answer is almost certainly here —
11
+ and if it is not, that is a gap in this package, not a reason to hand-roll one.
12
+ See *Gaps* below for the ones we know about.
13
+
14
+ > **Why this section exists.** A consumer re-derived three verdicts this package
15
+ > already owns, each with a local regex. That is a seam-design failure, not a
16
+ > consumer bug: every export was present and none was findable without already
17
+ > knowing its name. The fix is that the name is now the ANSWER rather than the
18
+ > search term.
19
+
20
+ | You want to know… | Ask | Never |
21
+ |---|---|---|
22
+ | Is this queue item done? | `queueItemsOf(doc)` → `item.done` | a `/\[x\]/` or `/done\|merged\|closed/i` regex |
23
+ | What are this queue's items at all? | `queueItemsOf(parseWorkDoc(text))` | splitting on `- [ ]` |
24
+ | What does this DONE entry cite? | `doneEntriesOf(doc)` → `entry.ref` | scraping `#\d+` from the line |
25
+ | Do two refs mean the same PR? | `refsMatch(a, b, { contextRepo })` | string equality |
26
+ | What CLOSED this queue row? | `closingCitation(row)` — a nullish result means *read the row* | any "closed by" in the body; 4 rows say it in prose |
27
+ | …the same commit? | `commitRefMatches(a, b, { contextRepo })` | comparing sha prefixes by hand |
28
+ | Which refs does this text carry? | `refsIn(text)` · `commitRefsIn(text)` | a local `#(\d+)` |
29
+ | Which board grammar is this? | `boardSchemaOf(doc)` | reading the header row yourself |
30
+ | Which board rows are `workstreams.v1`? | `workstreamsV1RowsOf(doc)` | indexing table cells positionally |
31
+ | Does this row carry a leading tag? | `leadingTagsOf(item)` · `sweepTagOf` · `awaitingOf` | matching `**[KEY:value]**` anywhere in the line |
32
+ | Is a zero an answer or an unread file? | `zeroIsUnparsed(count, sourceText, axis?)` | treating `0` as "none" |
33
+ | What is wrong with this document? | `workDocIssues(doc)` · `workDocIssuesDetailed(doc)` | your own validator |
34
+ | Which phase citations CLOSE a task? | `phaseClosureCitationsIn(text)` | `phaseCitationsIn`, which cannot tell "closes" from "unblocks" |
35
+ | Did this diff newly tick a task? | `newlyTickedInDiff(diff)` | diffing checkbox counts |
36
+
37
+ **A verdict this package exports is a verdict this package owns.** If your code
38
+ answers one of the questions above with its own regex, the two answers will
39
+ diverge — and the divergence is silent, because both sides parse cleanly.
40
+
41
+ ## Gaps — questions this package does NOT answer
42
+
43
+ Recorded so a consumer can tell "you missed it" from "we do not offer it", and so
44
+ the check that flags re-derivations knows what it must NOT flag.
45
+
46
+ - **Counting checkboxes in a phase file.** `newlyTickedInDiff` reads a DIFF;
47
+ `phaseCitationsIn` reads citations in prose. Neither counts `- [x]` in a phase
48
+ document. A consumer doing that is filling a gap, not duplicating a verdict.
49
+ - **The source LINE of a parsed item.** No item type carries a line, index or
50
+ offset, so anything that reports `docs/QUEUE.md:<line>` must scan lines itself.
51
+
52
+ ## Notes
53
+
7
54
  PR2 parse contract: any markdown table is a board, arity 5. Header-cell detection is PR3.
package/dist/docs.d.ts CHANGED
@@ -82,9 +82,19 @@ export type Block = {
82
82
  kind: "decisions";
83
83
  items: string[];
84
84
  };
85
+ /** What the continuation check looked at, and the RULE that produced it. */
86
+ export type ContinuationPopulation = {
87
+ rule: string;
88
+ linesConsidered: number;
89
+ loadBearing: number;
90
+ proseOnly: number;
91
+ excludedCommentBlockLines: number;
92
+ excludedBlockquoteLines: number;
93
+ };
85
94
  export type WorkDoc = {
86
95
  trailingNewline: boolean;
87
96
  blocks: Block[];
97
+ population?: ContinuationPopulation;
88
98
  warnings?: string[];
89
99
  advisories?: string[];
90
100
  };
@@ -94,12 +104,44 @@ export declare function parseQueueLine(line: string, section?: string): QueueIte
94
104
  /** The warning an invalid-priority item earns, quoting the tag and naming the set. */
95
105
  export declare function invalidPriorityIssue(item: QueueItem, line?: number): string;
96
106
  export declare function parseDoneLine(line: string, section?: string): DoneEntry | null;
107
+ /**
108
+ * Does this text carry a CITATION — an issue/PR ref or a commit sha?
109
+ *
110
+ * Deliberately delegates to `refsIn`/`hasCommitRef` rather than testing a local
111
+ * regex. The comment below those primitives says why: four separate matchers
112
+ * re-derived "what is a ref" in one day and each got it wrong differently. A
113
+ * fifth here would be the same mistake with a done line instead of a queue item.
114
+ *
115
+ * This is NOT `parseDoneLine`'s tail rule, and the difference is the point. That
116
+ * rule accepts any space-free token after ` — ` as a ref, which is right for a
117
+ * record it is about to keep but far too loose for deciding whether a HEADING was
118
+ * a completion in disguise: `## Phase 5 — shipped` would qualify. A malformation
119
+ * detector that cries wolf on ordinary prose gets switched off, so this one asks
120
+ * for a real citation shape.
121
+ */
122
+ export declare function carriesCitation(text: string): boolean;
123
+ export declare const doneReflessIssue: (line: number, text: string) => string;
124
+ export declare const doneHeadingFormIssue: (line: number, heading: string) => string;
97
125
  export declare function withoutCodeContext(text: string): string;
98
126
  export type Ref = {
99
127
  raw: string;
100
128
  repo: string | null;
101
129
  number: string;
102
130
  };
131
+ /**
132
+ * What a queue continuation line is HIDING, or null if it is only prose.
133
+ *
134
+ * ⟨q-3f81c92e⟩ — `queue.v1` is one line per item, so a continuation is kept for a
135
+ * human and dropped by every consumer. That is tolerable for commentary and not
136
+ * tolerable for the two things a reader ACTS on: the acceptance criteria and the
137
+ * citation. Hidden there, `list_work` hands a worker a row whose acceptance does
138
+ * not exist and whose PR cannot be followed, with nothing to indicate a loss.
139
+ *
140
+ * Reuses `refsIn` rather than re-deriving a ref pattern: four separate matchers
141
+ * re-derived ref matching in one day once and each got it wrong, which is why
142
+ * that primitive is shared (see `REF_RE`'s own note).
143
+ */
144
+ export declare function loadBearingContinuation(line: string): string | null;
103
145
  /** Every issue/PR reference in a string, qualified where the text qualified it. */
104
146
  export declare function refsIn(text: string): Ref[];
105
147
  /**
@@ -146,6 +188,51 @@ export declare function commitRefsIn(text: string): string[];
146
188
  * does not carry that state.
147
189
  */
148
190
  export declare function hasCommitRef(text: string): boolean;
191
+ /** Every state the fleet's boards actually declare. `unknown` is a real answer. */
192
+ export type WorkState = "blocked" | "in-progress" | "in-review" | "done" | "parked" | "orphaned" | "accepted-risk" | "staffable" | "skipped" | "no-downstream" | "unstaffable" | "unclassified" | "unknown";
193
+ /** The projection a renderer asks for: is anybody working on this? */
194
+ export type CoarseWorkState = "in-flight" | "finished" | "open" | "unknown";
195
+ export declare const declarationOf: (status: string) => string;
196
+ /**
197
+ * The state a board status DECLARES.
198
+ *
199
+ * Leading glyph decides; an unknown glyph is `unknown`; a glyphless status is read
200
+ * from its declaration only. Commentary never answers.
201
+ */
202
+ export declare function workStateOf(status: string | null | undefined): WorkState;
203
+ export declare function coarseOf(state: WorkState): CoarseWorkState;
204
+ /** What a queue row's CLOSING POSITION cites. */
205
+ export type ClosingCitation = {
206
+ token: string;
207
+ refs: Ref[];
208
+ commits: string[];
209
+ };
210
+ /**
211
+ * What the row's closing position cites, or `null` when it has none.
212
+ *
213
+ * ⚠ THE RETURN IS THREE-VALUED AND THE THIRD VALUE IS THE POINT:
214
+ * - a citation — the closing position names these refs/commits;
215
+ * - `null` — THERE IS NO CLOSING POSITION TO READ, so *read the row*.
216
+ *
217
+ * ⛔ `null` is returned BOTH when there is no token AND when no token cites
218
+ * anything, and the collapse is deliberate: a trailer saying something
219
+ * unparseable must not make a row uncited on the strength of its own prose.
220
+ *
221
+ * ⛔⛆ DO NOT "SIMPLIFY" THIS TO AN EMPTY REF LIST. An empty list reads as *"the
222
+ * closing position says nothing owes"* — a positive claim — where `null` says
223
+ * *"I cannot answer; ask the row."* On the delivery-marker side that substitution
224
+ * turns an honest `answerable: false` into a confident wrong zero.
225
+ *
226
+ * ⛔ THIS IS A **READER**, NOT A DECISION. It does not decide the tie, does not
227
+ * resolve repositories (that is `refsMatch` + a `contextRepo`, and a BARE `270`
228
+ * is correctly untieable with no origin), does not match `DONE.md` entries, and
229
+ * does not read commit subjects. Its consumers ask DIFFERENT questions of the
230
+ * same answer — `doctor` falls back to the whole row because it asks "can this
231
+ * row be tied at all?", while the delivery-marker gate stays untied and says so
232
+ * because it asks "which refs CLOSED it?" — so sharing this reader is correct
233
+ * and sharing either of those decisions is not.
234
+ */
235
+ export declare function closingCitation(text: string): ClosingCitation | null;
149
236
  /**
150
237
  * Do two cited shas name the same commit? Compared as a PREFIX, not
151
238
  * equality: git's own abbreviation convention means a 7-char short sha and a
@@ -154,11 +241,28 @@ export declare function hasCommitRef(text: string): boolean;
154
241
  * where the two citations were written at different times.
155
242
  */
156
243
  export declare function commitRefMatches(a: string, b: string): boolean;
244
+ export type LeadingTag = {
245
+ key: string;
246
+ value: string;
247
+ };
248
+ /** The tags an item CARRIES, in order, lowercased. Body mentions are not tags. */
249
+ export declare function leadingTagsOf(item: QueueItem): LeadingTag[];
157
250
  /**
158
251
  * The sweep tag an item CARRIES, or null. A tag quoted in the body is not carried
159
252
  * — that is the whole distinction, and it is why this is not a substring test.
160
253
  */
161
254
  export declare function sweepTagOf(item: QueueItem): string | null;
255
+ /**
256
+ * WHO an item declares it is waiting on, or null.
257
+ *
258
+ * `**[AWAITS:david]**` is a FIELD set by whoever files the item. It is never
259
+ * inferred: an item whose prose says "awaiting David's ruling" and carries no
260
+ * marker awaits nobody as far as any tool can tell. That is deliberate — reading
261
+ * intent out of English is how identity churned before ids were recorded, and
262
+ * gating goes the same way (q-e334937d). Routing verbs skip a carrier and
263
+ * REPORT it on its own axis; nothing makes it vanish.
264
+ */
265
+ export declare function awaitingOf(item: QueueItem): string | null;
162
266
  /** Items carrying `tag`. Never items merely mentioning it. */
163
267
  export declare function itemsBySweepTag(items: QueueItem[], tag: string): QueueItem[];
164
268
  /** Every carried tag with its item count, so a census never needs a grep. */
@@ -177,6 +281,85 @@ export declare function renderDoneLine(entry: DoneEntry): string;
177
281
  export declare function looksLikeBoardAttempt(cells: string[]): boolean;
178
282
  export declare function renderBoardRow(r: BoardRow | WorkstreamsV1Row | ExtensionRow | MalformedRow): string;
179
283
  export declare function parseWorkDoc(source: string): WorkDoc;
284
+ /**
285
+ * Stamp every queue item that carries no recorded id, and say which.
286
+ *
287
+ * THE WRITE IS WHERE IDENTITY GETS RECORDED, because a convention only works on
288
+ * seats that have read it and this file takes writes from outside the fleet
289
+ * (q-c50e9b83). Twice on 2026-09-04 a peer fleet appended a raw `- [ ] (P2)` row
290
+ * — by heredoc, then by a python insert — never invoking the stamping script:
291
+ * the id was not absorbed or lost, it was NEVER EMITTED. The gate then fired on
292
+ * whoever committed next rather than on the writer, so one seat reddened `main`
293
+ * for rows it had not written while the seat that could have prevented it never
294
+ * saw a failure.
295
+ *
296
+ * SO A SUPPORTED WRITE STAMPS EVERY UNSTAMPED ITEM, INCLUDING ONES THE CALLER
297
+ * DID NOT AUTHOR. Narrowing it to the caller's own rows would leave the absorbed
298
+ * row unstamped and keep the defect while moving the blame, which is the worst
299
+ * of both.
300
+ *
301
+ * WHY THAT IS SAFE RATHER THAN A SURPRISE DIFF: `withRecordedId` writes the id
302
+ * the item ALREADY HAS, derived from its current text, so no id changes value and
303
+ * no join key moves. Measured on the one row in this repo's queue that took both
304
+ * paths — a peer fleet's raw append, later stamped by hand: the derived id and the
305
+ * hand-written one are byte-equal.
306
+ *
307
+ * `stamped` exists so an ABSORBING WRITER LEARNS IT ABSORBED SOMETHING rather
308
+ * than discovering it in a diff. It lists only items this call stamped, never one
309
+ * that was already correct — a report that names every row is one its reader
310
+ * learns to ignore.
311
+ */
312
+ export declare function stampQueueIds(doc: WorkDoc): {
313
+ doc: WorkDoc;
314
+ stamped: string[];
315
+ };
316
+ /**
317
+ * The render every WRITER must use: identical to `renderWorkDoc` except that it
318
+ * stamps first and reports what it stamped.
319
+ *
320
+ * Kept separate from `renderWorkDoc` deliberately. That one is also the READ-side
321
+ * round-trip — parse, render, compare — and a render that mutates identity would
322
+ * make a faithful round-trip impossible to express. Two functions, two contracts:
323
+ * render what is there, or write what should be there.
324
+ */
325
+ export declare function renderWorkDocForWrite(doc: WorkDoc): {
326
+ text: string;
327
+ stamped: string[];
328
+ };
329
+ export type ZeroAxis = {
330
+ axis: "queue" | "done" | "board";
331
+ count: 0;
332
+ unparsed: boolean;
333
+ why?: string;
334
+ };
335
+ /** Non-blank lines that are not headings, frontmatter delimiters or comments. */
336
+ export declare function substantiveLines(text: string): number;
337
+ /** Checkbox list items outside frontmatter: queue rows somebody attempted to write. */
338
+ export declare function queueRowAttempts(text: string): number;
339
+ /**
340
+ * Is this axis's zero an ANSWER or an UNANSWERED QUESTION?
341
+ *
342
+ * `count > 0` is never in doubt, so this only ever qualifies a zero.
343
+ *
344
+ * THE QUEUE AXIS ASKS A NARROWER QUESTION THAN THE OTHERS, because on a queue
345
+ * document prose is not evidence of a missed row. `substantiveLines` was built
346
+ * for the done axis, where any authored line under an empty log is a fair
347
+ * reason to doubt the zero. A pruned `docs/QUEUE.md` keeps its headings AND its
348
+ * explanatory prose by design, so that same measure reported a perfectly
349
+ * healthy empty queue as unparsed — red-when-fine, measured at 793 substantive
350
+ * lines with zero rows present.
351
+ *
352
+ * So for `axis: "queue"` the discriminator is ROW ATTEMPTS, not content. The
353
+ * four states this separates, measured on this repo's real `docs/QUEUE.md`:
354
+ * populated (168 rows, count 168) `false` · pruned-but-prose-kept (0 attempts)
355
+ * `false` · heading-removed, so the rows exist but nothing parses them (168
356
+ * attempts, count 0) `true` · rows present but refused by the grammar (2
357
+ * attempts, count 0) `true`.
358
+ *
359
+ * Axis is OPTIONAL and omitting it preserves the old behaviour exactly, so the
360
+ * done and board axes are untouched by this change.
361
+ */
362
+ export declare function zeroIsUnparsed(count: number, sourceText: string, axis?: ZeroAxis["axis"]): boolean;
180
363
  export declare function renderWorkDoc(doc: WorkDoc): string;
181
364
  export declare function queueItemsOf(doc: WorkDoc): QueueItem[];
182
365
  export declare function doneEntriesOf(doc: WorkDoc): DoneEntry[];