@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.
- package/README.md +53 -33
- package/package.json +1 -1
- package/src/active-claims.js +5 -5
- package/src/agent-roster.js +2 -2
- package/src/audit/admission.js +14 -11
- package/src/audit/conflict-markers-rule.js +9 -9
- package/src/audit/grammar.js +21 -18
- package/src/audit/rule-builders.js +20 -19
- package/src/audit/rules.js +30 -27
- package/src/audit/scopes.js +34 -33
- package/src/audit/status-row.js +14 -15
- package/src/block-renderer.js +5 -4
- package/src/boot.js +10 -8
- package/src/budget-gate.js +39 -36
- package/src/budget.js +3 -3
- package/src/cli-definition.js +23 -20
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +1 -1
- package/src/commands/claim.js +44 -38
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +68 -64
- package/src/commands/inbox.js +1 -1
- package/src/commands/init.js +12 -7
- package/src/commands/ledger.js +11 -11
- package/src/commands/log.js +19 -17
- package/src/commands/memo.js +4 -1
- package/src/commands/product-mix.js +16 -15
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +9 -8
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +37 -33
- package/src/gitattributes.js +10 -9
- package/src/integrity.js +29 -27
- package/src/issue-list-renderer.js +24 -16
- package/src/lane-files.js +11 -10
- package/src/ledger/anchor.js +6 -6
- package/src/ledger/projection.js +35 -32
- package/src/ledger/reader.js +4 -4
- package/src/marker-scanner.js +3 -2
- package/src/sanitize.js +12 -11
- package/src/secret-gate.js +41 -40
- package/src/status.js +12 -11
- package/src/storyboard-skeleton.js +20 -18
- package/src/util/agent-flag.js +8 -8
- package/src/util/clock.js +1 -1
- package/src/util/wiki-dir.js +7 -7
- package/src/weekly-log.js +115 -101
- 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
|
|
8
|
-
* agent's summary (`<agent>.md`), a weekly log or sealed
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
31
|
-
* summary and weekly-log files, plus every
|
|
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
|
|
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 [];
|
package/src/ledger/anchor.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* ```
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
17
|
-
* adds no parser dependency. `kind` is one of `occ`, `nm`,
|
|
18
|
-
* `ids` is a list of display labels
|
|
19
|
-
* prior anchor id)
|
|
20
|
-
* display only, so
|
|
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
|
|
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}
|
package/src/ledger/projection.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fold the ordered allocation-anchor sequence into id assignments
|
|
3
|
-
* the two derived projections
|
|
4
|
-
* cross-cutting row. The anchor record is authoritative
|
|
5
|
-
* no sole-copy state and
|
|
6
|
-
* a cache miss
|
|
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
|
|
9
|
-
* resolves first-published-wins
|
|
10
|
-
*
|
|
11
|
-
* default
|
|
12
|
-
* and re-mints the loser at the next
|
|
13
|
-
* label never moves.
|
|
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
|
|
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.
|
|
55
|
-
*
|
|
56
|
-
* `<!-- anchor:ID -->`-cited blocks
|
|
57
|
-
* anchor that does not exist
|
|
58
|
-
* never silently
|
|
59
|
-
* double-allocation
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
192
|
-
*
|
|
193
|
-
* only sole-copy-free surface
|
|
194
|
-
* outside
|
|
195
|
-
* under a heading
|
|
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.
|
|
214
|
-
* surface alone
|
|
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}
|
package/src/ledger/reader.js
CHANGED
|
@@ -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
|
|
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`
|
|
13
|
-
* is its winner (first published wins).
|
|
14
|
-
*
|
|
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
|
package/src/marker-scanner.js
CHANGED
|
@@ -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.
|
|
88
|
-
* callback (default: discard)
|
|
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
|
|
7
|
-
// character-class range
|
|
8
|
-
// hyphenated identifiers ("staff-engineer", "dick-olsson")
|
|
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
|
|
23
|
-
* whitespace to single spaces
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* lookalikes render inert
|
|
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 "
|
|
42
|
-
* space after "(by"
|
|
43
|
-
* "(by <author>)" provenance suffix when the
|
|
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}
|
package/src/secret-gate.js
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fail-closed secret gate for the wiki push path.
|
|
3
|
-
* commit range a push introduces
|
|
4
|
-
* scanner-absent verdict. The wiki has no destination-side secret control
|
|
5
|
-
* GitHub Wiki repo runs no Actions and
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
10
|
-
* an error as clean. Findings carry only a
|
|
11
|
-
* the matched secret value, so an
|
|
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
|
|
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
|
|
22
|
+
* Scan the commit range a push introduces for secrets. Fail closed.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* `scanner-absent`.
|
|
26
|
-
* `git log` options,
|
|
27
|
-
* gitleaks' documented contract: `0` clean, `1` leaks
|
|
28
|
-
* non-zero an invocation error
|
|
29
|
-
* error
|
|
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
|
|
64
|
-
//
|
|
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.
|
|
70
|
-
* file, line, and rule of each entry
|
|
71
|
-
* record built from the result is secret-free by
|
|
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
|
|
97
|
-
* user.email`
|
|
98
|
-
*
|
|
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
|
|
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
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* result (clean
|
|
147
|
-
* through to its normal success
|
|
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
|
-
"
|
|
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
|
|
171
|
-
"
|
|
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
|
|
3
|
-
// spec (`1370/libutil`, …)
|
|
4
|
-
// sub-row reads `plan implemented`. An experiment row's id is
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// experiment PR. The `exp:` namespace
|
|
8
|
-
// anchor, so the two kinds never collide
|
|
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.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
3
|
-
* writes when the current-month board does not yet exist.
|
|
4
|
-
* structural surface libwiki owns: the five Toyota Kata
|
|
5
|
-
* generic `obstacles`/`experiments` issue-list marker blocks
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* participant seeds each missing marker pair (see the
|
|
11
|
-
* later refresh renders it. Section budgets and
|
|
12
|
-
* challenge here") stay in the skill's
|
|
13
|
-
* authoring layer
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
|
52
|
-
*
|
|
53
|
-
* `# Storyboard — {YYYY} {Month}
|
|
54
|
-
* scanner (`marker-scanner.js`) and renderer
|
|
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.
|
package/src/util/agent-flag.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve the required agent flag from frozen CLI options.
|
|
3
|
-
* filesystem and no environment, so it runs before any state
|
|
4
|
-
* `{ ok: true, agent }` when the flag is present
|
|
5
|
-
* `{ ok: false, code: 2, error }` when
|
|
6
|
-
* missing flag and shows a corrected example invocation. The error never
|
|
7
|
-
* mentions an environment variable
|
|
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
|
|
13
|
-
* (`--agent` by default, `--from` for `memo`)
|
|
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
|
-
*
|
|
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`.
|
package/src/util/wiki-dir.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Find the project root
|
|
5
|
-
* working directory
|
|
6
|
-
* Finder,
|
|
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
|
|
16
|
-
*
|
|
17
|
-
* when no explicit `--wiki-root
|
|
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
|
|
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.
|