@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
@@ -6,8 +6,8 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
6
6
 
7
7
  /**
8
8
  * Rotate the current weekly log to a sealed part file. Refuses an under-budget
9
- * target (exit 2) unless `--force`; the header-only floor stays a zero-exit
10
- * no-op even under `--force`; a missing target exits 2.
9
+ * target (exit 2) unless you pass `--force`. The header-only floor stays a
10
+ * zero-exit no-op even under `--force`. A missing target exits 2.
11
11
  */
12
12
  export function runRotateCommand(ctx) {
13
13
  const { runtime } = ctx.deps;
@@ -21,8 +21,8 @@ export function runRotateCommand(ctx) {
21
21
  const agent = resolved.agent;
22
22
  const wikiRoot = resolveWikiRoot(runtime, options);
23
23
  const today = options.today || currentDayIso(runtime);
24
- // Name the resolved target before any seal: the file follows from agent +
25
- // current week, not from any audit finding.
24
+ // Name the resolved target before any seal. The file follows from agent plus
25
+ // current week. No audit finding decides it.
26
26
  runtime.proc.stdout.write(
27
27
  `target → ${weeklyLogPath(wikiRoot, agent, today)}\n`,
28
28
  );
@@ -43,8 +43,8 @@ export function runRotateCommand(ctx) {
43
43
  }
44
44
  switch (result.status) {
45
45
  case "noop":
46
- // The header-only floor is a benign no-op; an under-budget or missing
47
- // target fails closed so a stale/typo'd invocation cannot pass silently.
46
+ // The header-only floor is a benign no-op. An under-budget or missing
47
+ // target fails closed. A stale or typo'd invocation cannot pass silently.
48
48
  if (result.reason === "floor") {
49
49
  runtime.proc.stdout.write(`no rotation needed for ${agent}\n`);
50
50
  return { ok: true };
@@ -79,14 +79,15 @@ export function runRotateCommand(ctx) {
79
79
  `section ${section} alone exceeds the budget ` +
80
80
  `(${lines} lines, ${words} words) and has no finer seam to split ` +
81
81
  `at: ${residuePath}\n` +
82
- `recover it by hand — shorten the section ` +
82
+ `recover it by hand. Shorten the section ` +
83
83
  `(see the memory protocol's manual-recovery convention)`,
84
84
  );
85
85
  return { ok: false, code: 1 };
86
86
  }
87
87
  default:
88
88
  // Defensive: the tagged union is exhaustive above, so this is
89
- // unreachable; kept so a future status can't fall through to no return.
89
+ // unreachable. It stays so a future status cannot fall through to no
90
+ // return.
90
91
  return { ok: true };
91
92
  }
92
93
  }
@@ -10,17 +10,17 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
10
10
 
11
11
  /**
12
12
  * Commit all wiki changes and push them through the secret-gated push path.
13
- * A detected secret or unavailable scanner fails the command closed and names
14
- * the cause on stderr without attempting the push; a clean write pushes or
15
- * reports nothing to push. The post-push tier-1 integrity detections surface in
16
- * the output; they never gate the push.
13
+ * A detected secret or unavailable scanner fails the command closed. It names
14
+ * the cause on stderr and never tries the push. A clean write pushes, or it
15
+ * reports nothing to push. The post-push tier-1 integrity detections surface
16
+ * in the output. They never gate the push.
17
17
  */
18
18
  export async function runPushCommand(ctx) {
19
19
  const { runtime, wikiSync } = ctx.deps;
20
20
  await wikiSync.inheritIdentity();
21
21
 
22
- // A caller that knows its narrower write-set passes `--paths` (repeatable);
23
- // the bare session-close invocation passes none and lands the session's own
22
+ // A caller that knows its narrower write-set passes `--paths` (repeatable).
23
+ // The bare session-close invocation passes none. It lands the session's own
24
24
  // dirty set under per-session checkout isolation.
25
25
  const paths = ctx.options?.paths?.length ? ctx.options.paths : undefined;
26
26
 
@@ -28,17 +28,19 @@ export async function runPushCommand(ctx) {
28
28
  try {
29
29
  result = await wikiSync.commitAndPush("wiki: update from session", paths);
30
30
  } catch (err) {
31
- // Honest CLI contract (the honest-CLI contract): non-zero on any non-land push
32
- // failure, and on the ancestry guard's refusal (the ancestry guard). The Stop-hook
33
- // recipe maps this to a stop-blocking exit; CI steps see a loud failure.
31
+ // Honest CLI contract (the honest-CLI contract): non-zero on any non-land
32
+ // push failure, and on the ancestry guard's refusal (the ancestry guard).
33
+ // The Stop-hook recipe maps this to a stop-blocking exit. CI steps see a
34
+ // loud failure.
34
35
  if (err instanceof WikiPushFailure || err instanceof AncestryRefusal) {
35
36
  runtime.proc.stderr.write(`${err.message}\n`);
36
37
  return { ok: false, code: 1 };
37
38
  }
38
39
  throw err;
39
40
  }
40
- // Fail the command closed on a secret-gate refusal (secret detected or scanner
41
- // unavailable); a clean push falls through to the normal reporting below.
41
+ // Fail the command closed on a secret-gate refusal (secret detected or
42
+ // scanner unavailable). A clean push falls through to the normal report
43
+ // below.
42
44
  const refusal = refusalEnvelope(runtime, result);
43
45
  if (refusal) return refusal;
44
46
  if (result.landed) {
@@ -52,7 +54,13 @@ export async function runPushCommand(ctx) {
52
54
  return { ok: true };
53
55
  }
54
56
 
55
- /** Fetch and rebase the local wiki on origin/master; on rebase conflict, return a non-zero envelope with a message to resolve manually or push first. After a clean pull, the tier-2 lane-record sweep surfaces any previous-session content absent at the fetched tip; it never gates the boot. */
57
+ /**
58
+ * Fetch and rebase the local wiki on origin/master. On a rebase conflict,
59
+ * return a non-zero envelope. Its message tells the caller to resolve the
60
+ * conflict by hand or to push first. After a clean pull, the tier-2
61
+ * lane-record sweep surfaces any previous-session content absent at the
62
+ * fetched tip. The sweep never gates the boot.
63
+ */
56
64
  export async function runPullCommand(ctx) {
57
65
  const { runtime, wikiSync, gitClient } = ctx.deps;
58
66
  await wikiSync.inheritIdentity();
@@ -64,19 +72,20 @@ export async function runPullCommand(ctx) {
64
72
  if (err instanceof WikiPullConflict) {
65
73
  createLogger("wiki", runtime).error(
66
74
  "pull",
67
- "rebase conflict — local divergence detected; resolve manually or push first",
75
+ "rebase conflict from local divergence. Resolve it by hand or push first",
68
76
  );
69
77
  return { ok: false, code: 1 };
70
78
  }
71
79
  throw err;
72
80
  }
73
81
 
74
- // Tier-2 sweep on the just-rebased tree. Detection-only: any failure degrades
75
- // to no detections, never throws into the flow, never changes the exit code.
82
+ // Tier-2 sweep on the just-rebased tree. This detects only. Any failure
83
+ // degrades to no detections. It never throws into the flow. It never changes
84
+ // the exit code.
76
85
  try {
77
86
  const wikiDir = resolveWikiRoot(runtime, ctx.options);
78
- // `--today` (ISO date) overrides the wall clock for deterministic tests;
79
- // a malformed value falls back to the runtime clock rather than NaN.
87
+ // `--today` (ISO date) overrides the wall clock for deterministic tests.
88
+ // A malformed value falls back to the runtime clock instead of NaN.
80
89
  const today = ctx.options.today
81
90
  ? Date.parse(ctx.options.today)
82
91
  : Number.NaN;
@@ -1,36 +1,36 @@
1
- // Structural detector for unresolved git conflict markers, shared by the wiki
2
- // audit's `conflict.markers` rule (audit/conflict-markers-rule.js) and the
3
- // WikiSync pre-push guard (wiki-sync.js). One home so the two layers cannot
1
+ // Structural detector for unresolved git conflict markers. The wiki audit's
2
+ // `conflict.markers` rule (audit/conflict-markers-rule.js) and the WikiSync
3
+ // pre-push guard (wiki-sync.js) both use it. One home so the two layers cannot
4
4
  // drift on what counts as a marker.
5
5
  //
6
- // Detection is line-anchored and structural, not a naive grep, so it does not
7
- // fire on markers legitimately quoted in prose:
6
+ // Detection is line-anchored and structural. It is not a naive grep, so it
7
+ // does not fire on markers legitimately quoted in prose:
8
8
  //
9
9
  // - Open (`<<<<<<<`) and close (`>>>>>>>`) marker lines fire UNCONDITIONALLY,
10
10
  // per file. A seal rotation can sever one conflict block across two sealed
11
- // files (the open in one, the separator + close in the other); a
12
- // complete-in-file-block matcher would miss both, so each marker form stands
11
+ // files (the open in one, the separator + close in the other). A matcher for
12
+ // a complete in-file block would miss both, so each marker form stands
13
13
  // alone. The single-space-or-end-of-line guard after the 7-char run admits
14
14
  // the stash-pop label forms (`<<<<<<< Updated upstream`,
15
- // `>>>>>>> Stashed changes`) and the branch/sha label forms while rejecting
15
+ // `>>>>>>> Stashed changes`) and the branch/sha label forms. It rejects
16
16
  // longer `<`/`>` runs.
17
17
  // - The separator (`=======`) fires ONLY while a conflict block is open in the
18
18
  // same file (block-conditioned). A lone separator with no open above it is
19
- // indistinguishable from a setext-heading underline and is a deliberate
19
+ // indistinguishable from a setext-heading underline. It is a deliberate
20
20
  // accepted non-detection.
21
- // - In a `fenceExempt` (prose) surface, occurrences inside a fenced code block
22
- // are suppressed — a fence quotes content. Markers quoted in a backtick code
23
- // SPAN are handled by the column-1 anchor itself: a span sits mid-line, so
24
- // `^` never matches. STATUS.md and non-markdown push targets pass
25
- // `fenceExempt:false`: their fenced rows are data, where a marker is never
26
- // legitimate, so fence state never suppresses.
21
+ // - In a `fenceExempt` (prose) surface, a fenced code block suppresses the
22
+ // occurrences inside it, because a fence quotes content. The column-1 anchor
23
+ // itself handles a marker quoted in a backtick code SPAN. A span sits
24
+ // mid-line, so `^` never matches. STATUS.md and non-markdown push targets
25
+ // pass `fenceExempt:false`. Their fenced rows are data, where a marker is
26
+ // never legitimate, so fence state never suppresses.
27
27
 
28
28
  const OPEN_RE = /^<{7}( |$)/;
29
29
  const CLOSE_RE = /^>{7}( |$)/;
30
30
  const SEPARATOR_RE = /^={7}\s*$/;
31
31
  // A fenced-code delimiter: three or more backticks or tildes, up to three
32
- // leading spaces of indentation (CommonMark). The info string after the run is
33
- // ignored — a delimiter line is never itself a marker.
32
+ // leading spaces of indentation (CommonMark). The scanner ignores the info
33
+ // string after the run. A delimiter line is never itself a marker.
34
34
  const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
35
35
 
36
36
  /**
@@ -50,9 +50,9 @@ export function scanConflictMarkers(text, { fenceExempt = true } = {}) {
50
50
  let insideFence = false;
51
51
  let openDepth = 0;
52
52
  for (let i = 0; i < lines.length; i++) {
53
- // Strip a trailing CR so a CRLF checkout is matched identically to LF — a
54
- // bare marker (`<<<<<<<\r`) would otherwise escape the `( |$)` anchor and
55
- // a CRLF corruption block would publish undetected.
53
+ // Strip a trailing CR so this matches a CRLF checkout and an LF checkout
54
+ // the same way. A bare marker (`<<<<<<<\r`) would otherwise escape the
55
+ // `( |$)` anchor, and a CRLF corruption block would publish undetected.
56
56
  const line = lines[i].replace(/\r$/, "");
57
57
  if (FENCE_RE.test(line)) {
58
58
  insideFence = !insideFence;
@@ -69,7 +69,7 @@ export function scanConflictMarkers(text, { fenceExempt = true } = {}) {
69
69
  }
70
70
 
71
71
  // Classify a single line as a marker kind, or null. The separator is
72
- // block-conditioned: it only counts while a conflict block is open.
72
+ // block-conditioned. It counts only while a conflict block is open.
73
73
  function classify(line, openDepth) {
74
74
  if (OPEN_RE.test(line)) return "open";
75
75
  if (CLOSE_RE.test(line)) return "close";
package/src/constants.js CHANGED
@@ -4,18 +4,19 @@ export const BROADCAST_TARGET = "all";
4
4
 
5
5
  export const MEMORY_FILE = "MEMORY.md";
6
6
 
7
- // Row-structured singleton surfaces under the sync-merge discipline: when a
7
+ // Row-structured singleton surfaces under the sync-merge discipline. When a
8
8
  // landing on one of these is contended, the resolution re-runs the row
9
- // operation against the fresh remote tip (rebase the operation, not the
10
- // lines), never a textual merge. Founding member: MEMORY.md (the Active Claims
11
- // table). STATUS.md phase rows join as their own re-apply operations land.
12
- // Metrics CSV appends take the complementary union-merge path below.
9
+ // operation against the fresh remote tip. It rebases the operation. It does
10
+ // not rebase the lines, and it never does a textual merge. Founding member:
11
+ // MEMORY.md (the Active Claims table). STATUS.md phase rows join as their own
12
+ // re-apply operations land. Metrics CSV appends take the complementary
13
+ // union-merge path below.
13
14
  export const SINGLETON_PATHS = new Set([MEMORY_FILE]);
14
15
 
15
16
  // The tracked `.gitattributes` declaration that makes concurrent appends to
16
- // metrics CSVs union-merge (keep both sides' rows) on every publish path,
17
- // instead of conflicting or side-picking. Carried by the wiki repo itself so
18
- // it governs every clone.
17
+ // metrics CSVs union-merge (keep both sides' rows) on every publish path. The
18
+ // appends then never conflict, and nothing picks one side. The wiki repo
19
+ // carries the declaration itself, so it governs every clone.
19
20
  export const GITATTRIBUTES_FILE = ".gitattributes";
20
21
  export const METRICS_CSV_MERGE_ATTRIBUTE = "metrics/**/*.csv merge=union";
21
22
  export const ACTIVE_CLAIMS_HEADING = "## Active Claims";
@@ -25,9 +26,9 @@ export const ACTIVE_CLAIMS_TABLE_SEPARATOR =
25
26
  "| --- | --- | --- | --- | --- | --- |";
26
27
 
27
28
  // Match a rendered pipe-table row (header or separator) line-anchored and
28
- // whitespace-tolerant between cells. Deriving the matcher from the rendered
29
- // literal keeps the claims parser (active-claims.js) and the audit
30
- // (audit/rules.js) from drifting on the column set — one literal, one matcher.
29
+ // whitespace-tolerant between cells. The matcher comes from the rendered
30
+ // literal, so the claims parser (active-claims.js) and the audit
31
+ // (audit/rules.js) cannot drift on the column set. One literal, one matcher.
31
32
  function pipeRowRe(literal, flags) {
32
33
  const cells = literal
33
34
  .split("|")
@@ -51,8 +52,8 @@ export const DECISION_HEADING = "### Decision";
51
52
  // Unified budgets for the audited surfaces (summary, weekly-log, storyboard,
52
53
  // memory). They keep per-surface rule pairs so the limits can diverge as the
53
54
  // context-tax model says one surface should be looser or tighter. MEMORY.md is
54
- // the tightest: it is read on every boot and holds settled cross-cutting state,
55
- // not history, so its budget is sized to keep the on-boot read cheap.
55
+ // the tightest. Every boot reads it, and it holds settled cross-cutting state
56
+ // and no history. So its budget keeps the on-boot read cheap.
56
57
  export const SUMMARY_LINE_BUDGET = 496;
57
58
  export const SUMMARY_WORD_BUDGET = 2048;
58
59
  export const WEEKLY_LOG_LINE_BUDGET = 496;
@@ -70,25 +71,27 @@ export const WEEKLY_LOG_NAME_RE = /^([a-z][a-z-]*)-(\d{4})-W(\d{2})\.md$/;
70
71
  export const WEEKLY_LOG_PART_NAME_RE =
71
72
  /^([a-z][a-z-]*)-(\d{4})-W(\d{2})-part\d+\.md$/;
72
73
 
73
- // Day-section seam: `## YYYY-MM-DD` at line start, a trailing suffix tolerated
74
- // (e.g. `## 2026-05-19 (third activation)`). One home so the rotation
75
- // seam-finder (weekly-log.js), the `log` command's last-entry probe
76
- // (commands/log.js), and the audit's heading-grammar-drift rule (audit/rules.js)
77
- // cannot disagree on what a conforming entry heading is. Source has no flags;
78
- // call sites add `g`/`m` as needed via `new RegExp(WEEKLY_LOG_SEAM_RE.source, …)`.
74
+ // Day-section seam: `## YYYY-MM-DD` at line start. The matcher tolerates a
75
+ // trailing suffix (e.g. `## 2026-05-19 (third activation)`). One home so the
76
+ // rotation seam-finder (weekly-log.js), the `log` command's last-entry probe
77
+ // (commands/log.js), and the audit's heading-grammar-drift rule
78
+ // (audit/rules.js) cannot disagree on what a conforming entry heading is. The
79
+ // source has no flags. Call sites add `g`/`m` as needed with
80
+ // `new RegExp(WEEKLY_LOG_SEAM_RE.source, …)`.
79
81
  export const WEEKLY_LOG_SEAM_RE = /^## (\d{4}-\d{2}-\d{2})/;
80
82
 
81
- // Tier-2 integrity sweep idle-gap: lane-authored commits separated by more
82
- // than this delimit sessions in the wiki history. 30 minutes.
83
+ // Idle-gap for the tier-2 integrity sweep. Lane-authored commits with a gap
84
+ // larger than this value delimit sessions in the wiki history. 30 minutes.
83
85
  export const SESSION_GAP_MS = 30 * 60 * 1000;
84
86
 
85
87
  // Carry-surface filename and H1 convention: `<agent>-carries.md` with an H1
86
- // `# <agent> — Carries`. The name capture group is the agent prefix (used by
87
- // the H1↔filename agreement rule). A Carry entry names its clearance trigger
88
- // with the `**Carry-clearance:**` marker — the existing live convention in
89
- // `wiki/release-engineer.md § Message Inbox`, preserved verbatim so the
90
- // migration relocates without re-marking. One home so the audit's classifier
91
- // (audit/scopes.js) and rules (audit/rules.js) cannot drift on the syntax.
88
+ // `# <agent> — Carries`. The name capture group is the agent prefix, which the
89
+ // H1↔filename agreement rule uses. A Carry entry names its clearance trigger
90
+ // with the `**Carry-clearance:**` marker. That is the existing live convention
91
+ // in `wiki/release-engineer.md § Message Inbox`, kept verbatim so the
92
+ // migration relocates the entry and does not re-mark it. One home so the
93
+ // audit's classifier (audit/scopes.js) and rules (audit/rules.js) cannot drift
94
+ // on the syntax.
92
95
  export const CARRY_SURFACE_NAME_RE = /^(.+)-carries\.md$/;
93
96
  export const CARRY_SURFACE_H1_RE = /^# (.+) — Carries$/;
94
97
  export const CARRY_CLEARANCE_MARKER_RE = /\*\*Carry-clearance:\*\*/;
@@ -98,8 +101,9 @@ export const CARRY_CLEARANCE_MARKER_RE = /\*\*Carry-clearance:\*\*/;
98
101
  // refresh." notice). One home so the marker scanner (marker-scanner.js) and the
99
102
  // audit's balance check (audit/rules.js) cannot drift on the syntax.
100
103
  // Capture groups: 1 metric, 2 csvPath, 3 optional prior-read anchor date. The
101
- // `prior=YYYY-MM-DD` token sits before the trailing-text group so the "Do not
102
- // edit" notice is still tolerated and does not swallow the anchor.
104
+ // `prior=YYYY-MM-DD` token sits before the trailing-text group, so the scanner
105
+ // still tolerates the "Do not edit" notice and the notice does not swallow the
106
+ // anchor.
103
107
  export const XMR_OPEN_RE =
104
108
  /^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+prior=(\d{4}-\d{2}-\d{2}))?(?:\s+[^>]*?)?\s*-->\s*$/;
105
109
  export const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
@@ -108,9 +112,9 @@ export const ISSUE_OPEN_RE =
108
112
  export const ISSUE_CLOSE_RE =
109
113
  /^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
110
114
 
111
- // Materialized per-agent experiments surface. A distinct marker
112
- // kind from `experiments:open` — it carries attributed, sanitized items plus a
113
- // last-successful-sync stamp, and is read offline by `gemba-wiki boot`. One home
115
+ // Materialized per-agent experiments surface. This is a distinct marker kind
116
+ // from `experiments:open`. It carries attributed, sanitized items plus a
117
+ // last-successful-sync stamp, and `gemba-wiki boot` reads it offline. One home
114
118
  // so the scanner (marker-scanner.js), the refresh renderer (commands/refresh.js),
115
119
  // the boot parser (boot.js), and the audit balance check (audit/rules.js) cannot
116
120
  // drift on the syntax.
@@ -121,7 +125,7 @@ export const AGENT_EXPERIMENTS_CLOSE_RE =
121
125
  export const LAST_SYNC_RE =
122
126
  /^<!--\s*last-successful-sync:\s*(\d{4}-\d{2}-\d{2})\s*-->\s*$/;
123
127
  // Attributed item line: `- #<n> [<agent>] <title> (by <author>)`. The author
124
- // suffix is mandatory and anchored at end; the title group is greedy, which is
128
+ // suffix is mandatory and anchored at end. The title group is greedy, which is
125
129
  // unambiguous because sanitizeTitle defuses any embedded ` (by ` token.
126
130
  export const AGENT_EXPERIMENT_ITEM_RE =
127
131
  /^- #(\d+) \[([a-z][a-z-]*)\] (.*) \(by (.+)\)$/;
@@ -5,18 +5,19 @@ import {
5
5
  } from "./constants.js";
6
6
 
7
7
  /**
8
- * Ensure the wiki's tracked `.gitattributes` declares the metrics-CSV union
8
+ * Make sure the wiki's tracked `.gitattributes` declares the metrics-CSV union
9
9
  * merge attribute (`metrics/**\/*.csv merge=union`), idempotently.
10
10
  *
11
- * Present-and-correct means no write: if the exact attribute line already
12
- * appears in the file, the file is left byte-unchanged and `{ changed: false }`
13
- * is returned. Otherwise the line is appended (preserving any existing
14
- * `.gitattributes` content) or the file is created with just that line, and
15
- * `{ changed: true }` is returned.
11
+ * A present and correct line means no write. If the exact attribute line
12
+ * already appears in the file, the function leaves the file byte-unchanged and
13
+ * returns `{ changed: false }`. Otherwise it appends the line and keeps any
14
+ * existing `.gitattributes` content, or it creates the file with only that
15
+ * line. It then returns `{ changed: true }`.
16
16
  *
17
17
  * The single union declaration governs every clone because it is a tracked
18
- * worktree file; per-clone config (`core.attributesFile`, `.git/info/attributes`)
19
- * would not propagate to the sibling sessions that cause the loss.
18
+ * worktree file. Per-clone config (`core.attributesFile`,
19
+ * `.git/info/attributes`) would not propagate to the sibling sessions that
20
+ * cause the loss.
20
21
  *
21
22
  * @param {string} wikiDir - The wiki clone directory.
22
23
  * @param {import('node:fs')} fsSync - Synchronous filesystem surface (`runtime.fsSync`).
@@ -30,7 +31,7 @@ export function ensureMetricsCsvMergeAttribute(wikiDir, fsSync) {
30
31
  .split("\n")
31
32
  .some((line) => line.trim() === METRICS_CSV_MERGE_ATTRIBUTE);
32
33
  if (present) return { changed: false };
33
- // Append the line, keeping existing content and ending with a newline.
34
+ // Append the line. Keep existing content and end with a newline.
34
35
  const base = text.endsWith("\n") || text === "" ? text : `${text}\n`;
35
36
  fsSync.writeFileSync(filePath, `${base}${METRICS_CSV_MERGE_ATTRIBUTE}\n`);
36
37
  return { changed: true };
package/src/integrity.js CHANGED
@@ -3,8 +3,8 @@ import { SESSION_GAP_MS } from "./constants.js";
3
3
  import { isLaneFile, enumerateLaneFiles } from "./lane-files.js";
4
4
 
5
5
  /**
6
- * Normalize a content line for content-keyed presence: strip a trailing CR and
7
- * trailing whitespace. Blank lines normalize to "" and are dropped by callers.
6
+ * Normalize a content line for content-keyed presence. Strip a trailing CR and
7
+ * trailing whitespace. A blank line normalizes to "". Callers drop it.
8
8
  * @param {string} line
9
9
  * @returns {string}
10
10
  */
@@ -13,10 +13,10 @@ export function normLine(line) {
13
13
  }
14
14
 
15
15
  /**
16
- * Parse `git diff --unified=0` text into per-file change records, attributing
16
+ * Parse `git diff --unified=0` text into per-file change records. Attribute
17
17
  * each `+`/`-` line to the file named by the most recent `+++ b/<path>` header.
18
- * The `+++`/`---`/`@@` framing lines are not content. `/dev/null` targets
19
- * (pure deletions) are kept as the home for their removed lines.
18
+ * The `+++`/`---`/`@@` framing lines are not content. The parser keeps
19
+ * `/dev/null` targets (pure deletions) as the home for their removed lines.
20
20
  * @param {string} diffText
21
21
  * @returns {Array<{home: string, added: string[], removed: string[]}>}
22
22
  */
@@ -38,7 +38,7 @@ export function parseDiff(diffText) {
38
38
  return [...byHome.values()];
39
39
  }
40
40
 
41
- /** Whether a diff line is framing (`---`, `@@`, `diff`, `index`) rather than content. */
41
+ /** Whether a diff line is framing (`---`, `@@`, `diff`, `index`) and carries no content. */
42
42
  function isDiffFraming(raw) {
43
43
  return (
44
44
  raw.startsWith("--- ") ||
@@ -48,7 +48,7 @@ function isDiffFraming(raw) {
48
48
  );
49
49
  }
50
50
 
51
- /** Strip a diff target's `a/`/`b/` prefix; `/dev/null` stays as-is. */
51
+ /** Strip a diff target's `a/`/`b/` prefix. `/dev/null` stays as-is. */
52
52
  function stripDiffTarget(target) {
53
53
  const t = target.trim();
54
54
  if (t === "/dev/null") return t;
@@ -57,10 +57,11 @@ function stripDiffTarget(target) {
57
57
 
58
58
  /**
59
59
  * Compose a window of change records (oldest→newest) into the surviving
60
- * addition assertions, then return those absent from the tip. Content-keyed on
61
- * `norm(line)`: a later own-lane deletion of an earlier-added line cancels its
62
- * assertion (additions-only, own-deletions cancel). A surviving assertion is
63
- * present iff its normalized line appears anywhere in `tipText`.
60
+ * addition assertions. Then return those absent from the tip. The composition
61
+ * is content-keyed on `norm(line)`. A later own-lane deletion of an
62
+ * earlier-added line cancels its assertion (additions-only, own-deletions
63
+ * cancel). A surviving assertion is present iff its normalized line appears
64
+ * anywhere in `tipText`.
64
65
  *
65
66
  * @param {Array<{home: string, added: string[], removed: string[]}>} changes
66
67
  * Window changes, oldest→newest.
@@ -78,7 +79,7 @@ export function findAbsent(changes, tipText, norm) {
78
79
  return absent;
79
80
  }
80
81
 
81
- /** Compose window changes into surviving additions; a later removal cancels its key. */
82
+ /** Compose window changes into surviving additions. A later removal cancels its key. */
82
83
  function composeAssertions(changes, norm) {
83
84
  const asserted = new Map(); // norm(line) -> { contentId, pushHome }
84
85
  for (const change of changes) {
@@ -108,10 +109,10 @@ function normalizedKeySet(text, norm) {
108
109
 
109
110
  /**
110
111
  * Resolve the previous-session push set from lane-authored commits (newest
111
- * first) by idle-gap. Tier 2 runs at boot before the current session has
112
- * pushed, so the most recent contiguous run is the previous session. Vacuous
113
- * only for empty history; the degenerate (content-unresolvable) case is raised
114
- * by {@link sweepTier2}, not here.
112
+ * first) by idle-gap. Tier 2 runs at boot before the current session pushes,
113
+ * so the most recent contiguous run is the previous session. The result is
114
+ * vacuous only for empty history. {@link sweepTier2} raises the degenerate
115
+ * (content-unresolvable) case. This function does not raise it.
115
116
  *
116
117
  * @param {Array<{sha: string, when: number}>} commits - Newest first.
117
118
  * @param {number} gapMs - Idle-gap threshold (ms).
@@ -121,7 +122,7 @@ export function previousSessionWindow(commits, gapMs) {
121
122
  if (commits.length === 0) return { kind: "vacuous" };
122
123
  const tipRun = [commits[0]];
123
124
  for (let i = 1; i < commits.length; i++) {
124
- // commits are newest-first; `when` is seconds, gap threshold is ms.
125
+ // commits are newest-first. `when` is seconds. The gap threshold is ms.
125
126
  const gap = (commits[i - 1].when - commits[i].when) * 1000;
126
127
  if (gap > gapMs) break;
127
128
  tipRun.push(commits[i]);
@@ -130,8 +131,8 @@ export function previousSessionWindow(commits, gapMs) {
130
131
  }
131
132
 
132
133
  /**
133
- * Build a detection record. `detectedAt` is the binding wall-clock stamp (ISO);
134
- * an exposure figure derived from commit timestamps carries the labeled
134
+ * Build a detection record. `detectedAt` is the binding wall-clock stamp
135
+ * (ISO). An exposure figure derived from commit timestamps carries the labeled
135
136
  * `commit-timestamp` fallback basis.
136
137
  *
137
138
  * @param {object} d
@@ -166,8 +167,9 @@ export function makeDetection({
166
167
 
167
168
  /**
168
169
  * Render detections to flow output text. Empty input renders the empty string
169
- * (clean-path silence). One line per detection naming tier, push-time home, and
170
- * content identity; exposure (when present) is labeled with its fallback basis.
170
+ * (clean-path silence). Each detection gets one line that names the tier, the
171
+ * push-time home, and the content identity. The line labels exposure with its
172
+ * fallback basis when exposure is present.
171
173
  * @param {object[]} detections
172
174
  * @returns {string}
173
175
  */
@@ -186,10 +188,10 @@ export function renderDetections(detections) {
186
188
  }
187
189
 
188
190
  /**
189
- * Tier-2 boot sweep: verify the lane's previous-session push set is still
190
- * content-present at the fetched (rebased) origin tip, returning detections for
191
+ * Tier-2 boot sweep. Verify the lane's previous-session push set is still
192
+ * content-present at the fetched (rebased) origin tip. Return detections for
191
193
  * any absence. Reads git and lane files from `wikiDir` (the rebased tree).
192
- * Never writes, never throws into the flow (the caller wraps it).
194
+ * Never writes. Never throws into the flow (the caller wraps it).
193
195
  *
194
196
  * @param {object} ctx
195
197
  * @param {import('@forwardimpact/libutil/runtime').Runtime} ctx.runtime
@@ -202,7 +204,7 @@ export function renderDetections(detections) {
202
204
  export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
203
205
  const email = await gitClient.configGet("user.email", { cwd: wikiDir });
204
206
  if (!email) {
205
- // Lane identity unresolvable — never a silent vacuous pass.
207
+ // Lane identity unresolvable. Never a silent vacuous pass.
206
208
  return [
207
209
  makeDetection({
208
210
  tier: 2,
@@ -221,7 +223,7 @@ export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
221
223
 
222
224
  const detections = [];
223
225
  const changes = [];
224
- // Oldest→newest so own-deletion cancellation composes in commit order.
226
+ // Oldest→newest so own-deletions cancel in commit order.
225
227
  const ordered = [...window.commits].reverse();
226
228
  for (const commit of ordered) {
227
229
  const diff = await gitClient.diffRange(`${commit.sha}~1 ${commit.sha}`, {
@@ -250,7 +252,7 @@ export async function sweepTier2({ runtime, gitClient, wikiDir, agent, now }) {
250
252
 
251
253
  for (const absent of findAbsent(changes, tipText, normLine)) {
252
254
  // Exposure runs from the LAST window commit that added this exact line
253
- // (its most recent assertion at origin), not merely a same-home commit.
255
+ // (its most recent assertion at origin). A same-home commit does not count.
254
256
  const when = lastAssertionTime(changes, absent.contentId);
255
257
  const exposureSeconds =
256
258
  when != null ? Math.round(now / 1000 - when) : undefined;
@@ -3,10 +3,11 @@ import { createLogger } from "@forwardimpact/libtelemetry";
3
3
  import { sanitizeCrossingField, sanitizeTitle } from "./sanitize.js";
4
4
 
5
5
  /**
6
- * Thrown when the tracker query for the agent-experiments materialization fails
7
- * (non-zero exit or unparseable JSON). Distinct from returning `[]` so the
8
- * refresh command can keep the previously materialized block instead of wiping
9
- * the routing surface when the tracker is briefly unavailable.
6
+ * `renderAgentExperiments` throws this when the tracker query for the
7
+ * agent-experiments materialization fails (non-zero exit or unparseable JSON).
8
+ * This error differs from a `[]` return. It lets the refresh command keep the
9
+ * previously materialized block when the tracker is briefly unavailable. The
10
+ * command does not wipe the routing surface.
10
11
  */
11
12
  export class TrackerQueryError extends Error {
12
13
  /** @param {string} reason */
@@ -18,7 +19,12 @@ export class TrackerQueryError extends Error {
18
19
 
19
20
  const AGENT_LABEL_RE = /^agent:([a-z][a-z-]*)$/;
20
21
 
21
- /** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
22
+ /**
23
+ * Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and
24
+ * proxy-rewritten URLs (e.g. `http://host/git/owner/repo`). It strips `.git`.
25
+ * Then it takes the last two path segments. Returns null when it finds nothing
26
+ * parseable.
27
+ */
22
28
  export function parseRepoSlug(originUrl) {
23
29
  if (!originUrl) return null;
24
30
  const stripped = originUrl.trim().replace(/\.git$/, "");
@@ -29,11 +35,12 @@ export function parseRepoSlug(originUrl) {
29
35
 
30
36
  /**
31
37
  * Render an issue-list block for an obstacles/experiments marker. Returns
32
- * markdown lines. `cwd` should be the parent monorepo's project root so `gh`
33
- * resolves the correct origin; `repo` is an explicit `owner/name` slug used when
34
- * the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token`
35
- * is the resolved GH token (e.g. via `Config.ghToken()`). The `gh` command runs
36
- * through `runtime.subprocess`, and stderr warnings through `runtime.proc`.
38
+ * markdown lines. Set `cwd` to the parent monorepo's project root so `gh`
39
+ * resolves the correct origin. `repo` is an explicit `owner/name` slug. Use it
40
+ * when `gh` cannot parse the origin remote (e.g. sandbox proxy URLs). `token`
41
+ * is the resolved GH token (e.g. through `Config.ghToken()`). The `gh` command
42
+ * runs through `runtime.subprocess`. Stderr warnings run through
43
+ * `runtime.proc`.
37
44
  *
38
45
  * @param {object} options
39
46
  * @param {string} options.topic
@@ -107,12 +114,13 @@ export async function renderIssueList({
107
114
  }
108
115
 
109
116
  /**
110
- * Render the attributed per-agent experiments surface. Fetches open
111
- * issues labeled `experiment`, keeps only those also carrying an
112
- * `agent:{name}` label, and emits one sanitized, body-free line per issue:
113
- * `- #<number> [<agent>] <title> (by <author>)`. Issue bodies are never read.
114
- * Throws {@link TrackerQueryError} on tracker failure so the caller can keep the
115
- * previously materialized block; never returns `[]` on failure.
117
+ * Render the attributed per-agent experiments surface. Fetches open issues
118
+ * labeled `experiment`. Keeps only the issues that also carry an
119
+ * `agent:{name}` label. Emits one sanitized, body-free line per issue:
120
+ * `- #<number> [<agent>] <title> (by <author>)`. This function never reads
121
+ * issue bodies. Throws {@link TrackerQueryError} on tracker failure so the
122
+ * caller can keep the previously materialized block. Never returns `[]` on
123
+ * failure.
116
124
  *
117
125
  * @param {object} options
118
126
  * @param {string} options.cwd