@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
@@ -38,7 +38,7 @@ function nextFreeIds(fold, kind, count) {
38
38
  return ids;
39
39
  }
40
40
 
41
- /** Read the ordered anchor sequence and fold it, resolving the repo slug. */
41
+ /** Read the ordered anchor sequence, fold it, and resolve the repo slug. */
42
42
  async function loadFold({ gitClient, ghClient, wikiDir, issue }) {
43
43
  const url = await gitClient.remoteGetUrl("origin", { cwd: wikiDir });
44
44
  const { owner, repo } = parseOwnerRepo(url);
@@ -70,10 +70,10 @@ async function allocate(env, options) {
70
70
  };
71
71
  }
72
72
  const { fold, owner, repo } = await loadFold(env);
73
- // Backfill registers an anchor for ids that predate the anchor surface, named
74
- // explicitly via --ids; their event keys already exist in history. A plain
75
- // allocate mints the next free ids of the kind. The conflict detector at
76
- // rebuild guards against double-registering an id that already has an anchor.
73
+ // Backfill registers an anchor for ids that predate the anchor surface. The
74
+ // --ids flag names them, and their event keys already exist in history. A
75
+ // plain allocate mints the next free ids of the kind. The conflict detector
76
+ // at rebuild guards against an id that already has an anchor.
77
77
  let ids;
78
78
  if (options.ids) {
79
79
  ids = options.ids
@@ -93,8 +93,8 @@ async function allocate(env, options) {
93
93
  ids = nextFreeIds(fold, kind, count);
94
94
  }
95
95
  const body = renderAnchorBody({ kind, ids, event, note: options.note ?? "" });
96
- // The anchor publication is the allocation; no projection is written here.
97
- // The printed ids are provisional — a rebuild over the published sequence is
96
+ // The anchor publication is the allocation. This code writes no projection.
97
+ // The printed ids are provisional. A rebuild over the published sequence is
98
98
  // authoritative and resolves any concurrent interleave first-published-wins.
99
99
  await ghClient.apiPost(
100
100
  `repos/${owner}/${repo}/issues/${env.issue}/comments`,
@@ -118,7 +118,7 @@ function readLedgerPage(runtime, wikiDir) {
118
118
  : "";
119
119
  }
120
120
 
121
- /** Project the anchor record onto the ledger-page body, preserving cited prose. */
121
+ /** Project the anchor record onto the ledger-page body. Keep cited prose. */
122
122
  async function project(env, options) {
123
123
  const { runtime, wikiDir } = env;
124
124
  const labelMode = options.gapped ? "gapped" : "renumber";
@@ -183,9 +183,9 @@ const SUBS = { allocate, rebuild, verify };
183
183
  /**
184
184
  * `gemba-wiki ledger <allocate|rebuild|verify>` — the allocation procedure that
185
185
  * keeps identity off the merge-contested page. Allocation publishes an anchor
186
- * comment to the obstacle issue with no projection write at allocation time;
187
- * rebuild and verify project the anchor record onto the ledger page and MEMORY
188
- * row, preserving anchor-cited prose.
186
+ * comment to the obstacle issue and writes no projection at allocation time.
187
+ * Rebuild and verify project the anchor record onto the ledger page and MEMORY
188
+ * row. Both keep anchor-cited prose.
189
189
  */
190
190
  export async function runLedgerCommand(ctx) {
191
191
  const { runtime, gitClient, ghClient } = ctx.deps;
@@ -28,8 +28,8 @@ function commonContext(runtime, options) {
28
28
  }
29
29
 
30
30
  function lastDateHeading(text) {
31
- // Match `## YYYY-MM-DD` at the start of a line, optionally followed by
32
- // suffix text (e.g. `## 2026-05-19 (third activation)`).
31
+ // Match `## YYYY-MM-DD` at the start of a line. Suffix text can follow
32
+ // (e.g. `## 2026-05-19 (third activation)`).
33
33
  const re = new RegExp(WEEKLY_LOG_SEAM_RE.source, "gm");
34
34
  let last = null;
35
35
  let match;
@@ -38,12 +38,13 @@ function lastDateHeading(text) {
38
38
  }
39
39
 
40
40
  /**
41
- * Rotate before an append, never blocking it. A bisecting seal may now produce
42
- * multiple parts; the append still proceeds against the fresh current file. An
43
- * `incomplete` residue (a lone over-cap day-section sealed as its own part —
44
- * never the live file) is surfaced to stderr but does not block the append. A
45
- * thrown fs error is reported and swallowed: the writer rolled back, so the
46
- * (intact) current file still receives the new entry.
41
+ * Rotate before an append. The rotation never blocks the append. A seal that
42
+ * bisects can now produce multiple parts, and the append still proceeds
43
+ * against the fresh current file. An `incomplete` residue is a lone over-cap
44
+ * day-section sealed as its own part, never the live file. The function
45
+ * reports that residue to stderr, and it does not block the append. The
46
+ * function reports a thrown fs error and swallows it. The writer rolled back,
47
+ * so the (intact) current file still receives the new entry.
47
48
  */
48
49
  function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
49
50
  try {
@@ -60,7 +61,7 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
60
61
  createLogger("wiki", runtime).warn(
61
62
  "log",
62
63
  `day-section ${res.residue.section} alone exceeds the budget ` +
63
- `(${res.residue.lines} lines, ${res.residue.words} words); ` +
64
+ `(${res.residue.lines} lines, ${res.residue.words} words), ` +
64
65
  `sealed as ${res.residue.path} for manual recovery`,
65
66
  );
66
67
  }
@@ -70,10 +71,11 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
70
71
  }
71
72
 
72
73
  /**
73
- * Report the just-written weekly log's budget state — value, cap, and remaining
74
- * headroom for both the line and word budget — so a writer sees the ceiling
75
- * before composing the next entry rather than discovering it via a red gate.
76
- * Runs after every successful append, on the rotated and non-rotated path alike.
74
+ * Report the budget state of the weekly log just written. The report gives the
75
+ * value, the cap, and the headroom left for both the line budget and the word
76
+ * budget. A writer then sees the ceiling before the next entry, and does not
77
+ * find it at a red gate. This runs after every successful append, on the
78
+ * rotated path and the non-rotated path alike.
77
79
  */
78
80
  function reportBudget(target, runtime) {
79
81
  const text = runtime.fsSync.existsSync(target)
@@ -131,13 +133,13 @@ function runNote(runtime, options) {
131
133
  return { ok: false, code: 2 };
132
134
  }
133
135
  const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
134
- // Conservative budget: assume we'll prepend a date heading. Rotate on the
135
- // larger `withHeading` body so the word/line projection never under-counts.
136
+ // Conservative budget: assume a prepended date heading. Rotate on the larger
137
+ // `withHeading` body so the word/line projection never under-counts.
136
138
  const withHeading = `## ${today}\n\n${fieldBlock}`;
137
139
  rotateBeforeAppend(wikiRoot, agent, today, withHeading, runtime);
138
140
  const target = weeklyLogPath(wikiRoot, agent, today);
139
- // Append under the open entry if the file's last `## YYYY-MM-DD` is today;
140
- // otherwise open a new entry by prepending a date heading.
141
+ // Append under the open entry if the file's last `## YYYY-MM-DD` is today.
142
+ // If it is not, prepend a date heading to open a new entry.
141
143
  const existing = runtime.fsSync.existsSync(target)
142
144
  ? runtime.fsSync.readFileSync(target, "utf-8")
143
145
  : "";
@@ -64,7 +64,10 @@ function writeBroadcast(
64
64
  return { ok: true };
65
65
  }
66
66
 
67
- /** Write a memo to a target agent's summary file (or broadcast to all except the sender); the sender is the required --from flag. */
67
+ /**
68
+ * Write a memo to a target agent's summary file. A broadcast reaches every
69
+ * agent except the sender. The required --from flag names the sender.
70
+ */
68
71
  export function runMemoCommand(ctx) {
69
72
  const { runtime } = ctx.deps;
70
73
  const options = ctx.options;
@@ -5,10 +5,10 @@ import { parseRepoSlug } from "../issue-list-renderer.js";
5
5
  import { currentDayIso } from "../util/clock.js";
6
6
  import { resolveProjectRoot } from "../util/wiki-dir.js";
7
7
 
8
- // Resolve the monorepo's `owner/repo` slug the way `refresh.js` does: an
9
- // explicit `FIT_GH_REPO` env override (sandbox proxy URLs), else the origin
10
- // remote parsed via the injected git client. Returns null when nothing
11
- // parseable is found, in which case `gh` falls back to its own cwd resolution.
8
+ // Resolve the monorepo's `owner/repo` slug the way `refresh.js` does. An
9
+ // explicit `FIT_GH_REPO` env override wins (sandbox proxy URLs). Otherwise the
10
+ // injected git client parses the origin remote. Returns null when nothing
11
+ // parses. `gh` then falls back to its own cwd resolution.
12
12
  async function deriveRepo(gitClient, cwd, env) {
13
13
  if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
14
14
  if (!gitClient) return null;
@@ -20,8 +20,8 @@ async function deriveRepo(gitClient, cwd, env) {
20
20
  }
21
21
  }
22
22
 
23
- // A missing token is non-fatal: `gh` may still resolve ambient auth, and a
24
- // hard fetch failure downstream collapses to a logged warning and no row.
23
+ // A missing token is non-fatal. `gh` can still resolve ambient auth. A hard
24
+ // fetch failure downstream collapses to a logged warning and no row.
25
25
  async function resolveToken() {
26
26
  try {
27
27
  return (await createScriptConfig("wiki")).ghToken();
@@ -30,8 +30,8 @@ async function resolveToken() {
30
30
  }
31
31
  }
32
32
 
33
- // `gh pr list` returns at most this many PRs; a window that hits the cap is
34
- // truncated, so the caller warns rather than silently undercounting.
33
+ // `gh pr list` returns at most this many PRs. A window that hits the cap is
34
+ // truncated, so the caller warns and does not undercount in silence.
35
35
  const FETCH_LIMIT = 200;
36
36
 
37
37
  // Fetch merged PRs in `[since, until]` and return their parsed JSON, or null on
@@ -60,7 +60,7 @@ async function fetchMergedPrs({ runtime, cwd, repo, since, until, token }) {
60
60
  }
61
61
 
62
62
  // Tally merged PRs by their classification label. A PR with neither label is
63
- // unlabeled; `product` wins if both are somehow present.
63
+ // unlabeled. `product` wins if both labels are somehow present.
64
64
  function countByLabel(prs) {
65
65
  const counts = { product: 0, internal: 0, unlabeled: 0 };
66
66
  for (const pr of prs) {
@@ -75,10 +75,11 @@ function countByLabel(prs) {
75
75
  /**
76
76
  * Emit the product-vs-internal mix of merged PRs as a `product_share` metric
77
77
  * row. Counts PRs merged in `[since, until]` by their `product` / `internal`
78
- * label and appends `product_share = round(product / (product + internal) *
79
- * 100)` to `wiki/metrics/product-mix/<YYYY>.csv` via the `gemba-xmr record` write
80
- * path. Deterministic — re-running over the same merged PRs yields the same
81
- * value. A window with no labeled merged PRs emits no row (avoids a 0/0 ratio).
78
+ * label. Appends `product_share = round(product / (product + internal) * 100)`
79
+ * to `wiki/metrics/product-mix/<YYYY>.csv` through the `gemba-xmr record`
80
+ * write path. The result is deterministic. A second run over the same merged
81
+ * PRs yields the same value. A window with no labeled merged PRs emits no row
82
+ * (it avoids a 0/0 ratio).
82
83
  */
83
84
  export async function runProductMixCommand(ctx) {
84
85
  const { runtime, gitClient } = ctx.deps;
@@ -101,7 +102,7 @@ export async function runProductMixCommand(ctx) {
101
102
  if (prs.length >= FETCH_LIMIT) {
102
103
  logger.warn(
103
104
  "product-mix",
104
- `window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap; product_share may undercount`,
105
+ `window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap, so product_share may undercount`,
105
106
  );
106
107
  }
107
108
 
@@ -110,7 +111,7 @@ export async function runProductMixCommand(ctx) {
110
111
  if (total === 0) {
111
112
  logger.info(
112
113
  "product-mix",
113
- `no labeled merged PRs in ${since}..${until}; emitting no row`,
114
+ `no labeled merged PRs in ${since}..${until}, so this run emits no row`,
114
115
  );
115
116
  return { ok: true };
116
117
  }
@@ -30,11 +30,12 @@ async function deriveParentRepo(gitClient, parentDir, env) {
30
30
  }
31
31
 
32
32
  // Compose the agent-experiments block body. On a successful tracker query the
33
- // body is a fresh last-successful-sync stamp followed by freshly rendered,
34
- // label-re-checked, sanitized item lines. On a tracker failure the previously
35
- // materialized body (stamp + items) is preserved verbatim so boot keeps serving
36
- // the last good routing surface instead of an empty one, and the timestamp is
37
- // not advanced, so staleness stays auditable from the stamp.
33
+ // body is a fresh last-successful-sync stamp, then freshly rendered,
34
+ // label-re-checked, sanitized item lines. On a tracker failure the function
35
+ // keeps the body it materialized before (stamp + items) verbatim. Boot then
36
+ // still serves the last good routing surface. It does not serve an empty one.
37
+ // The function does not advance the timestamp, so the stamp keeps
38
+ // staleness auditable.
38
39
  async function renderAgentExperimentsBlock(block, lines, ghContext, runtime) {
39
40
  const priorBody = lines.slice(block.openLine + 1, block.closeLine);
40
41
  try {
@@ -91,8 +92,8 @@ function spliceBlock(lines, block, rendered) {
91
92
  );
92
93
  }
93
94
 
94
- // A missing current-month storyboard is non-fatal: return null so the caller
95
- // can create it (see createStoryboardSkeleton) rather than fail the job.
95
+ // A missing current-month storyboard is non-fatal. Return null so the caller
96
+ // can create it (see createStoryboardSkeleton) and does not fail the job.
96
97
  function readStoryboardOrNull(runtime, storyboardPath) {
97
98
  try {
98
99
  return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
@@ -104,10 +105,11 @@ function readStoryboardOrNull(runtime, storyboardPath) {
104
105
 
105
106
  // Create the current-month storyboard from the minimal skeleton when it does
106
107
  // not exist. Refresh is the deterministic "freshen the wiki" step and runs
107
- // before the session (kata-agent pre-run), so creating here guarantees the file
108
- // is on disk before participants look for it — without any lead having a write
109
- // tool. The skeleton carries the section structure and the generic issue-list
110
- // markers; the render pass below fills them and participants seed metric blocks.
108
+ // before the session (kata-agent pre-run). Creation here guarantees the file
109
+ // is on disk before participants look for it, and no lead needs a write tool.
110
+ // The skeleton carries the section structure and the generic issue-list
111
+ // markers. The render pass below fills them, and participants seed metric
112
+ // blocks.
111
113
  function createStoryboardSkeleton(runtime, storyboardPath, logger) {
112
114
  const skeleton = renderStoryboardSkeleton(currentDayIso(runtime));
113
115
  runtime.fsSync.mkdirSync(path.dirname(storyboardPath), { recursive: true });
@@ -116,12 +118,12 @@ function createStoryboardSkeleton(runtime, storyboardPath, logger) {
116
118
  return skeleton;
117
119
  }
118
120
 
119
- // Drop every MEMORY.md `## Active Claims` row past its `expires_at`, writing the
121
+ // Drop every MEMORY.md `## Active Claims` row past its `expires_at`. Write the
120
122
  // trimmed table back in place. Refresh is the deterministic "freshen the wiki"
121
- // step, so clearing lapsed claims belongs here alongside the storyboard render;
122
- // it runs whether or not the storyboard has marker blocks to regenerate. The
123
- // write is local, mirroring the storyboard splice — the caller's push publishes
124
- // it. A missing wiki or claims table is a clean no-op.
123
+ // step, so a sweep of lapsed claims belongs here beside the storyboard render.
124
+ // It runs whether or not the storyboard has marker blocks to regenerate. The
125
+ // write is local and mirrors the storyboard splice. The caller's push
126
+ // publishes it. A missing wiki or claims table is a clean no-op.
125
127
  function clearExpiredClaims(runtime, options, today, logger) {
126
128
  const memPath = path.join(resolveWikiRoot(runtime, options), "MEMORY.md");
127
129
  if (!runtime.fsSync.existsSync(memPath)) return;
@@ -148,8 +150,9 @@ export async function runRefreshCommand(ctx) {
148
150
  const logger = createLogger("wiki", runtime);
149
151
  const projectRoot = resolveProjectRoot(runtime);
150
152
 
151
- // Independent of the storyboard render below (and its early returns), so a
152
- // wiki with no storyboard or no marker blocks still gets its claims swept.
153
+ // This is independent of the storyboard render below (and its early
154
+ // returns). The sweep still runs on a wiki with no storyboard and on a wiki
155
+ // with no marker blocks.
153
156
  clearExpiredClaims(runtime, options, currentDayIso(runtime), logger);
154
157
 
155
158
  const storyboardPath = path.resolve(
@@ -171,14 +174,14 @@ export async function runRefreshCommand(ctx) {
171
174
  try {
172
175
  token = config.ghToken();
173
176
  } catch {
174
- // Missing token is non-fatal; issue-list renders will fail with a stderr
175
- // warning and the block will collapse to the notice line.
177
+ // A missing token is non-fatal. An issue-list render then fails with a
178
+ // stderr warning, and the block collapses to the notice line.
176
179
  }
177
180
  // Spawn `gh` from the project root so it resolves the monorepo's origin
178
181
  // instead of whatever git context the caller's cwd happens to be in (the
179
182
  // wiki sibling repo, a subagent worktree, a service dir, etc.). Also
180
- // resolve an explicit owner/repo slug so `gh` works when origin has been
181
- // rewritten to a proxy URL (sandbox environments) — `FIT_GH_REPO` env
183
+ // resolve an explicit owner/repo slug so `gh` works when a proxy URL
184
+ // replaced origin (sandbox environments). The `FIT_GH_REPO` env var
182
185
  // overrides the parsed origin.
183
186
  const ghContext = {
184
187
  cwd: projectRoot,
@@ -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";