@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
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.
package/src/sanitize.js CHANGED
@@ -3,9 +3,10 @@ const ELLIPSIS = "…";
3
3
  const ZERO_WIDTH_SPACE = "\u200b";
4
4
 
5
5
  // Replace every newline, control character, or whitespace code point with a
6
- // single space, then collapse runs. Done by code-point inspection rather than a
7
- // character-class range so no literal hyphen is ever folded into a range and
8
- // hyphenated identifiers ("staff-engineer", "dick-olsson") survive intact.
6
+ // single space. Then collapse runs. This function inspects each code point and
7
+ // does not use a character-class range. So it never folds a literal hyphen
8
+ // into a range, and hyphenated identifiers ("staff-engineer", "dick-olsson")
9
+ // survive intact.
9
10
  function flattenWhitespace(input) {
10
11
  let out = "";
11
12
  for (const ch of input) {
@@ -19,11 +20,11 @@ function flattenWhitespace(input) {
19
20
 
20
21
  /**
21
22
  * Neutralize an anyone-editable issue-tracker field before it crosses into a
22
- * boot-readable wiki surface. Flattens newlines / control characters /
23
- * whitespace to single spaces (a multi-line value is what would let a field
24
- * inject a heading or block marker and move section boundaries), escapes a
25
- * leading protocol sigil ("[" or "<") so "[ask#N]" / "<tag>" / HTML-comment
26
- * lookalikes render inert, and length-caps the result.
23
+ * boot-readable wiki surface. Flattens newlines, control characters, and
24
+ * whitespace to single spaces. A multi-line value would let a field inject a
25
+ * heading or block marker and move section boundaries. Escapes a leading
26
+ * protocol sigil ("[" or "<") so "[ask#N]", "<tag>", and HTML-comment
27
+ * lookalikes render inert. Length-caps the result.
27
28
  * @param {string|null|undefined} value
28
29
  * @param {number} [maxLen]
29
30
  * @returns {string}
@@ -38,9 +39,9 @@ export function sanitizeCrossingField(value, maxLen = FIELD_CAP) {
38
39
 
39
40
  /**
40
41
  * Sanitize a materialized item title. Beyond {@link sanitizeCrossingField}, it
41
- * defuses the literal author-suffix token " (by " by inserting a zero-width
42
- * space after "(by", so a title can never be mistaken for the trailing
43
- * "(by <author>)" provenance suffix when the line is parsed back at boot.
42
+ * defuses the literal author-suffix token " (by ". It inserts a zero-width
43
+ * space after "(by". A title then never looks like the trailing
44
+ * "(by <author>)" provenance suffix when the boot parse reads the line back.
44
45
  * @param {string|null|undefined} value
45
46
  * @param {number} [maxLen]
46
47
  * @returns {string}
@@ -1,32 +1,33 @@
1
1
  /**
2
- * Fail-closed secret gate for the wiki push path. Runs gitleaks over the
3
- * commit range a push introduces and reports a clean / finding /
4
- * scanner-absent verdict. The wiki has no destination-side secret control (a
5
- * GitHub Wiki repo runs no Actions and is excluded from GitHub
6
- * secret-scanning), so this is the only place a content backstop can live.
2
+ * Fail-closed secret gate for the wiki push path. The gate runs gitleaks over
3
+ * the commit range a push introduces. It reports a clean, finding, or
4
+ * scanner-absent verdict. The wiki has no destination-side secret control. A
5
+ * GitHub Wiki repo runs no Actions, and GitHub secret-scanning excludes it. So
6
+ * this is the only place a content backstop can live.
7
7
  *
8
- * The module never throws on a scanner result: a missing or erroring scanner
9
- * resolves to `scanner-absent` so the caller fails closed rather than treating
10
- * an error as clean. Findings carry only a location (`file:line:rule`) — never
11
- * the matched secret value, so an audit record built from them cannot itself
12
- * leak.
8
+ * The module never throws on a scanner result. A missing scanner resolves to
9
+ * `scanner-absent`. A scanner that errors resolves the same way. The caller
10
+ * then fails closed and never reports an error as clean. Findings carry only a
11
+ * location (`file:line:rule`). They never carry the matched secret value, so an
12
+ * audit record built from them cannot itself leak.
13
13
  */
14
14
 
15
15
  import { isoTimestamp } from "@forwardimpact/libutil";
16
16
  import { createLogger } from "@forwardimpact/libtelemetry";
17
17
 
18
- /** The gitleaks binary name resolved on PATH; provisioning is an operator concern (see wiki-operations guide). */
18
+ /** The gitleaks binary name resolved on PATH. An operator provisions it (see the wiki-operations guide). */
19
19
  const GITLEAKS = "gitleaks";
20
20
 
21
21
  /**
22
- * Scan the commit range a push introduces for secrets, fail closed.
22
+ * Scan the commit range a push introduces for secrets. Fail closed.
23
23
  *
24
- * Probes `gitleaks version` first; an unresolvable binary short-circuits to
25
- * `scanner-absent`. Then runs `gitleaks detect` over `range` expressed as
26
- * `git log` options, reading the JSON report from stdout. Exit codes follow
27
- * gitleaks' documented contract: `0` clean, `1` leaks found, any other
28
- * non-zero an invocation error (treated as `scanner-absent` — fail closed, an
29
- * error is never reported as clean).
24
+ * The scan probes `gitleaks version` first. An unresolvable binary
25
+ * short-circuits to `scanner-absent`. The scan then runs `gitleaks detect` over
26
+ * `range` expressed as `git log` options, and reads the JSON report from
27
+ * stdout. Exit codes follow gitleaks' documented contract: `0` clean, `1` leaks
28
+ * found, any other non-zero an invocation error. The scan treats an invocation
29
+ * error as `scanner-absent` and fails closed. It never reports an error as
30
+ * clean.
30
31
  *
31
32
  * @param {object} args
32
33
  * @param {import('@forwardimpact/libutil/runtime').Runtime} args.runtime - Provides `subprocess.run`.
@@ -60,16 +61,16 @@ export async function scanPushWindow({ runtime, wikiDir, range }) {
60
61
  if (scan.exitCode === 1) {
61
62
  return { status: "finding", findings: parseFindings(scan.stdout) };
62
63
  }
63
- // Any other non-zero is an invocation/usage error, not a leak verdict:
64
- // fail closed rather than risk reporting a broken scan as clean.
64
+ // Any other non-zero is an invocation or usage error. It is not a leak
65
+ // verdict. Fail closed. Do not report a broken scan as clean.
65
66
  return { status: "scanner-absent" };
66
67
  }
67
68
 
68
69
  /**
69
- * Parse a gitleaks JSON report into location-only findings. Reads only the
70
- * file, line, and rule of each entry — never the matched secret value — so a
71
- * record built from the result is secret-free by construction. A malformed or
72
- * empty report yields an empty list.
70
+ * Parse a gitleaks JSON report into location-only findings. The parser reads
71
+ * only the file, line, and rule of each entry. It never reads the matched
72
+ * secret value, so a record built from the result is secret-free by
73
+ * construction. A malformed or empty report yields an empty list.
73
74
  *
74
75
  * @param {string} stdout - The gitleaks JSON report.
75
76
  * @returns {Array<{file: string, line: number, rule: string}>}
@@ -93,10 +94,10 @@ function parseFindings(stdout) {
93
94
  * Append one secret-free line to the wiki tree's `secret-overrides.log` and
94
95
  * stage it (path-scoped) so it lands in the same push as the overridden
95
96
  * content. The line records the override as a durable, inspectable audit
96
- * trail: an ISO timestamp, the asserted operator identity (`git config
97
- * user.email` — attribution of intent, NOT an authenticated identity), the
98
- * override class, the reason, and for a finding its location. It never carries
99
- * a matched secret value.
97
+ * trail. It holds an ISO timestamp, the asserted operator identity (`git
98
+ * config user.email`), the override class, the reason, and for a finding its
99
+ * location. That identity asserts intent. It is NOT an authenticated identity.
100
+ * The line never carries a matched secret value.
100
101
  *
101
102
  * @param {object} args
102
103
  * @param {import('@forwardimpact/libutil/runtime').Runtime} args.runtime - Provides `fs` and `clock`.
@@ -123,8 +124,8 @@ export async function appendOverrideRecord({
123
124
  "unspecified"
124
125
  : "scanner-absent";
125
126
  const ts = isoTimestamp(runtime.clock.now());
126
- // Tab-separated, single line; the reason is collapsed so the record stays
127
- // one inspectable row per override.
127
+ // Tab-separated, single line. The replace call collapses the reason so the
128
+ // record stays one inspectable row per override.
128
129
  const line = `${ts}\t${email}\t${klass}\t${reason.replace(/\s+/g, " ").trim()}\t${where}\n`;
129
130
  const logPath = `${wikiDir}/${OVERRIDE_LOG}`;
130
131
  await runtime.fs.appendFile(logPath, line);
@@ -140,12 +141,12 @@ export async function appendOverrideRecord({
140
141
  export const OVERRIDE_LOG = "secret-overrides.log";
141
142
 
142
143
  /**
143
- * Translate a `commitAndPush` security refusal into a command envelope,
144
- * logging the cause and its break-glass procedure at error level (always
145
- * surfaced, regardless of LOG_LEVEL). Returns `null` for any non-refusal
146
- * result (clean / pushed / network "saved locally"), so a caller can fall
147
- * through to its normal success handling. Shared by every command surface so
148
- * the refusal message and exit code live in one place.
144
+ * Translate a `commitAndPush` security refusal into a command envelope. Log the
145
+ * cause and its break-glass procedure at error level. The logger always
146
+ * surfaces them, regardless of LOG_LEVEL. Returns `null` for any
147
+ * non-refusal result (clean, pushed, or network "saved locally"), so a caller
148
+ * can fall through to its normal success path. Every command surface shares
149
+ * this function, so the refusal message and exit code live in one place.
149
150
  *
150
151
  * @param {object} runtime - The runtime bag (the logger writes to `proc.stderr`).
151
152
  * @param {{reason?: string, findings?: Array<{file: string, line: number, rule: string}>}} result - A `commitAndPush` result.
@@ -158,8 +159,8 @@ export function refusalEnvelope(runtime, result) {
158
159
  .join(", ");
159
160
  createLogger("wiki", runtime).error(
160
161
  "push",
161
- `push blocked: secret detected in wiki content${where ? ` (${where})` : ""}; ` +
162
- "the push was not attempted. After confirming a false positive, set " +
162
+ `push blocked: secret detected in wiki content${where ? ` (${where})` : ""}. ` +
163
+ "The push did not run. Confirm a false positive first. Then set " +
163
164
  "FIT_WIKI_SECRET_OVERRIDE to a reason to override (audited).",
164
165
  );
165
166
  return { ok: false, code: 1 };
@@ -167,8 +168,8 @@ export function refusalEnvelope(runtime, result) {
167
168
  if (result.reason === "scanner-unavailable") {
168
169
  createLogger("wiki", runtime).error(
169
170
  "push",
170
- "push blocked: the secret scanner (gitleaks) is unavailable; the push " +
171
- "was not attempted. Install gitleaks, or set FIT_WIKI_SCANNER_ABSENT_OK " +
171
+ "push blocked: the secret scanner (gitleaks) is unavailable. The push " +
172
+ "did not run. Install gitleaks, or set FIT_WIKI_SCANNER_ABSENT_OK " +
172
173
  "to a reason to override (audited).",
173
174
  );
174
175
  return { ok: false, code: 1 };
package/src/status.js CHANGED
@@ -1,20 +1,21 @@
1
1
  // STATUS.md rows come in two kinds. A spec row's id is four digits with an
2
- // optional `/<unit>` suffix denoting a per-migration-unit sub-row of a master
3
- // spec (`1370/libutil`, …); the master `NNNN` row advances only when every
4
- // sub-row reads `plan implemented`. An experiment row's id is `exp:<issue>`
5
- // and the row carries four tab cells — `exp:<issue><TAB><state><TAB><pin>
6
- // <TAB><plan-ref>` — keying the merge-gate approval path for a spec-less
7
- // experiment PR. The `exp:` namespace cannot match the spec id's `^\d{4}`
8
- // anchor, so the two kinds never collide for any issue-number width.
2
+ // optional `/<unit>` suffix. The suffix denotes a per-migration-unit sub-row
3
+ // of a master spec (`1370/libutil`, …). The master `NNNN` row advances only
4
+ // when every sub-row reads `plan implemented`. An experiment row's id is
5
+ // `exp:<issue>`. That row carries four tab cells:
6
+ // `exp:<issue><TAB><state><TAB><pin><TAB><plan-ref>`. The cells key the
7
+ // merge-gate approval path for a spec-less experiment PR. The `exp:` namespace
8
+ // cannot match the spec id's `^\d{4}` anchor, so the two kinds never collide
9
+ // for any issue-number width.
9
10
 
10
11
  /** Matches a status-row id: a four-digit spec id (optional `/<unit>`) or `exp:<issue>`. */
11
12
  export const STATUS_ID_REGEX = /^(\d{4}(\/[a-z0-9-]+)?|exp:\d+)$/;
12
13
 
13
14
  /**
14
- * Classify a status-row id into its kind and parts. Experiment rows are
15
- * identified by an `exp:` id together with a four-cell row; the optional
16
- * `cells` array supplies that count (a bare `exp:` id without four cells is
17
- * not a valid row and yields null).
15
+ * Classify a status-row id into its kind and parts. An `exp:` id together with
16
+ * a four-cell row identifies an experiment row. The optional `cells` array
17
+ * supplies that count. A bare `exp:` id without four cells is not a valid row
18
+ * and yields null.
18
19
  * @param {string} id - The id field (cell 0) of a STATUS.md row.
19
20
  * @param {string[]} [cells] - The full tab-separated cells of the row, when
20
21
  * available. Required to classify an experiment row.
@@ -1,16 +1,17 @@
1
1
  /**
2
- * Storyboard skeleton — the minimal, valid storyboard file `gemba-wiki refresh`
3
- * writes when the current-month board does not yet exist. It carries only the
4
- * structural surface libwiki owns: the five Toyota Kata sections and the
5
- * generic `obstacles`/`experiments` issue-list marker blocks that refresh
6
- * renders from tracker state.
2
+ * The storyboard skeleton is the minimal, valid storyboard file that
3
+ * `gemba-wiki refresh` writes when the current-month board does not yet exist.
4
+ * It carries only the structural surface libwiki owns: the five Toyota Kata
5
+ * sections and the generic `obstacles`/`experiments` issue-list marker blocks.
6
+ * Refresh renders those blocks from tracker state.
7
7
  *
8
- * Deliberately *not* here: the per-agent `#### {metric}` XmR blocks. Their
9
- * agent→metric grouping is per-installation curation libwiki cannot infer, so a
10
- * participant seeds each missing marker pair (see the kata-session skill) and a
11
- * later refresh renders it. Section budgets and authoring prose ("write the
12
- * challenge here") stay in the skill's `storyboard-template.md`, the L4
13
- * authoring layer — this skeleton is content-free scaffolding.
8
+ * This skeleton deliberately omits the per-agent `#### {metric}` XmR blocks.
9
+ * Each installation curates which metric belongs to which agent, so libwiki
10
+ * cannot infer the pairs. A participant seeds each missing marker pair (see the
11
+ * kata-session skill). A later refresh renders it. Section budgets and prose
12
+ * for authors ("write the challenge here") stay in the skill's
13
+ * `storyboard-template.md`, the L4 authoring layer. The skeleton itself carries
14
+ * no content.
14
15
  */
15
16
 
16
17
  const MONTH_NAMES = [
@@ -36,9 +37,9 @@ function isLeapYear(year) {
36
37
  }
37
38
 
38
39
  /**
39
- * Last calendar day of the month containing `todayIso` (ISO `YYYY-MM-DD`).
40
- * Pure integer/calendar math — no `Date`, so the module stays free of ambient
41
- * time deps.
40
+ * Return the last calendar day of the month that holds `todayIso` (ISO
41
+ * `YYYY-MM-DD`). The function uses pure integer and calendar math. It uses no
42
+ * `Date`, so the module stays free of ambient time deps.
42
43
  */
43
44
  function endOfMonthIso(todayIso) {
44
45
  const [year, month] = todayIso.split("-").map(Number);
@@ -48,10 +49,11 @@ function endOfMonthIso(todayIso) {
48
49
  }
49
50
 
50
51
  /**
51
- * Render the minimal storyboard skeleton for the month containing `todayIso`.
52
- * Pure — takes the day as an ISO string and returns markdown. The heading reads
53
- * `# Storyboard — {YYYY} {Month}`; the marker blocks match the syntax the
54
- * scanner (`marker-scanner.js`) and renderer (`commands/refresh.js`) expect.
52
+ * Render the minimal storyboard skeleton for the month that holds `todayIso`.
53
+ * The function is pure. It takes the day as an ISO string and returns
54
+ * markdown. The heading reads `# Storyboard — {YYYY} {Month}`. The marker
55
+ * blocks match the syntax the scanner (`marker-scanner.js`) and renderer
56
+ * (`commands/refresh.js`) expect.
55
57
  *
56
58
  * @param {string} todayIso - ISO date string (`YYYY-MM-DD`).
57
59
  * @returns {string} The skeleton markdown, newline-terminated.
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Resolve the required agent flag from frozen CLI options. Pure — reads no
3
- * filesystem and no environment, so it runs before any state change. Returns
4
- * `{ ok: true, agent }` when the flag is present, or
5
- * `{ ok: false, code: 2, error }` when it is missing, where `error` names the
6
- * missing flag and shows a corrected example invocation. The error never
7
- * mentions an environment variable: `libwiki` carries no ambient agent
2
+ * Resolve the required agent flag from frozen CLI options. The function is
3
+ * pure. It reads no filesystem and no environment, so it runs before any state
4
+ * change. It returns `{ ok: true, agent }` when the flag is present. It returns
5
+ * `{ ok: false, code: 2, error }` when the flag is missing. The `error` names
6
+ * the missing flag and shows a corrected example invocation. The error never
7
+ * mentions an environment variable. `libwiki` carries no ambient agent
8
8
  * identity, so there is no fallback to offer.
9
9
  *
10
10
  * @param {Record<string, unknown>} options - The frozen `ctx.options`.
11
11
  * @param {{ command: string, flag?: string, example: string }} spec
12
- * `command` names the failing subcommand; `flag` is the option key prefix
13
- * (`--agent` by default, `--from` for `memo`); `example` is a correct
12
+ * `command` names the subcommand that failed. `flag` is the option key prefix
13
+ * (`--agent` by default, `--from` for `memo`). `example` is a correct
14
14
  * invocation shown verbatim in the error.
15
15
  * @returns {{ ok: true, agent: string } | { ok: false, code: 2, error: string }}
16
16
  */
package/src/util/clock.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isoDate } from "@forwardimpact/libutil";
2
2
 
3
3
  /**
4
- * Today's ISO calendar date (`YYYY-MM-DD`) read from the injected clock.
4
+ * Return today's ISO calendar date (`YYYY-MM-DD`) from the injected clock.
5
5
  * Commands that previously inlined `new Date().toISOString().slice(0, 10)` (or
6
6
  * libwiki's `io.today()`) call this instead so the wall-clock read flows
7
7
  * through `runtime.clock`.
@@ -1,9 +1,9 @@
1
1
  import path from "node:path";
2
2
 
3
3
  /**
4
- * Find the project root by upward `package.json` discovery from the current
5
- * working directory, using the injected `runtime.finder` (the one canonical
6
- * Finder, constructed only inside libutil).
4
+ * Find the project root. The search looks upward for a `package.json` from the
5
+ * current working directory. It uses the injected `runtime.finder` (the one
6
+ * canonical Finder, which libutil alone constructs).
7
7
  * @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
8
8
  * @returns {string}
9
9
  */
@@ -12,9 +12,9 @@ export function resolveProjectRoot(runtime) {
12
12
  }
13
13
 
14
14
  /**
15
- * Resolve the wiki root, preserving the pre-1370 order: the `--wiki-root`
16
- * option when given, else `<projectRoot>/wiki`. The finder is consulted only
17
- * when no explicit `--wiki-root` is supplied.
15
+ * Resolve the wiki root and keep the pre-1370 order: the `--wiki-root` option
16
+ * when the caller gives one, else `<projectRoot>/wiki`. The function consults
17
+ * the finder only when the caller supplies no explicit `--wiki-root`.
18
18
  * @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
19
19
  * @param {Record<string, unknown>} [options] - Parsed CLI options (`ctx.options`).
20
20
  * @returns {string}
@@ -26,7 +26,7 @@ export function resolveWikiRoot(runtime, options = {}) {
26
26
  /**
27
27
  * Report whether the resolved wiki root exists on disk. Commands that read or
28
28
  * sync an existing wiki use this to degrade gracefully (warn and exit 0) when
29
- * the tree was never bootstrapped — e.g. a fresh worktree where
29
+ * nobody bootstrapped the tree. One example is a fresh worktree where
30
30
  * `scripts/bootstrap.sh` did not run.
31
31
  * @param {import('@forwardimpact/libutil/runtime').Runtime} runtime
32
32
  * @param {string} wikiDir - The resolved wiki root.