@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
@@ -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
  ];
@@ -39,8 +39,8 @@ function listMdFiles(wikiRoot, fs) {
39
39
  }
40
40
 
41
41
  // Recursively collect every *.csv under `<wikiRoot>/metrics/`. The real layout
42
- // is `metrics/<skill>/<year>.csv` (two levels), so the walk recurses rather
43
- // than assuming a fixed depth. Uses readdirSync + statSync (rather than
42
+ // is `metrics/<skill>/<year>.csv` (two levels), so the walk recurses and does
43
+ // not assume a fixed depth. The walk uses readdirSync + statSync (rather than
44
44
  // `withFileTypes` Dirents) so it runs unchanged under the in-memory mock fs.
45
45
  function listCsvFiles(wikiRoot, fs) {
46
46
  const metricsRoot = path.join(wikiRoot, "metrics");
@@ -57,7 +57,7 @@ function listCsvFiles(wikiRoot, fs) {
57
57
  return found;
58
58
  }
59
59
 
60
- // Load a metrics CSV as an audit subject: `rows` is the array of line strings,
60
+ // Load a metrics CSV as an audit subject. `rows` is the array of line strings,
61
61
  // so a rule indexes `rows[i]` (a string) and `i + 1` is its line number.
62
62
  function loadCsv(filePath, fs) {
63
63
  return {
@@ -97,8 +97,8 @@ function loadFile(filePath, fs) {
97
97
  function classifyFile(filePath, fs) {
98
98
  const base = path.basename(filePath);
99
99
  if (EXCLUDED_BASES.has(base)) return null;
100
- // STATUS.md is loaded separately (readOptional in buildContext) and audited
101
- // via the dedicated `status-row` scope — skip the per-file classification.
100
+ // buildContext loads STATUS.md separately with readOptional. The dedicated
101
+ // `status-row` scope audits it. So skip the per-file classification.
102
102
  if (base === "STATUS.md") return null;
103
103
  if (NON_SUMMARY_PREFIXES.some((p) => base.startsWith(p))) return null;
104
104
  if (WEEKLY_LOG_NAME_RE.test(base)) {
@@ -109,23 +109,23 @@ function classifyFile(filePath, fs) {
109
109
  }
110
110
  const subject = loadFile(filePath, fs);
111
111
  // Carry surface: a `<agent>-carries.md` whose H1 matches the Carry H1 RE.
112
- // Both axes must match (filename prefix and H1), mirroring the summary
113
- // classifier. The two H1 REs end in distinct literals (`— Carries` vs
114
- // `— Summary`) so the branches cannot cross-capture regardless of order;
115
- // a name-match + H1-miss is left unclassified, like a malformed summary.
112
+ // Both axes must match (filename prefix and H1), like the summary
113
+ // classifier. The two H1 REs end in distinct literals (`— Carries` and
114
+ // `— Summary`) so the branches cannot cross-capture regardless of order.
115
+ // A name-match with an H1-miss stays unclassified, like a malformed summary.
116
116
  if (CARRY_SURFACE_NAME_RE.test(base)) {
117
117
  if (CARRY_SURFACE_H1_RE.test(subject.firstLine)) {
118
118
  return { kind: "carry-surface", subject };
119
119
  }
120
120
  return null;
121
121
  }
122
- // Files that do not match a summary or weekly-log shape are left
123
- // unclassified: stray files are not audited.
122
+ // A file that does not match a summary or weekly-log shape stays
123
+ // unclassified. The audit skips stray files.
124
124
  if (!SUMMARY_H1_RE.test(subject.firstLine)) return null;
125
125
  return { kind: "summary", subject };
126
126
  }
127
127
 
128
- // Read a file if present; an absent file yields empty text so callers audit
128
+ // Read a file if present. An absent file yields empty text so callers audit
129
129
  // "missing" uniformly. The common { path, text, exists } shape backs the
130
130
  // MEMORY.md, STATUS.md, and storyboard context loads.
131
131
  function readOptional(filePath, fs) {
@@ -139,7 +139,8 @@ function readOptional(filePath, fs) {
139
139
 
140
140
  // MEMORY.md carries the same line/word budget rules as the prose surfaces, so
141
141
  // its subject needs the `lines`/`words` counters those check builders read.
142
- // Counted off the canonical budget.js pair, like every other budgeted surface.
142
+ // The loader counts them off the canonical budget.js pair, like every other
143
+ // budgeted surface.
143
144
  function loadMemory(filePath, fs) {
144
145
  const base = readOptional(filePath, fs);
145
146
  return {
@@ -150,11 +151,11 @@ function loadMemory(filePath, fs) {
150
151
  }
151
152
 
152
153
  /**
153
- * Parse the rows inside STATUS.md's fenced block into audit subjects. Lines
154
- * outside the ``` fence (header prose) and blank lines are skipped. Each row
155
- * carries a `kind` from {@link parseStatusRowId} (`"spec"`, `"experiment"`, or
156
- * `null` for an unrecognized id); spec-shaped rules read the positional
157
- * `id`/`phase`/`status` fields, experiment rules read `cells`.
154
+ * Parse the rows inside STATUS.md's fenced block into audit subjects. The
155
+ * parser skips lines outside the ``` fence (header prose) and blank lines.
156
+ * Each row carries a `kind` from {@link parseStatusRowId} (`"spec"`,
157
+ * `"experiment"`, or `null` for an unrecognized id). Spec-shaped rules read
158
+ * the positional `id`/`phase`/`status` fields. Experiment rules read `cells`.
158
159
  * @param {string} statusText - The full STATUS.md contents.
159
160
  * @returns {Array<{lineNo: number, text: string, cells: string[], id: string, phase: string, status: string, kind: string|null}>}
160
161
  */
@@ -171,9 +172,9 @@ function parseStatusRows(statusText) {
171
172
  if (!inFence || line.trim() === "") continue;
172
173
  const cells = line.split("\t");
173
174
  // Classify by id prefix so a malformed `exp:` row (e.g. wrong cell count)
174
- // is still routed to the experiment rules, which flag it — rather than
175
- // slipping through the spec-shaped rules. parseStatusRowId returns the
176
- // structured fields only for a well-formed row; the rules read `cells`.
175
+ // still routes to the experiment rules, which flag it. It does not slip
176
+ // through the spec-shaped rules. parseStatusRowId returns the structured
177
+ // fields only for a well-formed row. The rules read `cells`.
177
178
  const isExp = typeof cells[0] === "string" && cells[0].startsWith("exp:");
178
179
  const parsed = parseStatusRowId(cells[0], cells);
179
180
  rows.push({
@@ -276,12 +277,12 @@ const SCOPE_RESOLVERS = {
276
277
 
277
278
  // Normalize every audited surface into a uniform `{ path, text, fenceExempt }`
278
279
  // subject for the conflict-marker scan. The per-file subjects (summaries,
279
- // weekly logs and sealed parts, storyboard) carry `fileLines`; MEMORY.md and
280
+ // weekly logs and sealed parts, storyboard) carry `fileLines`. MEMORY.md and
280
281
  // STATUS.md carry `text` (readOptional shape). `fenceExempt` is true for prose
281
- // surfaces, where a fence quotes content, and false for STATUS.md, whose fenced
282
- // rows are data — a marker there is never legitimate (per-surface fence
283
- // contract). Files absent from disk (missing MEMORY/STATUS/storyboard) yield
284
- // empty text and produce no findings.
282
+ // surfaces, where a fence quotes content. It is false for STATUS.md, whose
283
+ // fenced rows are data. A marker there is never legitimate (per-surface fence
284
+ // contract). A file absent from disk (no MEMORY/STATUS/storyboard) yields
285
+ // empty text and produces no findings.
285
286
  function conflictScanSubjects(ctx) {
286
287
  const subjects = [];
287
288
  const fileScopes = ["summary", "weekly-log-main", "weekly-log-part"];
@@ -320,12 +321,12 @@ export function resolveScope(scopeKey, ctx) {
320
321
  }
321
322
 
322
323
  /**
323
- * Build the admission slice: the tracked-file universe plus the
324
- * `rootSummaryAgents` set that gates `<agent>/` sidecar directories. The agent
325
- * set is derived first (a root-level summary-class file's stem) so the
326
- * `admission` scope can classify sidecar directories against it.
324
+ * Build the admission slice. It holds the tracked-file universe plus the
325
+ * `rootSummaryAgents` set that gates `<agent>/` sidecar directories. The
326
+ * function derives the agent set first (a root-level summary-class file's
327
+ * stem) so the `admission` scope can classify sidecar directories against it.
327
328
  *
328
- * Returns the empty universe when `subprocess` is absent — callers that only
329
+ * Returns the empty universe when `subprocess` is absent. Callers that only
329
330
  * read `.subjects` (the rotation pre-pass) skip the git read and the tree walk
330
331
  * entirely, and produce no `admission` findings.
331
332
  */
@@ -342,9 +343,9 @@ function buildAdmission(wikiRoot, fs, subprocess) {
342
343
  }
343
344
 
344
345
  /**
345
- * Build the audit context: classifies and loads every wiki file once.
346
+ * Build the audit context. It classifies and loads every wiki file once.
346
347
  * @param {{wikiRoot: string, today: string, fs: object, subprocess: object}} options
347
- * `fs` is the sync filesystem surface (`runtime.fsSync`); `subprocess` is
348
+ * `fs` is the sync filesystem surface (`runtime.fsSync`). `subprocess` is
348
349
  * `runtime.subprocess` (its `runSync` backs the admission scope's git read).
349
350
  */
350
351
  export function buildContext({ wikiRoot, today, fs, subprocess }) {
@@ -1,7 +1,7 @@
1
1
  import { STATUS_ID_REGEX } from "../status.js";
2
2
 
3
- // Validate every row inside wiki/STATUS.md's code fence. Rows are resolved by
4
- // the `status-row` scope in scopes.js; each subject carries
3
+ // Validate every row inside wiki/STATUS.md's code fence. The `status-row`
4
+ // scope in scopes.js resolves the rows. Each subject carries
5
5
  // `{ cells, id, phase, status, kind, text }`. Two row kinds share the fence:
6
6
  //
7
7
  // spec `{id}<TAB>{phase}<TAB>{status}` — three cells
@@ -40,7 +40,7 @@ export const STATUS_ROW_RULES = [
40
40
  when: hasThreeCells,
41
41
  check: (s) => (STATUS_ID_REGEX.test(s.id) ? null : { id: s.id }),
42
42
  message: (_s, r) => `Bad id '${r.id}' (expected ^\\d{4}(/[a-z0-9-]+)?$)`,
43
- hint: "spec ids are four digits; a sub-row appends `/<unit>` (e.g. 1370/libutil)",
43
+ hint: "spec ids are four digits. a sub-row appends `/<unit>` (e.g. 1370/libutil)",
44
44
  },
45
45
  {
46
46
  id: "status-row.phase",
@@ -73,11 +73,10 @@ export const STATUS_ROW_RULES = [
73
73
  hint: "each experiment row is `exp:{issue}<TAB>{state}<TAB>{pin}<TAB>{plan-ref}`",
74
74
  },
75
75
  {
76
- // An experiment-kind row is classified by its `exp:` id prefix
77
- // (scopes.js), so the spec `id-format` rule is skipped for it; this rule
78
- // enforces the `exp:\d+` id so a non-numeric issue (e.g. `exp:abc`) flags
79
- // rather than auditing clean — keeping the audit aligned with
80
- // STATUS_ID_REGEX / parseStatusRowId.
76
+ // scopes.js classifies an experiment-kind row by its `exp:` id prefix, so
77
+ // the spec `id-format` rule skips it. This rule enforces the `exp:\d+` id,
78
+ // so a non-numeric issue (e.g. `exp:abc`) flags and does not audit clean.
79
+ // That keeps the audit aligned with STATUS_ID_REGEX / parseStatusRowId.
81
80
  id: "status-row.exp-id-format",
82
81
  scope: "status-row",
83
82
  severity: "fail",
@@ -101,10 +100,10 @@ export const STATUS_ROW_RULES = [
101
100
  scope: "status-row",
102
101
  severity: "fail",
103
102
  when: hasFourCells,
104
- // The pin is decidable per state, with no "ever approved" inference: a
105
- // `registered` row has no pin (`-`); an `approved` row pins the 40-hex
106
- // head; a `cancelled` row may carry the retained pin or `-` (it may or may
107
- // not have been approved before cancellation), so both are accepted.
103
+ // The pin is decidable per state, with no "ever approved" inference. A
104
+ // `registered` row has no pin (`-`). An `approved` row pins the 40-hex
105
+ // head. A `cancelled` row may carry the retained pin or `-`, because it
106
+ // may or may not pass through `approved` first. So the check accepts both.
108
107
  check: (s) => {
109
108
  const [, state, pin] = s.cells;
110
109
  if (state === "registered") {
@@ -118,11 +117,11 @@ export const STATUS_ROW_RULES = [
118
117
  ? null
119
118
  : { state, pin, want: "`-` or a 40-hex SHA" };
120
119
  }
121
- return null; // bad state already flagged by exp-state
120
+ return null; // exp-state already flags a bad state
122
121
  },
123
122
  message: (_s, r) =>
124
123
  `Bad pin '${r.pin}' for state '${r.state}' (expected ${r.want})`,
125
- hint: "registered pins `-`; approved pins a 40-hex SHA; cancelled pins either",
124
+ hint: "registered pins `-`. approved pins a 40-hex SHA. cancelled pins either",
126
125
  },
127
126
  {
128
127
  id: "status-row.exp-planref",
@@ -131,6 +130,6 @@ export const STATUS_ROW_RULES = [
131
130
  when: hasFourCells,
132
131
  check: (s) => (/^#\d+$/.test(s.cells[3]) ? null : { planRef: s.cells[3] }),
133
132
  message: (_s, r) => `Bad plan-ref '${r.planRef}' (expected #NNN)`,
134
- hint: "the plan-ref names the issue carrying the execution plan, e.g. #NNN",
133
+ hint: "the plan-ref names the issue that carries the execution plan, e.g. #NNN",
135
134
  },
136
135
  ];
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { analyze, renderChart, MIN_POINTS } from "@forwardimpact/libxmr";
3
3
 
4
- /** Error thrown when an XmR block cannot be rendered due to missing CSV or metric. */
4
+ /** Error the renderer throws when an XmR block has no CSV or no metric. */
5
5
  export class BlockRenderError extends Error {
6
6
  /** Create a BlockRenderError with the given reason string. */
7
7
  constructor(reason) {
@@ -11,11 +11,12 @@ export class BlockRenderError extends Error {
11
11
  }
12
12
 
13
13
  /**
14
- * Render an XmR chart block for a metric by reading its CSV and producing
14
+ * Render an XmR chart block for a metric. Read its CSV and produce the
15
15
  * markdown lines.
16
16
  * @param {{metric: string, csvPath: string, projectRoot: string, fs: object, priorReadAnchor?: string|null}} options
17
- * `fs` is the sync filesystem surface (`runtime.fsSync`). `priorReadAnchor`,
18
- * when supplied, stamps per-signal provenance surfaced in the Signals line.
17
+ * `fs` is the sync filesystem surface (`runtime.fsSync`). `priorReadAnchor`
18
+ * stamps per-signal provenance when the caller supplies it. The Signals line
19
+ * surfaces that provenance.
19
20
  */
20
21
  export function renderBlock({
21
22
  metric,
package/src/boot.js CHANGED
@@ -130,10 +130,11 @@ function bulletItem(threshold, agent) {
130
130
  }
131
131
 
132
132
  // Advance the agent-section scan for one storyboard line that is NOT inside the
133
- // materialized block. Returns the next `inAgent` state and pushes an h3-bullet
134
- // item for the booting agent when one is found. An h2 ends the agent-section
135
- // scan (team-wide sections follow the last agent h3 — without this the scan
136
- // would run past the agent sections and misattribute team-wide bullets).
133
+ // materialized block. Returns the next `inAgent` state. When the scan finds an
134
+ // h3 bullet for the agent that boots, it pushes that item. An h2 ends the
135
+ // agent-section scan, because team-wide sections follow the last agent h3.
136
+ // Without this the scan would run past the agent sections and misattribute
137
+ // team-wide bullets.
137
138
  function scanAgentLine(line, agent, inAgent, items) {
138
139
  if (/^## /.test(line)) return false;
139
140
  const h3Match = line.match(/^### (.+)$/);
@@ -152,8 +153,9 @@ function parseStoryboardItems(text, agent) {
152
153
  let inBlock = false;
153
154
  for (const line of text.split("\n")) {
154
155
  // The materialized block carries `- #N [agent] …` bullets that the agent
155
- // scan must never capture; track it so the bullet loop skips inside it.
156
- // (Without it the legacy scan double-counted these as the last agent's bullets.)
156
+ // scan must never capture. Track it so the bullet loop skips inside it.
157
+ // (Without it the legacy scan double-counted these as the last agent's
158
+ // bullets.)
157
159
  if (AGENT_EXPERIMENTS_OPEN_RE.test(line)) {
158
160
  inBlock = true;
159
161
  inAgent = false;
@@ -204,7 +206,7 @@ function countInbox(text) {
204
206
  /**
205
207
  * Remaining budget for a budgeted surface: current value, cap, and headroom for
206
208
  * both the line and word budget. An absent file (empty text) reports zero usage
207
- * and full headroom so a writer sees the ceiling before composing.
209
+ * and full headroom so a writer sees the ceiling before they compose.
208
210
  */
209
211
  function headroom(text, lineCap, wordCap) {
210
212
  const lines = countLines(text);
@@ -237,7 +239,7 @@ function mapClaim(c) {
237
239
  /**
238
240
  * Build the boot digest JSON object.
239
241
  * @param {{wikiRoot: string, agent: string, today: string, fs: object}} options
240
- * `fs` is the sync filesystem surface (`runtime.fsSync`); `today` is an ISO
242
+ * `fs` is the sync filesystem surface (`runtime.fsSync`). `today` is an ISO
241
243
  * date string.
242
244
  */
243
245
  export function buildDigest({ wikiRoot, agent, today, fs }) {
@@ -1,14 +1,14 @@
1
1
  // Post-landing, pre-push budget re-validation on the size (word/line) axis.
2
2
  //
3
3
  // The wiki landing flow re-runs the audit's budget predicates over the
4
- // outgoing tree between landing and push, and refuses a push that introduces
4
+ // outgoing tree between landing and push. It refuses a push that introduces
5
5
  // or deepens a per-file budget breach this writer's push would publish. The
6
- // gate reuses the audit's budget rules by reference: it resolves the rule
7
- // objects named by `BUDGET_RULE_IDS` and calls each rule's own `check` (the
8
- // over-cap predicate) plus the same `countWords` / `countLines` the audit
9
- // builds its subjects from. It never re-defines a budget, never routes through
10
- // the `runRules` engine (which drops the numeric value and emits nothing under
11
- // cap), and never edits — it refuses, keeping commits local.
6
+ // gate reuses the audit's budget rules by reference. It resolves the rule
7
+ // objects named by `BUDGET_RULE_IDS`. It then calls each rule's own `check`
8
+ // (the over-cap predicate) plus the same `countWords` / `countLines` the audit
9
+ // builds its subjects from. It never re-defines a budget. It never routes
10
+ // through the `runRules` engine, which drops the numeric value and emits
11
+ // nothing under cap. It never edits. It refuses, which keeps commits local.
12
12
 
13
13
  import path from "node:path";
14
14
  import { BUDGET_RULE_IDS, RULES } from "./audit/rules.js";
@@ -16,9 +16,10 @@ import { buildContext, resolveScope } from "./audit/scopes.js";
16
16
  import { countLines, countWords } from "./budget.js";
17
17
 
18
18
  /**
19
- * Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES`, tagging each with
20
- * the count axis its id implies. Throws if a named id is missing from `RULES`,
21
- * so a rule rename surfaces here rather than silently dropping a predicate.
19
+ * Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES`. Tag each one with
20
+ * the count axis its id implies. It throws if a named id is missing from
21
+ * `RULES`, so a rule rename surfaces here and does not silently drop a
22
+ * predicate.
22
23
  * @returns {Array<{id: string, scope: string, axis: 'words'|'lines', check: Function}>}
23
24
  */
24
25
  export function budgetRules() {
@@ -36,10 +37,10 @@ export function budgetRules() {
36
37
  }
37
38
 
38
39
  /**
39
- * Enumerate which wiki files are budgeted, by reusing the audit's
40
- * classification. Subjects carry an absolute `path`, so each is reduced to the
41
- * `<file>` half of `git show <ref>:<file>` relative to `wikiRoot`. No count is
42
- * read off the working-dir subject — only the file identity and its scope.
40
+ * Enumerate which wiki files are budgeted. Reuse the audit's classification.
41
+ * Subjects carry an absolute `path`. Reduce each one to the `<file>` half of
42
+ * `git show <ref>:<file>`, relative to `wikiRoot`. This function reads no count
43
+ * off the working-dir subject. It reads only the file identity and its scope.
43
44
  * @param {object} ctx - An audit context from `buildContext`.
44
45
  * @param {string} wikiRoot - The wiki clone directory the paths are relative to.
45
46
  * @returns {Array<{relPath: string, scope: string}>}
@@ -56,12 +57,12 @@ export function budgetedFiles(ctx, wikiRoot) {
56
57
  }
57
58
 
58
59
  /**
59
- * Measure the budget predicates for the tree at `ref`. Reads each budgeted
60
- * file's blob via the cwd-bound `showFile`, counts it once with the audit's
61
- * counters, then for every budget rule on that file's scope records the axis
60
+ * Measure the budget predicates for the tree at `ref`. Read each budgeted
61
+ * file's blob through the cwd-bound `showFile`. Count it once with the audit's
62
+ * counters. Then, for every budget rule on that file's scope, record the axis
62
63
  * value and whether the rule's own `check` flags it over cap. An absent path
63
- * at the ref counts as 0 (matching the audit's "missing counts as empty"
64
- * posture); an unreadable ref makes `showFile` throw, which propagates.
64
+ * at the ref counts as 0. This matches the audit's "missing counts as empty"
65
+ * posture. An unreadable ref makes `showFile` throw, and the throw propagates.
65
66
  * @param {(ref: string, file: string) => Promise<string|null>} showFile
66
67
  * @param {string} ref - The tree-ish to measure (e.g. "HEAD", a SHA).
67
68
  * @param {Array<{relPath: string, scope: string}>} budgeted
@@ -88,15 +89,16 @@ export async function measureRef(showFile, ref, budgeted) {
88
89
  }
89
90
 
90
91
  /**
91
- * Compare the outgoing tree against the two push-input baselines and return the
92
+ * Compare the outgoing tree against the two push-input baselines. Return the
92
93
  * per-file/per-predicate refusal delta. For each (file, rule) the baseline is
93
- * the worse (higher) of the session-base and origin-tip values, treating an
94
- * absent measurement as 0. A predicate refuses iff the outgoing value is over
95
- * cap AND strictly exceeds that baseline — so equal-or-better states pass, and
96
- * a foreign breach the writer did not worsen passes. A `summary.*` breach on a
97
- * file listed in `exemptSummaryFiles` is surfaced instead of refused — the
98
- * memo-delivery seam, where blocking a delivery into deficient headroom would
99
- * enforce a contradiction the memo-headroom measures exist to resolve.
94
+ * the worse (higher) of the session-base and origin-tip values. An absent
95
+ * measurement counts as 0. A predicate refuses iff the outgoing value is over
96
+ * cap AND strictly exceeds that baseline. So equal-or-better states pass, and
97
+ * a foreign breach the writer did not worsen passes. The gate surfaces a
98
+ * `summary.*` breach on a file listed in `exemptSummaryFiles` instead of
99
+ * refusing it. Those files are the memo-delivery seam. A block on a delivery
100
+ * into deficient headroom would enforce a contradiction. The memo-headroom
101
+ * measures exist to resolve that contradiction.
100
102
  *
101
103
  * @param {object} args
102
104
  * @param {Map<string, Map<string, {value: number, overCap: boolean}>>} args.outgoing
@@ -137,14 +139,14 @@ export function revalidateBudgets({
137
139
  }
138
140
 
139
141
  /**
140
- * Run the gate end to end over the outgoing tree. Builds the audit context,
141
- * enumerates the budgeted files, measures the committed `HEAD` (what publishes)
142
- * and the two push-input baselines through the one `measureRef` path, then
143
- * computes the per-file/per-predicate delta. An unreadable baseline ref makes
144
- * `showFile` throw, which aborts the gate WITHOUT refusing — the gate only
145
- * refuses a regression it can prove, so a read failure surfaces (the push
146
- * proceeds) rather than fabricating a value-0 baseline that would wrongly block
147
- * a foreign pre-existing breach.
142
+ * Run the gate end to end over the outgoing tree. Build the audit context.
143
+ * Enumerate the budgeted files. Measure the committed `HEAD` (what publishes)
144
+ * and the two push-input baselines through the one `measureRef` path. Then
145
+ * compute the per-file/per-predicate delta. An unreadable baseline ref makes
146
+ * `showFile` throw, which aborts the gate WITHOUT refusing. The gate only
147
+ * refuses a regression it can prove. So a read failure surfaces and the push
148
+ * proceeds. The gate does not fabricate a value-0 baseline that would wrongly
149
+ * block a foreign pre-existing breach.
148
150
  *
149
151
  * @param {object} args
150
152
  * @param {(ref: string, file: string) => Promise<string|null>} args.showFile
@@ -181,7 +183,8 @@ export async function runBudgetGate({
181
183
  }
182
184
  originTip = await measureRef(showFile, originRef, budgeted);
183
185
  } catch {
184
- // Cannot prove a regression (unreadable ref) ⇒ do not refuse; fail-visible.
186
+ // Cannot prove a regression (unreadable ref) ⇒ do not refuse. This is the
187
+ // fail-visible posture.
185
188
  return { refusals: [], surfaced: [] };
186
189
  }
187
190
  return revalidateBudgets({
package/src/budget.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // Canonical line- and word-counters for the budgeted wiki surfaces. The audit
2
2
  // (`audit/scopes.js`) and the rotation primitive's bisecting seal
3
- // (`weekly-log.js`) both import this one pair so a part the seal calls
4
- // conforming cannot later be flagged by an audit counting differently.
3
+ // (`weekly-log.js`) both import this one pair. So no audit that counts
4
+ // differently can later flag a part the seal accepts as conforming.
5
5
 
6
- /** Count lines, not counting a trailing newline as an empty final line. */
6
+ /** Count lines. Do not count a trailing newline as an empty final line. */
7
7
  export function countLines(text) {
8
8
  return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
9
9
  }