@forwardimpact/libwiki 0.3.0 → 0.3.1

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 +30 -29
  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 +19 -16
  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,8 +150,8 @@ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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 = [];
@@ -56,9 +56,9 @@ import {
56
56
  import { STATUS_ROW_RULES } from "./status-row.js";
57
57
 
58
58
  // The budget predicates the post-landing pre-push gate re-runs over the
59
- // outgoing tree. Naming the ids here keeps the gate selecting rules by
60
- // membership of this set, so a future predicate change to any of these
61
- // rules flows through the gate with no gate-code change.
59
+ // outgoing tree. This set names the ids, so the gate selects rules by
60
+ // membership of the set. A future predicate change to any of these
61
+ // rules then flows through the gate with no gate-code change.
62
62
  export const BUDGET_RULE_IDS = new Set([
63
63
  "summary.line-budget",
64
64
  "summary.word-budget",
@@ -110,7 +110,7 @@ export const RULES = [
110
110
  severity: "fail",
111
111
  check: lineBudget(SUMMARY_LINE_BUDGET),
112
112
  message: (_s, r) => `${r.value} lines (limit ${SUMMARY_LINE_BUDGET})`,
113
- hint: "trim history into the weekly log; the summary holds settled state, not history",
113
+ hint: "trim history into the weekly log. the summary holds settled state. it does not hold history",
114
114
  },
115
115
  {
116
116
  id: "summary.word-budget",
@@ -118,7 +118,7 @@ export const RULES = [
118
118
  severity: "fail",
119
119
  check: wordBudget(SUMMARY_WORD_BUDGET),
120
120
  message: (_s, r) => `${r.value} words (limit ${SUMMARY_WORD_BUDGET})`,
121
- hint: "trim history into the weekly log; the summary holds settled state, not history",
121
+ hint: "trim history into the weekly log. the summary holds settled state. it does not hold history",
122
122
  },
123
123
  {
124
124
  id: "summary.h1-agent-matches-filename",
@@ -176,7 +176,7 @@ export const RULES = [
176
176
  check: headingGrammarDrift,
177
177
  message: (_s, r) =>
178
178
  `Entry heading '${r.observed}' does not match the dated grammar`,
179
- hint: "weekly-log entry headings must be '## YYYY-MM-DD'; open entries with `gemba-wiki log decision/note`, which emit a conforming heading the rotation seam-finder can split",
179
+ hint: "weekly-log entry headings must be '## YYYY-MM-DD'. open entries with `gemba-wiki log decision/note`, which emit a heading that conforms and that the rotation seam-finder can split",
180
180
  },
181
181
  {
182
182
  id: "decision-block.heading-within-5",
@@ -189,9 +189,9 @@ export const RULES = [
189
189
  }),
190
190
  message: (_s, r) =>
191
191
  r.nearMiss
192
- ? `Entry opens with '${r.nearMiss}'; the heading must be exactly '${DECISION_HEADING}' — move the suffix into the body`
192
+ ? `Entry opens with '${r.nearMiss}'. The heading must be exactly '${DECISION_HEADING}'. Move the suffix into the body`
193
193
  : `Entry lacks a line that is exactly '${DECISION_HEADING}'`,
194
- hint: `open each '## YYYY-MM-DD' entry with \`gemba-wiki log decision\`, which emits a line containing exactly '${DECISION_HEADING}' (no suffix — the check is an exact match); put the one-line summary in the body below it, drawn from the entry's own narrative — do not invent rationale the entry does not support`,
194
+ hint: `open each '## YYYY-MM-DD' entry with \`gemba-wiki log decision\`. it emits a line that contains exactly '${DECISION_HEADING}' (no suffix, because the check is an exact match). put the one-line summary in the body below it, drawn from the entry's own narrative. do not invent rationale the entry does not support`,
195
195
  },
196
196
 
197
197
  // -- Weekly logs (sealed parts) --
@@ -211,7 +211,7 @@ export const RULES = [
211
211
  check: headingGrammarDrift,
212
212
  message: (_s, r) =>
213
213
  `Entry heading '${r.observed}' does not match the dated grammar`,
214
- hint: "weekly-log entry headings must be '## YYYY-MM-DD'; open entries with `gemba-wiki log decision/note`, which emit a conforming heading the rotation seam-finder can split",
214
+ hint: "weekly-log entry headings must be '## YYYY-MM-DD'. open entries with `gemba-wiki log decision/note`, which emit a heading that conforms and that the rotation seam-finder can split",
215
215
  },
216
216
  {
217
217
  id: "weekly-log-part.line-budget",
@@ -220,7 +220,7 @@ export const RULES = [
220
220
  remediation: "rotate",
221
221
  check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
222
222
  message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
223
- hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams; only a single '### ' block that alone exceeds the budget remains for a human to shorten",
223
+ hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams. only a single '### ' block that alone exceeds the budget remains for a human to shorten",
224
224
  },
225
225
  {
226
226
  id: "weekly-log-part.word-budget",
@@ -229,7 +229,7 @@ export const RULES = [
229
229
  remediation: "rotate",
230
230
  check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
231
231
  message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
232
- hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams; only a single '### ' block that alone exceeds the budget remains for a human to shorten",
232
+ hint: "`bunx gemba-wiki fix` re-bisects an over-budget part at its day-section seams and, for a lone over-cap day, at its '### ' block seams. only a single '### ' block that alone exceeds the budget remains for a human to shorten",
233
233
  },
234
234
  {
235
235
  id: "weekly-log-part.h1-agent-matches-filename",
@@ -242,10 +242,11 @@ export const RULES = [
242
242
  },
243
243
 
244
244
  // -- Carry surfaces --
245
- // No `h1-shape` rule: unlike the weekly logs (classified on filename alone),
246
- // the carry classifier (scopes.js) requires the Carry H1 before assigning the
247
- // scope, so a malformed-H1 file is left unclassified rather than reaching an
248
- // h1-shape rule. The two rules below are the reachable, failable set (SC #2).
245
+ // There is no `h1-shape` rule. The classifier classifies a weekly log on
246
+ // filename alone. The carry classifier (scopes.js) instead requires the
247
+ // Carry H1 before it assigns the scope. So it leaves a malformed-H1 file
248
+ // unclassified, and that file never reaches an h1-shape rule. The two rules
249
+ // below are the reachable, failable set (SC #2).
249
250
 
250
251
  {
251
252
  id: "carry-surface.h1-agent-matches-filename",
@@ -282,7 +283,7 @@ export const RULES = [
282
283
  when: memoryExists,
283
284
  check: lineBudget(MEMORY_LINE_BUDGET),
284
285
  message: (_s, r) => `${r.value} lines (limit ${MEMORY_LINE_BUDGET})`,
285
- hint: "MEMORY.md holds settled cross-cutting state, not history; release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
286
+ hint: "MEMORY.md holds settled cross-cutting state. it does not hold history. release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
286
287
  },
287
288
  {
288
289
  id: "memory.word-budget",
@@ -291,7 +292,7 @@ export const RULES = [
291
292
  when: memoryExists,
292
293
  check: wordBudget(MEMORY_WORD_BUDGET),
293
294
  message: (_s, r) => `${r.value} words (limit ${MEMORY_WORD_BUDGET})`,
294
- hint: "MEMORY.md holds settled cross-cutting state, not history; release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
295
+ hint: "MEMORY.md holds settled cross-cutting state. it does not hold history. release settled claims, prune stale priority rows, and move event-by-event detail to the relevant ledger page or weekly log",
295
296
  },
296
297
  {
297
298
  id: "memory.priority-heading",
@@ -400,7 +401,7 @@ export const RULES = [
400
401
  when: storyboardExists,
401
402
  check: lineBudget(STORYBOARD_LINE_BUDGET),
402
403
  message: (_s, r) => `${r.value} lines (limit ${STORYBOARD_LINE_BUDGET})`,
403
- hint: "see per-section word budgets in storyboard-template.md; retire prior-session Headlines/Notes/Next-review entries to weekly logs",
404
+ hint: "see per-section word budgets in storyboard-template.md. retire prior-session Headlines/Notes/Next-review entries to weekly logs",
404
405
  },
405
406
  {
406
407
  id: "storyboard.word-budget",
@@ -409,7 +410,7 @@ export const RULES = [
409
410
  when: storyboardExists,
410
411
  check: wordBudget(STORYBOARD_WORD_BUDGET),
411
412
  message: (_s, r) => `${r.value} words (limit ${STORYBOARD_WORD_BUDGET})`,
412
- hint: "see per-section word budgets in storyboard-template.md; retire prior-session Headlines/Notes/Next-review entries to weekly logs",
413
+ hint: "see per-section word budgets in storyboard-template.md. retire prior-session Headlines/Notes/Next-review entries to weekly logs",
413
414
  },
414
415
  {
415
416
  id: "storyboard.markers-balanced.xmr",
@@ -454,8 +455,9 @@ export const RULES = [
454
455
  hint: "every '<!-- agent-experiments -->' needs a matching '<!-- /agent-experiments -->'",
455
456
  },
456
457
 
457
- // -- Metrics CSVs (union merge keeps both sides on concurrent appends;
458
- // exact-duplicate rows are surfaced here, never silently removed) --
458
+ // -- Metrics CSVs (union merge keeps both sides on concurrent appends. The
459
+ // audit surfaces exact-duplicate rows here. It never silently removes
460
+ // them) --
459
461
 
460
462
  {
461
463
  id: "metrics-csv.duplicate-row",
@@ -464,7 +466,7 @@ export const RULES = [
464
466
  check: duplicateCsvRows,
465
467
  message: (_s, r) =>
466
468
  `Duplicate metrics row at line ${r.lineNo} (exact match of an earlier row)`,
467
- hint: "remove the surplus row, or differentiate a genuinely-distinct measurement by editing its run id or note so the rows are no longer identical",
469
+ hint: "remove the surplus row, or differentiate a genuinely-distinct measurement. edit its run id or note so the rows are no longer identical",
468
470
  },
469
471
 
470
472
  // -- STATUS.md rows (per-migration-unit sub-row schema) --
@@ -478,17 +480,18 @@ export const RULES = [
478
480
  // -- Filename admission --
479
481
 
480
482
  // The `admission` resolver yields one subject per git-tracked path the
481
- // filename grammar rejects, so the check always fires. Flag-for-human: a
482
- // wrong automated move or delete destroys memory, so `fix` routes this to the
483
- // human report (any non-`agent` remediation class does) and never touches the
484
- // file.
483
+ // filename grammar rejects, so the check always fires. This finding is a
484
+ // flag for a human. A wrong automated move or delete destroys memory, so
485
+ // `fix` routes it to the human report and never touches the file. Any
486
+ // non-`agent` remediation class does the same.
485
487
  {
486
488
  id: "admission.not-in-grammar",
487
489
  scope: "admission",
488
490
  severity: "fail",
489
491
  remediation: "flag",
490
492
  check: () => ({}),
491
- message: (s) => `${s.relPath} matches no wiki filename grammar class`,
493
+ message: (s) =>
494
+ `${s.relPath} matches no class in the wiki filename grammar`,
492
495
  hint: "rename to an admitted class, or extend the Wiki Filename Grammar section in memory-protocol.md and audit/grammar.js together (the single admission path)",
493
496
  },
494
497
  ];