@forwardimpact/libwiki 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +30 -29
  2. package/package.json +1 -1
  3. package/src/active-claims.js +5 -5
  4. package/src/agent-roster.js +2 -2
  5. package/src/audit/admission.js +14 -11
  6. package/src/audit/conflict-markers-rule.js +9 -9
  7. package/src/audit/grammar.js +21 -18
  8. package/src/audit/rule-builders.js +20 -19
  9. package/src/audit/rules.js +30 -27
  10. package/src/audit/scopes.js +34 -33
  11. package/src/audit/status-row.js +14 -15
  12. package/src/block-renderer.js +5 -4
  13. package/src/boot.js +10 -8
  14. package/src/budget-gate.js +39 -36
  15. package/src/budget.js +3 -3
  16. package/src/cli-definition.js +19 -16
  17. package/src/commands/audit.js +3 -3
  18. package/src/commands/boot.js +1 -1
  19. package/src/commands/claim.js +44 -38
  20. package/src/commands/curate.js +34 -31
  21. package/src/commands/fix.js +68 -64
  22. package/src/commands/inbox.js +1 -1
  23. package/src/commands/init.js +12 -7
  24. package/src/commands/ledger.js +11 -11
  25. package/src/commands/log.js +19 -17
  26. package/src/commands/memo.js +4 -1
  27. package/src/commands/product-mix.js +16 -15
  28. package/src/commands/refresh.js +25 -22
  29. package/src/commands/rotate.js +9 -8
  30. package/src/commands/sync.js +26 -17
  31. package/src/conflict-markers.js +21 -21
  32. package/src/constants.js +37 -33
  33. package/src/gitattributes.js +10 -9
  34. package/src/integrity.js +29 -27
  35. package/src/issue-list-renderer.js +24 -16
  36. package/src/lane-files.js +11 -10
  37. package/src/ledger/anchor.js +6 -6
  38. package/src/ledger/projection.js +35 -32
  39. package/src/ledger/reader.js +4 -4
  40. package/src/marker-scanner.js +3 -2
  41. package/src/sanitize.js +12 -11
  42. package/src/secret-gate.js +41 -40
  43. package/src/status.js +12 -11
  44. package/src/storyboard-skeleton.js +20 -18
  45. package/src/util/agent-flag.js +8 -8
  46. package/src/util/clock.js +1 -1
  47. package/src/util/wiki-dir.js +7 -7
  48. package/src/weekly-log.js +115 -101
  49. package/src/wiki-sync.js +393 -361
package/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
package/src/lane-files.js CHANGED
@@ -4,12 +4,13 @@ import { WEEKLY_LOG_NAME_RE, WEEKLY_LOG_PART_NAME_RE } from "./constants.js";
4
4
  const METRICS_CSV_RE = /^metrics\/[^/]+\/\d{4}\.csv$/;
5
5
 
6
6
  /**
7
- * Whether a wiki-root-relative path is one of the lane's own files: the
8
- * agent's summary (`<agent>.md`), a weekly log or sealed part
9
- * (`<agent>-YYYY-Www.md`, `<agent>-YYYY-Www-partN.md`, matched on the captured
10
- * agent token), or a metrics CSV (`metrics/<skill>/<year>.csv`). Metrics CSVs
11
- * match by path for every agent; lane ownership of a metrics CSV is enforced by
12
- * the tier-2 sweep's author filter at the commit level, not here.
7
+ * Whether a wiki-root-relative path is one of the lane's own files. The lane's
8
+ * own files are the agent's summary (`<agent>.md`), a weekly log or sealed
9
+ * part, and a metrics CSV (`metrics/<skill>/<year>.csv`). A weekly log or
10
+ * sealed part is `<agent>-YYYY-Www.md` or `<agent>-YYYY-Www-partN.md`, matched
11
+ * on the captured agent token. Metrics CSVs match by path for every agent. The
12
+ * tier-2 sweep's author filter enforces lane ownership of a metrics CSV at the
13
+ * commit level. This function does not.
13
14
  *
14
15
  * @param {string} relPath - Path relative to the wiki root (POSIX or native).
15
16
  * @param {string} agent - Agent profile id (e.g. "staff-engineer").
@@ -27,9 +28,9 @@ export function isLaneFile(relPath, agent) {
27
28
  }
28
29
 
29
30
  /**
30
- * Enumerate the lane's own files present under `wikiRoot`: matching top-level
31
- * summary and weekly-log files, plus every `metrics/<skill>/<year>.csv`.
32
- * Returns wiki-root-relative POSIX paths.
31
+ * Enumerate the lane's own files present under `wikiRoot`. The list holds the
32
+ * top-level summary and weekly-log files that match, plus every
33
+ * `metrics/<skill>/<year>.csv`. Returns wiki-root-relative POSIX paths.
33
34
  *
34
35
  * @param {string} wikiRoot
35
36
  * @param {string} agent
@@ -45,7 +46,7 @@ export function enumerateLaneFiles(wikiRoot, agent, fsSync) {
45
46
  return out;
46
47
  }
47
48
 
48
- /** Wiki-root-relative `metrics/<skill>/<year>.csv` paths matching the lane. */
49
+ /** Wiki-root-relative `metrics/<skill>/<year>.csv` paths that match the lane. */
49
50
  function enumerateMetricsCsvs(wikiRoot, agent, fsSync) {
50
51
  const metricsDir = path.join(wikiRoot, "metrics");
51
52
  if (!fsSync.existsSync(metricsDir)) return [];
@@ -13,11 +13,11 @@
13
13
  * ```
14
14
  * ```
15
15
  *
16
- * The block is parsed by structure, not by a general YAML engine, so libwiki
17
- * adds no parser dependency. `kind` is one of `occ`, `nm`, `fold`, `meta`;
18
- * `ids` is a list of display labels; `event` is the durable key (a SHA or a
19
- * prior anchor id); `note` is free text. The durable key is `event`; labels are
20
- * display only, so relabeling is lossless.
16
+ * The parser reads the block by structure. It does not use a general YAML
17
+ * engine, so libwiki adds no parser dependency. `kind` is one of `occ`, `nm`,
18
+ * `fold`, `meta`. `ids` is a list of display labels. `event` is the durable
19
+ * key (a SHA or a prior anchor id). `note` is free text. The durable key is
20
+ * `event`. Labels are display only, so a relabel loses nothing.
21
21
  */
22
22
 
23
23
  const FENCE_OPEN = "```yaml alloc";
@@ -59,7 +59,7 @@ export function parseAnchor(body) {
59
59
  }
60
60
 
61
61
  /**
62
- * Render the canonical anchor body for posting.
62
+ * Render the canonical anchor body to post.
63
63
  *
64
64
  * @param {{kind: string, ids: string[], event: string, note?: string}} anchor
65
65
  * @returns {string}
@@ -1,16 +1,18 @@
1
1
  /**
2
- * Fold the ordered allocation-anchor sequence into id assignments and render
3
- * the two derived projections — the ledger page body and the MEMORY
4
- * cross-cutting row. The anchor record is authoritative; these projections hold
5
- * no sole-copy state and are rebuildable from it, so erasure of a projection is
6
- * a cache miss repaired by rebuild, not a loss event.
2
+ * Fold the ordered allocation-anchor sequence into id assignments. Then render
3
+ * the two derived projections: the ledger page body and the MEMORY
4
+ * cross-cutting row. The anchor record is authoritative. These projections
5
+ * hold no sole-copy state, and a rebuild recreates them from the record. So an
6
+ * erased projection is a cache miss that a rebuild repairs. It is not a loss
7
+ * event.
7
8
  *
8
- * Identity is the `event` key; labels are display output, so a double-allocation
9
- * resolves first-published-wins and the loser is re-labeled without losing any
10
- * record. The labeling policy is a `labelMode` parameter: `renumber` (the
11
- * default, matching the team's established convention) keeps the labels dense
12
- * and re-mints the loser at the next free index; `gapped` leaves a gap so a
13
- * label never moves. Both are supported; neither is forced.
9
+ * Identity is the `event` key. Labels are display output. So a
10
+ * double-allocation resolves first-published-wins. The loser takes a new
11
+ * label, and no record is lost. The `labelMode` parameter sets the label
12
+ * policy. `renumber` is the default and matches the team's established
13
+ * convention. It keeps the labels dense and re-mints the loser at the next
14
+ * free index. `gapped` leaves a gap so a label never moves. The code supports
15
+ * both. It forces neither.
14
16
  */
15
17
 
16
18
  /**
@@ -37,7 +39,7 @@ export function foldAnchors(anchors) {
37
39
  assignments.set(label, record);
38
40
  continue;
39
41
  }
40
- // existing was published earlier (anchors are id-ordered): it wins.
42
+ // the existing record is older (anchors are id-ordered), so it wins.
41
43
  if (!contested.has(label)) contested.set(label, []);
42
44
  contested.get(label).push(record);
43
45
  }
@@ -51,13 +53,13 @@ export function foldAnchors(anchors) {
51
53
  }
52
54
 
53
55
  /**
54
- * Render the ledger page body from a fold. Entries are grouped by kind and
55
- * ordered by their winning anchor's id. Authored prose carried by
56
- * `<!-- anchor:ID -->`-cited blocks is re-emitted in anchor-id order; a cited
57
- * anchor that does not exist is reported in the returned `missingProse` list,
58
- * never silently dropped. `labelMode` selects the loser re-mint guidance for a
59
- * double-allocation: `renumber` (default) re-mints at the next free index,
60
- * `gapped` leaves the loser's index as a gap.
56
+ * Render the ledger page body from a fold. The renderer groups entries by kind
57
+ * and orders them by the id of the anchor that won. It re-emits authored prose
58
+ * from `<!-- anchor:ID -->`-cited blocks in anchor-id order. The returned
59
+ * `missingProse` list names any cited anchor that does not exist. The renderer
60
+ * never drops one silently. `labelMode` selects the re-mint guidance for the
61
+ * loser of a double-allocation. `renumber` (default) re-mints at the next free
62
+ * index. `gapped` leaves the loser's index as a gap.
61
63
  *
62
64
  * @param {{assignments: Map, conflicts: Array}} fold
63
65
  * @param {Array<{anchorId: number, text: string}>} [prose] - Anchor-cited prose blocks.
@@ -72,7 +74,7 @@ export function renderLedgerPage(
72
74
  const lines = [
73
75
  "# Parallel-Collision Ledger",
74
76
  "",
75
- "Derived projection of the allocation-anchor record. Rebuilt by `gemba-wiki ledger rebuild`; do not hand-edit identifiers here — allocate at an anchor.",
77
+ "Derived projection of the allocation-anchor record. `gemba-wiki ledger rebuild` rebuilds it. Do not hand-edit identifiers here. Allocate at an anchor.",
76
78
  "",
77
79
  ...renderKindSections(fold),
78
80
  ...renderConflicts(fold, labelMode),
@@ -123,8 +125,8 @@ function renderConflicts(fold, labelMode) {
123
125
 
124
126
  /**
125
127
  * Extract `<!-- anchor:ID -->`-cited prose blocks from an existing ledger-page
126
- * body so a rebuild re-emits them rather than dropping them. Each block runs
127
- * from its citation marker to the next marker or end of input.
128
+ * body so a rebuild re-emits them. A rebuild does not drop them. Each block
129
+ * runs from its citation marker to the next marker or end of input.
128
130
  *
129
131
  * @param {string} pageBody - The current ledger-page text.
130
132
  * @returns {Array<{anchorId: number, text: string}>}
@@ -160,8 +162,8 @@ function appendProse(lines, fold, prose) {
160
162
  }
161
163
 
162
164
  /**
163
- * Render the MEMORY cross-cutting row counters from a fold: next-free index per
164
- * kind, plus the total assigned count.
165
+ * Render the MEMORY cross-cutting row counters from a fold. The counters are
166
+ * the next-free index per kind, plus the total assigned count.
165
167
  *
166
168
  * @param {{assignments: Map}} fold
167
169
  * @returns {string}
@@ -176,7 +178,7 @@ export function renderMemoryRow(fold) {
176
178
  return (
177
179
  `Parallel-collision allocation (derived from the anchor record): ` +
178
180
  `${fold.assignments.size} ids assigned; next free #${next.occ}, NM${next.nm}, ` +
179
- `n=${next.fold}, M${next.meta}. Allocate at an anchor, never by editing this row.`
181
+ `n=${next.fold}, M${next.meta}. Allocate at an anchor. Never allocate by editing this row.`
180
182
  );
181
183
  }
182
184
 
@@ -188,11 +190,12 @@ const MEMORY_REGION_RE = new RegExp(
188
190
 
189
191
  /**
190
192
  * Write the derived MEMORY-row counters into a delimited region of a MEMORY.md
191
- * body, so the row is a rebuildable projection of the anchor record
192
- * without overwriting the surrounding authored narrative. The region is the
193
- * only sole-copy-free surface: its interior is fully regenerated, everything
194
- * outside it is preserved byte-for-byte. If the region is absent it is appended
195
- * under a heading; if present, only its interior is replaced.
193
+ * body. The row is then a rebuildable projection of the anchor record. The
194
+ * write does not overwrite the surrounding authored narrative. The region is
195
+ * the only sole-copy-free surface. This function regenerates its interior in
196
+ * full. It preserves everything outside the region byte-for-byte. If the
197
+ * region is absent, this function appends it under a heading. If the region is
198
+ * present, this function replaces only its interior.
196
199
  *
197
200
  * @param {string} memoryBody - Current `MEMORY.md` text.
198
201
  * @param {{assignments: Map}} fold
@@ -210,8 +213,8 @@ export function writeMemoryRowRegion(memoryBody, fold) {
210
213
 
211
214
  /**
212
215
  * Extract the derived MEMORY-row region interior from a MEMORY.md body, or
213
- * `null` if the region is absent. Used by `verify` to diff the projection
214
- * surface alone, never the surrounding narrative.
216
+ * `null` if the region is absent. `verify` uses this to diff the projection
217
+ * surface alone. It never diffs the surrounding narrative.
215
218
  *
216
219
  * @param {string} memoryBody
217
220
  * @returns {string|null}
@@ -2,16 +2,16 @@ import { parseAnchor } from "./anchor.js";
2
2
 
3
3
  /**
4
4
  * The obstacle issue whose comment thread is the allocation-anchor surface.
5
- * GitHub serializes comment creation and assigns a monotonic `id`, so that `id`
5
+ * GitHub serializes comment creation and assigns a monotonic `id`. So the `id`
6
6
  * order is the allocation serialization no merge can erase.
7
7
  */
8
8
  export const DEFAULT_ANCHOR_ISSUE = 1564;
9
9
 
10
10
  /**
11
11
  * Read every allocation anchor from the obstacle issue's comment thread, in
12
- * server `id` order ascending. The lowest comment `id` claiming a given label
13
- * is its winner (first published wins). Comments carrying no anchor block are
14
- * skipped.
12
+ * server `id` order ascending. The lowest comment `id` that claims a given
13
+ * label is its winner (first published wins). This function skips comments
14
+ * that carry no anchor block.
15
15
  *
16
16
  * @param {object} ghClient - A GhClient (or mock) exposing `apiGetPaginated`.
17
17
  * @param {object} opts
@@ -84,8 +84,9 @@ function matchClose(line, open) {
84
84
 
85
85
  /**
86
86
  * Scan text for paired marker blocks (xmr or issue-list). Returns positions and
87
- * metadata. Dangling open markers are reported through the injected `warn`
88
- * callback (default: discard) instead of writing to the process directly.
87
+ * metadata. The scanner reports dangling open markers through the injected
88
+ * `warn` callback (default: discard). It does not write to the process
89
+ * directly.
89
90
  * @param {string} text - The storyboard text to scan.
90
91
  * @param {{warn?: (message: string) => void}} [options]
91
92
  * @returns {Array<object>} The paired marker blocks.