@forwardimpact/libwiki 0.3.0 → 0.3.2

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.
Files changed (49) hide show
  1. package/README.md +53 -33
  2. package/package.json +1 -1
  3. package/src/active-claims.js +5 -5
  4. package/src/agent-roster.js +2 -2
  5. package/src/audit/admission.js +14 -11
  6. package/src/audit/conflict-markers-rule.js +9 -9
  7. package/src/audit/grammar.js +21 -18
  8. package/src/audit/rule-builders.js +20 -19
  9. package/src/audit/rules.js +30 -27
  10. package/src/audit/scopes.js +34 -33
  11. package/src/audit/status-row.js +14 -15
  12. package/src/block-renderer.js +5 -4
  13. package/src/boot.js +10 -8
  14. package/src/budget-gate.js +39 -36
  15. package/src/budget.js +3 -3
  16. package/src/cli-definition.js +23 -20
  17. package/src/commands/audit.js +3 -3
  18. package/src/commands/boot.js +1 -1
  19. package/src/commands/claim.js +44 -38
  20. package/src/commands/curate.js +34 -31
  21. package/src/commands/fix.js +68 -64
  22. package/src/commands/inbox.js +1 -1
  23. package/src/commands/init.js +12 -7
  24. package/src/commands/ledger.js +11 -11
  25. package/src/commands/log.js +19 -17
  26. package/src/commands/memo.js +4 -1
  27. package/src/commands/product-mix.js +16 -15
  28. package/src/commands/refresh.js +25 -22
  29. package/src/commands/rotate.js +9 -8
  30. package/src/commands/sync.js +26 -17
  31. package/src/conflict-markers.js +21 -21
  32. package/src/constants.js +37 -33
  33. package/src/gitattributes.js +10 -9
  34. package/src/integrity.js +29 -27
  35. package/src/issue-list-renderer.js +24 -16
  36. package/src/lane-files.js +11 -10
  37. package/src/ledger/anchor.js +6 -6
  38. package/src/ledger/projection.js +35 -32
  39. package/src/ledger/reader.js +4 -4
  40. package/src/marker-scanner.js +3 -2
  41. package/src/sanitize.js +12 -11
  42. package/src/secret-gate.js +41 -40
  43. package/src/status.js +12 -11
  44. package/src/storyboard-skeleton.js +20 -18
  45. package/src/util/agent-flag.js +8 -8
  46. package/src/util/clock.js +1 -1
  47. package/src/util/wiki-dir.js +7 -7
  48. package/src/weekly-log.js +115 -101
  49. package/src/wiki-sync.js +393 -361
package/README.md CHANGED
@@ -8,11 +8,11 @@ parallel work.
8
8
 
9
9
  <!-- END:description -->
10
10
 
11
- A wiki under `wiki/` holds each agent's running state: per-agent summaries,
11
+ A wiki under `wiki/` holds each agent's current state: per-agent summaries,
12
12
  weekly logs, shared memory (priorities and active claims), and monthly
13
- storyboards. `libwiki` keeps that wiki coherent across sessions — agents boot
14
- from it, write decisions back, send memos to each other, and audit the
15
- result against a declarative rule set.
13
+ storyboards. `libwiki` keeps that wiki coherent across sessions. Agents boot
14
+ from it. They write decisions back. They send memos to each other. They audit
15
+ the result against a declarative rule set.
16
16
 
17
17
  The primary interface is the `gemba-wiki` CLI. The library also exposes a few
18
18
  helpers for programmatic use.
@@ -29,7 +29,7 @@ npx gemba-wiki audit
29
29
 
30
30
  Every command accepts `--wiki-root` (default `wiki/`) and `--today` (default
31
31
  today, ISO date). Agent-scoped commands require an explicit `--agent <name>`
32
- (`--from` for `memo`) and fail closed without it — there is no environment
32
+ (`--from` for `memo`). They fail closed without it. There is no environment
33
33
  fallback. The only exception is `release --expired`, a cross-agent cleanup
34
34
  sweep that runs without `--agent`.
35
35
 
@@ -50,8 +50,8 @@ npx gemba-wiki log note --agent X --field "PR Status" --body "merged"
50
50
  npx gemba-wiki log done --agent X
51
51
  ```
52
52
 
53
- Appends to `wiki/<agent>-YYYY-WVV.md`. Auto-rotates to `*-partN.md` when the
54
- line budget would be exceeded.
53
+ `log` appends to `wiki/<agent>-YYYY-WVV.md`. It auto-rotates to `*-partN.md`
54
+ when the log would exceed the line budget.
55
55
 
56
56
  ### `claim` / `release` — coordinate work
57
57
 
@@ -61,9 +61,10 @@ npx gemba-wiki release --agent X --target spec-NNNN
61
61
  npx gemba-wiki release --expired
62
62
  ```
63
63
 
64
- Maintains the `## Active Claims` table in `MEMORY.md`. Duplicates refused;
65
- row absent means settled. `expires_at` defaults to `claimed_at + 1 day` — a
66
- claim is a short-lived "shipping this now" assertion, not a lease.
64
+ These commands maintain the `## Active Claims` table in `MEMORY.md`. They
65
+ refuse duplicates. An absent row means the claim is settled. `expires_at`
66
+ defaults to `claimed_at + 1 day`. A claim is a short-lived "shipping this
67
+ now" assertion. It is not a lease.
67
68
 
68
69
  ### `inbox` — triage memos
69
70
 
@@ -74,8 +75,8 @@ npx gemba-wiki inbox promote --agent X --index 0 [--owner X]
74
75
  npx gemba-wiki inbox drop --agent X --index 0
75
76
  ```
76
77
 
77
- Reads bullets under the `<!-- memo:inbox -->` marker in the agent's summary.
78
- `promote` moves a bullet into the cross-cutting priorities table.
78
+ `inbox` reads bullets under the `<!-- memo:inbox -->` marker in the agent's
79
+ summary. `promote` moves a bullet into the cross-cutting priorities table.
79
80
 
80
81
  ### `memo` — cross-team coordination
81
82
 
@@ -84,7 +85,7 @@ npx gemba-wiki memo --from X --to Y --message "audit d642ff0c"
84
85
  npx gemba-wiki memo --from X --to all --message "new XmR baseline"
85
86
  ```
86
87
 
87
- Inserts a bullet `- YYYY-MM-DD from **X**: ...` after the recipient's
88
+ `memo` inserts a bullet `- YYYY-MM-DD from **X**: ...` after the recipient's
88
89
  `<!-- memo:inbox -->` marker.
89
90
 
90
91
  ### `audit` — verify wiki state
@@ -93,16 +94,16 @@ Inserts a bullet `- YYYY-MM-DD from **X**: ...` after the recipient's
93
94
  npx gemba-wiki audit [--format text|json]
94
95
  ```
95
96
 
96
- Runs a declarative catalogue of rules across the wiki. Exits 0 on pass, 1
97
- on any failure. Text output: `WARN ...` and `FAIL ...` lines plus a
97
+ `audit` runs a declarative catalogue of rules across the wiki. It exits 0 on
98
+ pass and 1 on any failure. Text output: `WARN ...` and `FAIL ...` lines plus a
98
99
  `RESULT: ...` trailer. JSON output:
99
100
 
100
101
  ```json
101
102
  { "result": "pass|fail", "failures": [...], "warnings": [...] }
102
103
  ```
103
104
 
104
- Each finding carries a stable `id` for filtering. The catalogue lives in
105
- `src/audit/rules.js` — adding a rule is one literal.
105
+ Each finding carries a stable `id` so you can filter on it. The catalogue
106
+ lives in `src/audit/rules.js`. Each new rule is one literal.
106
107
 
107
108
  ### `rotate` — force a part split
108
109
 
@@ -110,8 +111,8 @@ Each finding carries a stable `id` for filtering. The catalogue lives in
110
111
  npx gemba-wiki rotate --agent X
111
112
  ```
112
113
 
113
- Renames the current weekly log to the next `-partN.md` and starts a fresh
114
- main file.
114
+ `rotate` renames the current weekly log to the next `-partN.md`. It then
115
+ starts a fresh main file.
115
116
 
116
117
  ### `refresh` — re-render storyboard blocks
117
118
 
@@ -119,11 +120,11 @@ main file.
119
120
  npx gemba-wiki refresh [storyboard-path]
120
121
  ```
121
122
 
122
- Re-renders `<!-- xmr:metric:csv-path -->` and `<!-- obstacles:open[:Nd] -->`
123
- marker blocks inside a storyboard from their backing CSV / GitHub state.
124
- Default path: `wiki/storyboard-YYYY-MMM.md` for the current month. Also sweeps
125
- every expired row from `MEMORY.md ## Active Claims` as part of the same
126
- deterministic refresh.
123
+ `refresh` re-renders `<!-- xmr:metric:csv-path -->` and
124
+ `<!-- obstacles:open[:Nd] -->` marker blocks inside a storyboard from the CSV
125
+ and GitHub state behind them. Default path: `wiki/storyboard-YYYY-MMM.md` for
126
+ the current month. It also sweeps every expired row from
127
+ `MEMORY.md ## Active Claims` as part of the same deterministic refresh.
127
128
 
128
129
  ### `init` / `push` / `pull` — wiki working tree
129
130
 
@@ -133,9 +134,9 @@ npx gemba-wiki push
133
134
  npx gemba-wiki pull
134
135
  ```
135
136
 
136
- `init` clones the wiki repo if missing, scaffolds Active Claims in
137
- `MEMORY.md`, and creates `wiki/metrics/<skill>/` directories. `push` and
138
- `pull` are thin wrappers over `git` with conflict handling.
137
+ `init` clones the wiki repo if it is missing. It scaffolds Active Claims in
138
+ `MEMORY.md`. It creates `wiki/metrics/<skill>/` directories. `push` and
139
+ `pull` are thin wrappers over `git` that handle conflicts.
139
140
 
140
141
  ## Programmatic API
141
142
 
@@ -149,14 +150,33 @@ import {
149
150
  bullet after the `<!-- memo:inbox -->` marker.
150
151
  - `listAgents({ agentsDir, wikiRoot })` — discover agents from
151
152
  `.claude/agents/*.md` and derive wiki summary paths.
152
- - `insertMarkers({ agentsDir, wikiRoot })` — idempotent insertion of the
153
- memo marker into existing summaries.
153
+ - `insertMarkers({ agentsDir, wikiRoot })` — insert the memo marker into
154
+ existing summaries. The call is idempotent.
154
155
  - `runAudit(rules, ctx)` — pure audit engine: `(rules, ctx) → findings[]`.
155
156
  - `RULES` — the audit rule catalogue (one literal per rule).
156
157
 
157
158
  ## Documentation
158
159
 
159
- - [Operate a Predictable Agent Team](https://www.forwardimpact.team/docs/libraries/predictable-team/index.md)
160
- - [Send a Memo or Update a Storyboard](https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-operations/index.md)
161
- - [Audit and Auto-Fix the Wiki](https://www.forwardimpact.team/docs/libraries/predictable-team/wiki-integrity/index.md)
162
- - [Allocate Collision-Ledger Entries for Parallel Work](https://www.forwardimpact.team/docs/libraries/predictable-team/collision-ledger/index.md)
160
+ - [Operate a Predictable Agent Team](https://www.gemba.team/docs/predictable-team/index.md)
161
+ - [Send a Memo or Update a Storyboard](https://www.gemba.team/docs/predictable-team/wiki-operations/index.md)
162
+ - [Audit and Auto-Fix the Wiki](https://www.gemba.team/docs/predictable-team/wiki-integrity/index.md)
163
+ - [Allocate Collision-Ledger Entries for Parallel Work](https://www.gemba.team/docs/predictable-team/collision-ledger/index.md)
164
+
165
+ ## Documentation home
166
+
167
+ libwiki is an import-only library. It declares no `bin`. The `gemba-wiki`
168
+ command ships with the Gemba product, which imports these modules. Run it
169
+ with `npx gemba-wiki`.
170
+
171
+ The package publishes as `@forwardimpact/libwiki` on the Forward Impact npm
172
+ scope. Install it with `npm install @forwardimpact/libwiki`. Its task guides
173
+ live on the Gemba site at <https://www.gemba.team/>. The Forward Impact library
174
+ guide tree at <https://www.forwardimpact.team/docs/libraries/index.md> is not
175
+ this library's guide home.
176
+
177
+ **Decision (2026-08-26):** the split package scope and guide host are
178
+ deliberate. libwiki stays a Gear npm package, so `package.json .homepage`
179
+ keeps <https://www.forwardimpact.team>. The agent-memory guides moved to
180
+ gemba.team with the rest of the Gemba product. The `## Documentation` list
181
+ above carries the current URLs. Old `forwardimpact.team/docs/libraries/`
182
+ addresses forward to gemba.team.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Wiki lifecycle for agent teams — persistent memory, declarative integrity audits, and a collision ledger so coordination survives across sessions and parallel work.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -5,9 +5,9 @@ import {
5
5
  ACTIVE_CLAIMS_TABLE_SEPARATOR,
6
6
  } from "./constants.js";
7
7
 
8
- // The header matcher is shared with the audit (via constants.js); the loose
9
- // separator and the 6-cell row parser stay local — they serve parsing, not the
10
- // audit's column-count check.
8
+ // This module and the audit share the header matcher through constants.js.
9
+ // The loose separator and the 6-cell row parser stay local. This module uses
10
+ // them to parse the table. The audit's column-count check does not use them.
11
11
  const SEPARATOR_RE = /^\|\s*---\s*\|/;
12
12
  const ROW_RE =
13
13
  /^\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*$/;
@@ -169,7 +169,7 @@ export function appendClaim(memoryText, claim, _today) {
169
169
  };
170
170
  }
171
171
 
172
- /** Remove the claim row matching (agent, target). Idempotent. */
172
+ /** Remove the claim row that matches (agent, target). Idempotent. */
173
173
  export function removeClaim(memoryText, { agent, target }) {
174
174
  const lines = memoryText.split("\n");
175
175
  const heading = findSection(lines);
@@ -186,7 +186,7 @@ export function removeClaim(memoryText, { agent, target }) {
186
186
  return { text: memoryText, removed: false };
187
187
  }
188
188
 
189
- /** Split claims into active vs expired based on `expires_at >= today`. */
189
+ /** Split claims into active and expired, based on `expires_at >= today`. */
190
190
  export function filterExpired(claims, today) {
191
191
  const active = [];
192
192
  const expired = [];
@@ -2,8 +2,8 @@ import path from "node:path";
2
2
  import { BROADCAST_TARGET } from "./constants.js";
3
3
 
4
4
  /**
5
- * List all agent markdown files in the agents directory, returning agent names
6
- * and summary paths.
5
+ * List all agent markdown files in the agents directory. Return the agent
6
+ * names and the summary paths.
7
7
  * @param {{agentsDir: string, wikiRoot: string}} dirs
8
8
  * @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
9
9
  */
@@ -1,13 +1,15 @@
1
1
  import path from "node:path";
2
2
 
3
- // Tracked-file enumerator for the admission scope. Yields the
4
- // admission universe: the wiki-relative paths the filename grammar governs.
3
+ // Tracked-file enumerator for the admission scope. It yields the
4
+ // admission universe. That universe holds the wiki-relative paths the
5
+ // filename grammar governs.
5
6
  //
6
- // The universe is the on-disk file tree under `wikiRoot`, intersected with the
7
- // git index when git state is present. Where there is no git state — a fresh
8
- // bootstrap or a test fixture — the universe is the whole walk. This keeps the
9
- // rule's true positive (a *git-tracked* residue) in scope while excluding VCS
10
- // internals and uncommitted scratch where a real repo exists.
7
+ // The enumerator walks the on-disk file tree under `wikiRoot`. It then
8
+ // intersects the walk with the git index when git state is present. Without
9
+ // git state (a fresh bootstrap or a test fixture), the universe is the whole
10
+ // walk. This keeps the rule's true positive (a *git-tracked* residue) in
11
+ // scope. It also excludes VCS internals and uncommitted scratch where a real
12
+ // repo exists.
11
13
 
12
14
  /** Recursively collect file paths (wiki-relative, POSIX) under `dir`. */
13
15
  function walk(absDir, wikiRoot, fs, out) {
@@ -25,9 +27,10 @@ function walk(absDir, wikiRoot, fs, out) {
25
27
 
26
28
  /**
27
29
  * Read the git index at `wikiRoot` as a set of wiki-relative paths, or `null`
28
- * when there is no git state (no `.git`, or the path is not a work tree — both
29
- * surface as a non-zero `git ls-files` exit). `-z` is used so paths with
30
- * unusual characters round-trip; output is NUL-delimited and relative to the
30
+ * when there is no git state. There is no git state when `.git` is absent, or
31
+ * when the path is not a work tree. Both cases surface as a non-zero
32
+ * `git ls-files` exit. The `-z` flag makes paths with unusual characters
33
+ * round-trip. Git delimits the output with NUL. The paths are relative to the
31
34
  * repository root, which is `wikiRoot`.
32
35
  */
33
36
  function trackedSet(wikiRoot, subprocess) {
@@ -39,7 +42,7 @@ function trackedSet(wikiRoot, subprocess) {
39
42
  /**
40
43
  * Enumerate the admission universe under `wikiRoot`.
41
44
  * @param {{wikiRoot: string, fs: object, subprocess: object}} options
42
- * `fs` is the sync filesystem surface (`runtime.fsSync`); `subprocess` is
45
+ * `fs` is the sync filesystem surface (`runtime.fsSync`). `subprocess` is
43
46
  * `runtime.subprocess` (its `runSync` shells out to git).
44
47
  * @returns {string[]} Wiki-relative POSIX paths, tracked-filtered when git state exists.
45
48
  */
@@ -1,14 +1,14 @@
1
1
  import { scanConflictMarkers } from "../conflict-markers.js";
2
2
 
3
3
  // Fail-severity audit rule that flags unresolved git conflict markers in any
4
- // audited wiki surface. Resolved by the `conflict-scan` scope (scopes.js),
5
- // which yields one `{ path, text, fenceExempt }` subject per file. The rule is
6
- // deliberately distinct from the budget rules: a marker block can fit inside
7
- // the word/line budget, and when it does trip the budget the budget hint
8
- // ("trim history") is actively wrong for this defect. A co-occurring size
9
- // breach therefore reports BOTH findings, never misattributing structure as
10
- // size. The hint directs the writer to adjudicate the merged form, never to
11
- // trim.
4
+ // audited wiki surface. The `conflict-scan` scope (scopes.js) resolves it.
5
+ // That scope yields one `{ path, text, fenceExempt }` subject per file. The
6
+ // rule is deliberately distinct from the budget rules. A marker block can fit
7
+ // inside the word/line budget. When it does trip the budget, the budget hint
8
+ // ("trim history") is actively wrong for this defect. So a co-occurring size
9
+ // breach reports BOTH findings. It never misattributes structure as size. The
10
+ // hint directs the writer to adjudicate the merged form. It never directs the
11
+ // writer to trim.
12
12
  export const CONFLICT_MARKER_RULE = {
13
13
  id: "conflict.markers",
14
14
  scope: "conflict-scan",
@@ -20,5 +20,5 @@ export const CONFLICT_MARKER_RULE = {
20
20
  : hits.map((h) => ({ lineNo: h.lineNo, kind: h.kind }));
21
21
  },
22
22
  message: (_s, r) => `unresolved git conflict marker (${r.kind})`,
23
- hint: "adjudicate the merged form: reconcile the two variants into the intended content, then delete the markers — this is corruption, not a size breach, so do not shorten history to clear it",
23
+ hint: "adjudicate the merged form. reconcile the two variants into the intended content, then delete the markers. this defect is corruption. it is not a size breach, so do not shorten history to clear it",
24
24
  };
@@ -1,10 +1,10 @@
1
1
  import path from "node:path";
2
2
  import { WEEKLY_LOG_NAME_RE, WEEKLY_LOG_PART_NAME_RE } from "../constants.js";
3
3
 
4
- // The wiki filename admission grammar. A pure classifier: given a
5
- // wiki-relative path, decide whether the filename grammar admits it. The
4
+ // The wiki filename admission grammar. It is a pure classifier. It takes a
5
+ // wiki-relative path and decides whether the filename grammar admits it. The
6
6
  // normative prose lives in memory-protocol.md's "Wiki Filename Grammar"
7
- // section; this module is its enforcement — one home per policy, so the two
7
+ // section. This module enforces that prose. One home per policy, so the two
8
8
  // cannot drift. The audit's `admission` scope is the only consumer.
9
9
 
10
10
  const NAMED_LEDGERS = new Set(["Home.md", "MEMORY.md", "STATUS.md"]);
@@ -12,9 +12,9 @@ const STORYBOARD_RE = /^storyboard-\d{4}-M\d{2}\.md$/;
12
12
  const DATED_DELIVERABLE_RE = /^(.+)-\d{4}-\d{2}-\d{2}\.md$/;
13
13
 
14
14
  // Calendar tokens, anchored at hyphen-segment boundaries so a token must occupy
15
- // whole `-`-delimited segments: `8080` *inside* a longer segment
16
- // (`release8080-notes`) is not a token, but a standalone `8080` segment is a
17
- // bare year. Anchoring with `(?:^|-)…(?=-|$)` is load-bearing — a per-segment
15
+ // whole `-`-delimited segments. `8080` *inside* a longer segment
16
+ // (`release8080-notes`) is not a token. A standalone `8080` segment is a
17
+ // bare year. The anchors `(?:^|-)…(?=-|$)` are load-bearing. A per-segment
18
18
  // split would silently miss the multi-segment week/month/date tokens.
19
19
  const CALENDAR_TOKEN_RES = [
20
20
  /(?:^|-)\d{4}-W\d{2}(?=-|$)/, // week YYYY-Www
@@ -33,7 +33,10 @@ export function hasCalendarToken(stem) {
33
33
  return CALENDAR_TOKEN_RES.some((re) => re.test(stem));
34
34
  }
35
35
 
36
- /** A root-level `.md` file whose stem carries no calendar token: the summary class. */
36
+ /**
37
+ * A root-level `.md` file whose stem carries no calendar token. This is the
38
+ * summary class.
39
+ */
37
40
  function isSummaryName(base) {
38
41
  if (!base.endsWith(".md")) return false;
39
42
  return !hasCalendarToken(base.slice(0, -".md".length));
@@ -41,8 +44,8 @@ function isSummaryName(base) {
41
44
 
42
45
  /**
43
46
  * The summary-class stem of a root-level basename, or `null`. Named ledgers
44
- * (`MEMORY.md` etc.) are not summaries. Used to derive the `rootSummaryAgents`
45
- * set that gates `<agent>/` sidecar directory admission.
47
+ * (`MEMORY.md` etc.) are not summaries. Callers use it to derive the
48
+ * `rootSummaryAgents` set that gates `<agent>/` sidecar directory admission.
46
49
  * @param {string} base - A root-level basename.
47
50
  * @returns {string|null}
48
51
  */
@@ -59,23 +62,23 @@ function classifyRootFile(base) {
59
62
  }
60
63
  if (STORYBOARD_RE.test(base)) return "admitted";
61
64
  const dated = base.match(DATED_DELIVERABLE_RE);
62
- // A dated deliverable's `<topic>` must itself be token-free, so a trailing
63
- // date cannot smuggle a token-bearing stem (`…-history-YYYY-MM-DD.md`) in.
65
+ // A dated deliverable's `<topic>` must itself be token-free, so a date at
66
+ // the end cannot smuggle a token-bearing stem (`…-history-YYYY-MM-DD.md`) in.
64
67
  if (dated && !hasCalendarToken(dated[1])) return "admitted";
65
68
  if (isSummaryName(base)) return "admitted";
66
- // Anything else at the root — non-`.md`, or a token-bearing name matching no
67
- // exact dated shape — is rejected.
69
+ // The classifier rejects anything else at the root. That covers a non-`.md`
70
+ // name, and a token-bearing name that matches no exact dated shape.
68
71
  return "rejected";
69
72
  }
70
73
 
71
74
  /**
72
75
  * Classify one wiki-relative path against the filename admission grammar.
73
76
  *
74
- * Root files (no `/`) are classified by their basename. A nested path is
75
- * admitted iff its first segment is an admitted root-level directory —
76
- * `metrics` or an `<agent>` that has a root summary-class file — and then every
77
- * file beneath it is admitted by membership (innards unpoliced). Directory
78
- * evaluation is at the wiki root level only.
77
+ * The grammar classifies root files (no `/`) by their basename. It admits a
78
+ * nested path iff the first segment is an admitted root-level directory. That
79
+ * is `metrics`, or an `<agent>` that has a root summary-class file. It then
80
+ * admits every file beneath that directory by membership (innards unpoliced).
81
+ * The grammar evaluates directories at the wiki root level only.
79
82
  *
80
83
  * @param {string} relPath - Path relative to the wiki root (POSIX separators).
81
84
  * @param {{rootSummaryAgents: Set<string>|string[]}} options
@@ -15,8 +15,8 @@ import {
15
15
  // Check builders and the derived matchers they share, extracted from rules.js
16
16
  // so the rule table stays under the per-file line cap. Each builder takes a
17
17
  // subject (plus optional ctx) and returns null | finding | finding[]. rules.js
18
- // imports exactly the symbols its rule table references; the pure constants in
19
- // constants.js/scopes.js it still imports straight from source.
18
+ // imports exactly the symbols its rule table references. It still imports the
19
+ // pure constants in constants.js/scopes.js straight from source.
20
20
 
21
21
  export const PRIORITY_INDEX_HEADING_RE = new RegExp(
22
22
  `^${PRIORITY_INDEX_HEADING}$`,
@@ -28,7 +28,7 @@ export const PRIORITY_SEPARATOR_RE =
28
28
  export const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
29
29
 
30
30
  // improvement-coach is the storyboard facilitator and carries no domain
31
- // metrics; only the five domain agents need their own H3.
31
+ // metrics. Only the five domain agents need their own H3.
32
32
  const STORYBOARD_DOMAIN_AGENTS = [
33
33
  "product-manager",
34
34
  "release-engineer",
@@ -69,9 +69,9 @@ export const columnCount = (expected) => (s) =>
69
69
  export const exists = (s) => (s.exists ? null : {});
70
70
  export const expired = (s, ctx) => (s.expires_at < ctx.today ? {} : null);
71
71
 
72
- // The heading must equal `requiredLine` exactly — a suffixed variant like
73
- // "### Decision — <summary>" does not satisfy it, but is reported as a
74
- // near miss so the writer fixes the heading instead of hunting for a
72
+ // The heading must equal `requiredLine` exactly. A suffixed variant like
73
+ // "### Decision — <summary>" does not satisfy it. The rule reports the variant
74
+ // as a near miss, so the writer fixes the heading and does not hunt for a
75
75
  // "missing" line that is right there.
76
76
  function entryHasDecision(lines, startIdx, requiredLine, stopRe) {
77
77
  let seen = 0;
@@ -99,11 +99,11 @@ export const decisionWithin5 =
99
99
  return offenders.length === 0 ? null : offenders;
100
100
  };
101
101
 
102
- // Flag entry-shaped `## ` headings that the rotation seam-finder would skip —
103
- // the grammar-drift that degrades a whole file to one unsplittable prologue and
104
- // is otherwise silent (the decision-block rule matches only dated headings).
105
- // Uses the same WEEKLY_LOG_SEAM_RE the seam-finder uses, so the flagged set is
106
- // exactly the complement of the rotatable set.
102
+ // Flag entry-shaped `## ` headings that the rotation seam-finder would skip.
103
+ // This is the grammar-drift that degrades a whole file to one unsplittable
104
+ // prologue and is otherwise silent (the decision-block rule matches only dated
105
+ // headings). The check uses the same WEEKLY_LOG_SEAM_RE the seam-finder uses,
106
+ // so the flagged set is exactly the complement of the rotatable set.
107
107
  export const headingGrammarDrift = (s) => {
108
108
  const offenders = [];
109
109
  for (let i = 0; i < s.fileLines.length; i++) {
@@ -178,7 +178,7 @@ export const weeklyAgentMismatch = (s) => {
178
178
  // Carry surface: the H1 agent slug (`# <agent> — Carries`) must agree with the
179
179
  // filename prefix (`<agent>-carries.md`), the carry analogue of the summary /
180
180
  // weekly-log agreement rules. The H1 is the slug form already, so slugify is a
181
- // no-op for well-formed files; it normalises a stray capitalisation otherwise.
181
+ // no-op for well-formed files. Otherwise it normalises a stray capitalisation.
182
182
  export const carryAgentMismatch = (s) => {
183
183
  const m = s.firstLine.match(CARRY_SURFACE_H1_RE);
184
184
  if (!m) return null;
@@ -186,10 +186,10 @@ export const carryAgentMismatch = (s) => {
186
186
  return titleSlug === s.agentPrefix ? null : { titleSlug };
187
187
  };
188
188
 
189
- // Each Carry entry is an H3 block; every block must name a clearance trigger
189
+ // Each Carry entry is an H3 block. Every block must name a clearance trigger
190
190
  // (the `**Carry-clearance:**` marker). Walk the file's H3 boundaries and emit
191
- // one finding per block missing the marker — the finding[]-returning shape
192
- // `nothingAfterH2` uses.
191
+ // one finding for each block that lacks the marker. This uses the same
192
+ // finding[] shape as `nothingAfterH2`.
193
193
  export const carryEntryHasClearance = (s) => {
194
194
  const offenders = [];
195
195
  let blockStart = -1;
@@ -221,10 +221,11 @@ export const AGENT_H3_REQUIREMENTS = STORYBOARD_DOMAIN_AGENTS.map((agent) => ({
221
221
  // -- Metrics CSV duplicate rows --
222
222
 
223
223
  // Report every data line byte-identical to an earlier data line in the same
224
- // CSV. Line 1 is the header (positionally) and blank lines are skipped; the
225
- // header is never a duplicate subject. Keying on exact line equality gives the
226
- // spec's exit path for free: any column edit (run id or note) on one row makes
227
- // the pair non-identical and stops the finding firing.
224
+ // CSV. Line 1 is the header (positionally) and the check skips blank lines.
225
+ // The header is never a duplicate subject. The check keys on exact line
226
+ // equality, which gives the spec's exit path for free. Any column edit (run id
227
+ // or note) on one row makes the pair non-identical, so the finding no longer
228
+ // fires.
228
229
  export const duplicateCsvRows = (s) => {
229
230
  const seen = new Set();
230
231
  const findings = [];