@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.
- package/README.md +30 -29
- 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 +19 -16
- 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/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.
|