@forwardimpact/libwiki 0.2.35 → 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 +53 -52
- package/package.json +6 -8
- 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 +35 -32
- package/src/audit/scopes.js +35 -34
- 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 +35 -32
- package/src/commands/audit.js +3 -3
- package/src/commands/boot.js +2 -2
- package/src/commands/claim.js +46 -40
- package/src/commands/curate.js +34 -31
- package/src/commands/fix.js +74 -70
- package/src/commands/inbox.js +2 -2
- package/src/commands/init.js +13 -8
- package/src/commands/ledger.js +12 -12
- package/src/commands/log.js +20 -18
- package/src/commands/memo.js +5 -2
- package/src/commands/product-mix.js +18 -17
- package/src/commands/refresh.js +25 -22
- package/src/commands/rotate.js +10 -9
- package/src/commands/sync.js +26 -17
- package/src/conflict-markers.js +21 -21
- package/src/constants.js +38 -34
- 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 +411 -379
- package/bin/fit-wiki.js +0 -94
package/src/commands/log.js
CHANGED
|
@@ -19,7 +19,7 @@ function commonContext(runtime, options) {
|
|
|
19
19
|
const resolved = requireAgentFlag(options, {
|
|
20
20
|
command: "log",
|
|
21
21
|
example:
|
|
22
|
-
'
|
|
22
|
+
'gemba-wiki log decision --agent staff-engineer --chosen "..." --rationale "..."',
|
|
23
23
|
});
|
|
24
24
|
if (!resolved.ok) return { error: resolved };
|
|
25
25
|
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
@@ -28,8 +28,8 @@ function commonContext(runtime, options) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function lastDateHeading(text) {
|
|
31
|
-
// Match `## YYYY-MM-DD` at the start of a line
|
|
32
|
-
//
|
|
31
|
+
// Match `## YYYY-MM-DD` at the start of a line. Suffix text can follow
|
|
32
|
+
// (e.g. `## 2026-05-19 (third activation)`).
|
|
33
33
|
const re = new RegExp(WEEKLY_LOG_SEAM_RE.source, "gm");
|
|
34
34
|
let last = null;
|
|
35
35
|
let match;
|
|
@@ -38,12 +38,13 @@ function lastDateHeading(text) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Rotate before an append
|
|
42
|
-
* multiple parts
|
|
43
|
-
* `incomplete` residue
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Rotate before an append. The rotation never blocks the append. A seal that
|
|
42
|
+
* bisects can now produce multiple parts, and the append still proceeds
|
|
43
|
+
* against the fresh current file. An `incomplete` residue is a lone over-cap
|
|
44
|
+
* day-section sealed as its own part, never the live file. The function
|
|
45
|
+
* reports that residue to stderr, and it does not block the append. The
|
|
46
|
+
* function reports a thrown fs error and swallows it. The writer rolled back,
|
|
47
|
+
* so the (intact) current file still receives the new entry.
|
|
47
48
|
*/
|
|
48
49
|
function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
49
50
|
try {
|
|
@@ -60,7 +61,7 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
|
60
61
|
createLogger("wiki", runtime).warn(
|
|
61
62
|
"log",
|
|
62
63
|
`day-section ${res.residue.section} alone exceeds the budget ` +
|
|
63
|
-
`(${res.residue.lines} lines, ${res.residue.words} words)
|
|
64
|
+
`(${res.residue.lines} lines, ${res.residue.words} words), ` +
|
|
64
65
|
`sealed as ${res.residue.path} for manual recovery`,
|
|
65
66
|
);
|
|
66
67
|
}
|
|
@@ -70,10 +71,11 @@ function rotateBeforeAppend(wikiRoot, agent, today, body, runtime) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Report the
|
|
74
|
-
* headroom for both the line
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* Report the budget state of the weekly log just written. The report gives the
|
|
75
|
+
* value, the cap, and the headroom left for both the line budget and the word
|
|
76
|
+
* budget. A writer then sees the ceiling before the next entry, and does not
|
|
77
|
+
* find it at a red gate. This runs after every successful append, on the
|
|
78
|
+
* rotated path and the non-rotated path alike.
|
|
77
79
|
*/
|
|
78
80
|
function reportBudget(target, runtime) {
|
|
79
81
|
const text = runtime.fsSync.existsSync(target)
|
|
@@ -131,13 +133,13 @@ function runNote(runtime, options) {
|
|
|
131
133
|
return { ok: false, code: 2 };
|
|
132
134
|
}
|
|
133
135
|
const fieldBlock = `### ${options.field}\n\n${options.body}\n`;
|
|
134
|
-
// Conservative budget: assume
|
|
135
|
-
//
|
|
136
|
+
// Conservative budget: assume a prepended date heading. Rotate on the larger
|
|
137
|
+
// `withHeading` body so the word/line projection never under-counts.
|
|
136
138
|
const withHeading = `## ${today}\n\n${fieldBlock}`;
|
|
137
139
|
rotateBeforeAppend(wikiRoot, agent, today, withHeading, runtime);
|
|
138
140
|
const target = weeklyLogPath(wikiRoot, agent, today);
|
|
139
|
-
// Append under the open entry if the file's last `## YYYY-MM-DD` is today
|
|
140
|
-
//
|
|
141
|
+
// Append under the open entry if the file's last `## YYYY-MM-DD` is today.
|
|
142
|
+
// If it is not, prepend a date heading to open a new entry.
|
|
141
143
|
const existing = runtime.fsSync.existsSync(target)
|
|
142
144
|
? runtime.fsSync.readFileSync(target, "utf-8")
|
|
143
145
|
: "";
|
package/src/commands/memo.js
CHANGED
|
@@ -64,7 +64,10 @@ function writeBroadcast(
|
|
|
64
64
|
return { ok: true };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Write a memo to a target agent's summary file. A broadcast reaches every
|
|
69
|
+
* agent except the sender. The required --from flag names the sender.
|
|
70
|
+
*/
|
|
68
71
|
export function runMemoCommand(ctx) {
|
|
69
72
|
const { runtime } = ctx.deps;
|
|
70
73
|
const options = ctx.options;
|
|
@@ -72,7 +75,7 @@ export function runMemoCommand(ctx) {
|
|
|
72
75
|
command: "memo",
|
|
73
76
|
flag: "--from",
|
|
74
77
|
example:
|
|
75
|
-
'
|
|
78
|
+
'gemba-wiki memo --from staff-engineer --to security-engineer --message "..."',
|
|
76
79
|
});
|
|
77
80
|
if (!resolved.ok) return resolved;
|
|
78
81
|
const sender = resolved.agent;
|
|
@@ -5,10 +5,10 @@ import { parseRepoSlug } from "../issue-list-renderer.js";
|
|
|
5
5
|
import { currentDayIso } from "../util/clock.js";
|
|
6
6
|
import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
7
7
|
|
|
8
|
-
// Resolve the monorepo's `owner/repo` slug the way `refresh.js` does
|
|
9
|
-
// explicit `FIT_GH_REPO` env override (sandbox proxy URLs)
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Resolve the monorepo's `owner/repo` slug the way `refresh.js` does. An
|
|
9
|
+
// explicit `FIT_GH_REPO` env override wins (sandbox proxy URLs). Otherwise the
|
|
10
|
+
// injected git client parses the origin remote. Returns null when nothing
|
|
11
|
+
// parses. `gh` then falls back to its own cwd resolution.
|
|
12
12
|
async function deriveRepo(gitClient, cwd, env) {
|
|
13
13
|
if (env.FIT_GH_REPO) return env.FIT_GH_REPO;
|
|
14
14
|
if (!gitClient) return null;
|
|
@@ -20,8 +20,8 @@ async function deriveRepo(gitClient, cwd, env) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// A missing token is non-fatal
|
|
24
|
-
//
|
|
23
|
+
// A missing token is non-fatal. `gh` can still resolve ambient auth. A hard
|
|
24
|
+
// fetch failure downstream collapses to a logged warning and no row.
|
|
25
25
|
async function resolveToken() {
|
|
26
26
|
try {
|
|
27
27
|
return (await createScriptConfig("wiki")).ghToken();
|
|
@@ -30,8 +30,8 @@ async function resolveToken() {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// `gh pr list` returns at most this many PRs
|
|
34
|
-
// truncated, so the caller warns
|
|
33
|
+
// `gh pr list` returns at most this many PRs. A window that hits the cap is
|
|
34
|
+
// truncated, so the caller warns and does not undercount in silence.
|
|
35
35
|
const FETCH_LIMIT = 200;
|
|
36
36
|
|
|
37
37
|
// Fetch merged PRs in `[since, until]` and return their parsed JSON, or null on
|
|
@@ -60,7 +60,7 @@ async function fetchMergedPrs({ runtime, cwd, repo, since, until, token }) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Tally merged PRs by their classification label. A PR with neither label is
|
|
63
|
-
// unlabeled
|
|
63
|
+
// unlabeled. `product` wins if both labels are somehow present.
|
|
64
64
|
function countByLabel(prs) {
|
|
65
65
|
const counts = { product: 0, internal: 0, unlabeled: 0 };
|
|
66
66
|
for (const pr of prs) {
|
|
@@ -75,10 +75,11 @@ function countByLabel(prs) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Emit the product-vs-internal mix of merged PRs as a `product_share` metric
|
|
77
77
|
* row. Counts PRs merged in `[since, until]` by their `product` / `internal`
|
|
78
|
-
* label
|
|
79
|
-
*
|
|
80
|
-
* path.
|
|
81
|
-
* value. A window with no labeled merged PRs emits no row
|
|
78
|
+
* label. Appends `product_share = round(product / (product + internal) * 100)`
|
|
79
|
+
* to `wiki/metrics/product-mix/<YYYY>.csv` through the `gemba-xmr record`
|
|
80
|
+
* write path. The result is deterministic. A second run over the same merged
|
|
81
|
+
* PRs yields the same value. A window with no labeled merged PRs emits no row
|
|
82
|
+
* (it avoids a 0/0 ratio).
|
|
82
83
|
*/
|
|
83
84
|
export async function runProductMixCommand(ctx) {
|
|
84
85
|
const { runtime, gitClient } = ctx.deps;
|
|
@@ -101,7 +102,7 @@ export async function runProductMixCommand(ctx) {
|
|
|
101
102
|
if (prs.length >= FETCH_LIMIT) {
|
|
102
103
|
logger.warn(
|
|
103
104
|
"product-mix",
|
|
104
|
-
`window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap
|
|
105
|
+
`window ${since}..${until} hit the ${FETCH_LIMIT}-PR fetch cap, so product_share may undercount`,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
108
|
|
|
@@ -110,14 +111,14 @@ export async function runProductMixCommand(ctx) {
|
|
|
110
111
|
if (total === 0) {
|
|
111
112
|
logger.info(
|
|
112
113
|
"product-mix",
|
|
113
|
-
`no labeled merged PRs in ${since}..${until}
|
|
114
|
+
`no labeled merged PRs in ${since}..${until}, so this run emits no row`,
|
|
114
115
|
);
|
|
115
116
|
return { ok: true };
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
const share = Math.round((product / total) * 100);
|
|
119
120
|
const recordArgs = [
|
|
120
|
-
"
|
|
121
|
+
"gemba-xmr",
|
|
121
122
|
"record",
|
|
122
123
|
"--skill",
|
|
123
124
|
"product-mix",
|
|
@@ -142,7 +143,7 @@ export async function runProductMixCommand(ctx) {
|
|
|
142
143
|
|
|
143
144
|
const recordResult = await runtime.subprocess.run("npx", recordArgs, { cwd });
|
|
144
145
|
if (recordResult.exitCode !== 0) {
|
|
145
|
-
logger.warn("product-mix", "
|
|
146
|
+
logger.warn("product-mix", "gemba-xmr record failed");
|
|
146
147
|
}
|
|
147
148
|
return { ok: true };
|
|
148
149
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -30,11 +30,12 @@ async function deriveParentRepo(gitClient, parentDir, env) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Compose the agent-experiments block body. On a successful tracker query the
|
|
33
|
-
// body is a fresh last-successful-sync stamp
|
|
34
|
-
// label-re-checked, sanitized item lines. On a tracker failure the
|
|
35
|
-
//
|
|
36
|
-
// the last good routing surface
|
|
37
|
-
//
|
|
33
|
+
// body is a fresh last-successful-sync stamp, then freshly rendered,
|
|
34
|
+
// label-re-checked, sanitized item lines. On a tracker failure the function
|
|
35
|
+
// keeps the body it materialized before (stamp + items) verbatim. Boot then
|
|
36
|
+
// still serves the last good routing surface. It does not serve an empty one.
|
|
37
|
+
// The function does not advance the timestamp, so the stamp keeps
|
|
38
|
+
// staleness auditable.
|
|
38
39
|
async function renderAgentExperimentsBlock(block, lines, ghContext, runtime) {
|
|
39
40
|
const priorBody = lines.slice(block.openLine + 1, block.closeLine);
|
|
40
41
|
try {
|
|
@@ -91,8 +92,8 @@ function spliceBlock(lines, block, rendered) {
|
|
|
91
92
|
);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// A missing current-month storyboard is non-fatal
|
|
95
|
-
// can create it (see createStoryboardSkeleton)
|
|
95
|
+
// A missing current-month storyboard is non-fatal. Return null so the caller
|
|
96
|
+
// can create it (see createStoryboardSkeleton) and does not fail the job.
|
|
96
97
|
function readStoryboardOrNull(runtime, storyboardPath) {
|
|
97
98
|
try {
|
|
98
99
|
return runtime.fsSync.readFileSync(storyboardPath, "utf-8");
|
|
@@ -104,10 +105,11 @@ function readStoryboardOrNull(runtime, storyboardPath) {
|
|
|
104
105
|
|
|
105
106
|
// Create the current-month storyboard from the minimal skeleton when it does
|
|
106
107
|
// not exist. Refresh is the deterministic "freshen the wiki" step and runs
|
|
107
|
-
// before the session (kata-agent pre-run)
|
|
108
|
-
// is on disk before participants look for it
|
|
109
|
-
//
|
|
110
|
-
// markers
|
|
108
|
+
// before the session (kata-agent pre-run). Creation here guarantees the file
|
|
109
|
+
// is on disk before participants look for it, and no lead needs a write tool.
|
|
110
|
+
// The skeleton carries the section structure and the generic issue-list
|
|
111
|
+
// markers. The render pass below fills them, and participants seed metric
|
|
112
|
+
// blocks.
|
|
111
113
|
function createStoryboardSkeleton(runtime, storyboardPath, logger) {
|
|
112
114
|
const skeleton = renderStoryboardSkeleton(currentDayIso(runtime));
|
|
113
115
|
runtime.fsSync.mkdirSync(path.dirname(storyboardPath), { recursive: true });
|
|
@@ -116,12 +118,12 @@ function createStoryboardSkeleton(runtime, storyboardPath, logger) {
|
|
|
116
118
|
return skeleton;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
// Drop every MEMORY.md `## Active Claims` row past its `expires_at
|
|
121
|
+
// Drop every MEMORY.md `## Active Claims` row past its `expires_at`. Write the
|
|
120
122
|
// trimmed table back in place. Refresh is the deterministic "freshen the wiki"
|
|
121
|
-
// step, so
|
|
122
|
-
//
|
|
123
|
-
// write is local
|
|
124
|
-
// it. A missing wiki or claims table is a clean no-op.
|
|
123
|
+
// step, so a sweep of lapsed claims belongs here beside the storyboard render.
|
|
124
|
+
// It runs whether or not the storyboard has marker blocks to regenerate. The
|
|
125
|
+
// write is local and mirrors the storyboard splice. The caller's push
|
|
126
|
+
// publishes it. A missing wiki or claims table is a clean no-op.
|
|
125
127
|
function clearExpiredClaims(runtime, options, today, logger) {
|
|
126
128
|
const memPath = path.join(resolveWikiRoot(runtime, options), "MEMORY.md");
|
|
127
129
|
if (!runtime.fsSync.existsSync(memPath)) return;
|
|
@@ -148,8 +150,9 @@ export async function runRefreshCommand(ctx) {
|
|
|
148
150
|
const logger = createLogger("wiki", runtime);
|
|
149
151
|
const projectRoot = resolveProjectRoot(runtime);
|
|
150
152
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
+
// This is independent of the storyboard render below (and its early
|
|
154
|
+
// returns). The sweep still runs on a wiki with no storyboard and on a wiki
|
|
155
|
+
// with no marker blocks.
|
|
153
156
|
clearExpiredClaims(runtime, options, currentDayIso(runtime), logger);
|
|
154
157
|
|
|
155
158
|
const storyboardPath = path.resolve(
|
|
@@ -171,14 +174,14 @@ export async function runRefreshCommand(ctx) {
|
|
|
171
174
|
try {
|
|
172
175
|
token = config.ghToken();
|
|
173
176
|
} catch {
|
|
174
|
-
//
|
|
175
|
-
// warning and the block
|
|
177
|
+
// A missing token is non-fatal. An issue-list render then fails with a
|
|
178
|
+
// stderr warning, and the block collapses to the notice line.
|
|
176
179
|
}
|
|
177
180
|
// Spawn `gh` from the project root so it resolves the monorepo's origin
|
|
178
181
|
// instead of whatever git context the caller's cwd happens to be in (the
|
|
179
182
|
// wiki sibling repo, a subagent worktree, a service dir, etc.). Also
|
|
180
|
-
// resolve an explicit owner/repo slug so `gh` works when
|
|
181
|
-
//
|
|
183
|
+
// resolve an explicit owner/repo slug so `gh` works when a proxy URL
|
|
184
|
+
// replaced origin (sandbox environments). The `FIT_GH_REPO` env var
|
|
182
185
|
// overrides the parsed origin.
|
|
183
186
|
const ghContext = {
|
|
184
187
|
cwd: projectRoot,
|
package/src/commands/rotate.js
CHANGED
|
@@ -6,8 +6,8 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Rotate the current weekly log to a sealed part file. Refuses an under-budget
|
|
9
|
-
* target (exit 2) unless `--force
|
|
10
|
-
* no-op even under `--force
|
|
9
|
+
* target (exit 2) unless you pass `--force`. The header-only floor stays a
|
|
10
|
+
* zero-exit no-op even under `--force`. A missing target exits 2.
|
|
11
11
|
*/
|
|
12
12
|
export function runRotateCommand(ctx) {
|
|
13
13
|
const { runtime } = ctx.deps;
|
|
@@ -15,14 +15,14 @@ export function runRotateCommand(ctx) {
|
|
|
15
15
|
const options = ctx.options;
|
|
16
16
|
const resolved = requireAgentFlag(options, {
|
|
17
17
|
command: "rotate",
|
|
18
|
-
example: "
|
|
18
|
+
example: "gemba-wiki rotate --agent staff-engineer",
|
|
19
19
|
});
|
|
20
20
|
if (!resolved.ok) return resolved;
|
|
21
21
|
const agent = resolved.agent;
|
|
22
22
|
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
23
23
|
const today = options.today || currentDayIso(runtime);
|
|
24
|
-
// Name the resolved target before any seal
|
|
25
|
-
// current week
|
|
24
|
+
// Name the resolved target before any seal. The file follows from agent plus
|
|
25
|
+
// current week. No audit finding decides it.
|
|
26
26
|
runtime.proc.stdout.write(
|
|
27
27
|
`target → ${weeklyLogPath(wikiRoot, agent, today)}\n`,
|
|
28
28
|
);
|
|
@@ -43,8 +43,8 @@ export function runRotateCommand(ctx) {
|
|
|
43
43
|
}
|
|
44
44
|
switch (result.status) {
|
|
45
45
|
case "noop":
|
|
46
|
-
// The header-only floor is a benign no-op
|
|
47
|
-
// target fails closed
|
|
46
|
+
// The header-only floor is a benign no-op. An under-budget or missing
|
|
47
|
+
// target fails closed. A stale or typo'd invocation cannot pass silently.
|
|
48
48
|
if (result.reason === "floor") {
|
|
49
49
|
runtime.proc.stdout.write(`no rotation needed for ${agent}\n`);
|
|
50
50
|
return { ok: true };
|
|
@@ -79,14 +79,15 @@ export function runRotateCommand(ctx) {
|
|
|
79
79
|
`section ${section} alone exceeds the budget ` +
|
|
80
80
|
`(${lines} lines, ${words} words) and has no finer seam to split ` +
|
|
81
81
|
`at: ${residuePath}\n` +
|
|
82
|
-
`recover it by hand
|
|
82
|
+
`recover it by hand. Shorten the section ` +
|
|
83
83
|
`(see the memory protocol's manual-recovery convention)`,
|
|
84
84
|
);
|
|
85
85
|
return { ok: false, code: 1 };
|
|
86
86
|
}
|
|
87
87
|
default:
|
|
88
88
|
// Defensive: the tagged union is exhaustive above, so this is
|
|
89
|
-
// unreachable
|
|
89
|
+
// unreachable. It stays so a future status cannot fall through to no
|
|
90
|
+
// return.
|
|
90
91
|
return { ok: true };
|
|
91
92
|
}
|
|
92
93
|
}
|
package/src/commands/sync.js
CHANGED
|
@@ -10,17 +10,17 @@ import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Commit all wiki changes and push them through the secret-gated push path.
|
|
13
|
-
* A detected secret or unavailable scanner fails the command closed
|
|
14
|
-
* the cause on stderr
|
|
15
|
-
* reports nothing to push. The post-push tier-1 integrity detections surface
|
|
16
|
-
* the output
|
|
13
|
+
* A detected secret or unavailable scanner fails the command closed. It names
|
|
14
|
+
* the cause on stderr and never tries the push. A clean write pushes, or it
|
|
15
|
+
* reports nothing to push. The post-push tier-1 integrity detections surface
|
|
16
|
+
* in the output. They never gate the push.
|
|
17
17
|
*/
|
|
18
18
|
export async function runPushCommand(ctx) {
|
|
19
19
|
const { runtime, wikiSync } = ctx.deps;
|
|
20
20
|
await wikiSync.inheritIdentity();
|
|
21
21
|
|
|
22
|
-
// A caller that knows its narrower write-set passes `--paths` (repeatable)
|
|
23
|
-
//
|
|
22
|
+
// A caller that knows its narrower write-set passes `--paths` (repeatable).
|
|
23
|
+
// The bare session-close invocation passes none. It lands the session's own
|
|
24
24
|
// dirty set under per-session checkout isolation.
|
|
25
25
|
const paths = ctx.options?.paths?.length ? ctx.options.paths : undefined;
|
|
26
26
|
|
|
@@ -28,17 +28,19 @@ export async function runPushCommand(ctx) {
|
|
|
28
28
|
try {
|
|
29
29
|
result = await wikiSync.commitAndPush("wiki: update from session", paths);
|
|
30
30
|
} catch (err) {
|
|
31
|
-
// Honest CLI contract (the honest-CLI contract): non-zero on any non-land
|
|
32
|
-
// failure, and on the ancestry guard's refusal (the ancestry guard).
|
|
33
|
-
// recipe maps this to a stop-blocking exit
|
|
31
|
+
// Honest CLI contract (the honest-CLI contract): non-zero on any non-land
|
|
32
|
+
// push failure, and on the ancestry guard's refusal (the ancestry guard).
|
|
33
|
+
// The Stop-hook recipe maps this to a stop-blocking exit. CI steps see a
|
|
34
|
+
// loud failure.
|
|
34
35
|
if (err instanceof WikiPushFailure || err instanceof AncestryRefusal) {
|
|
35
36
|
runtime.proc.stderr.write(`${err.message}\n`);
|
|
36
37
|
return { ok: false, code: 1 };
|
|
37
38
|
}
|
|
38
39
|
throw err;
|
|
39
40
|
}
|
|
40
|
-
// Fail the command closed on a secret-gate refusal (secret detected or
|
|
41
|
-
// unavailable)
|
|
41
|
+
// Fail the command closed on a secret-gate refusal (secret detected or
|
|
42
|
+
// scanner unavailable). A clean push falls through to the normal report
|
|
43
|
+
// below.
|
|
42
44
|
const refusal = refusalEnvelope(runtime, result);
|
|
43
45
|
if (refusal) return refusal;
|
|
44
46
|
if (result.landed) {
|
|
@@ -52,7 +54,13 @@ export async function runPushCommand(ctx) {
|
|
|
52
54
|
return { ok: true };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Fetch and rebase the local wiki on origin/master. On a rebase conflict,
|
|
59
|
+
* return a non-zero envelope. Its message tells the caller to resolve the
|
|
60
|
+
* conflict by hand or to push first. After a clean pull, the tier-2
|
|
61
|
+
* lane-record sweep surfaces any previous-session content absent at the
|
|
62
|
+
* fetched tip. The sweep never gates the boot.
|
|
63
|
+
*/
|
|
56
64
|
export async function runPullCommand(ctx) {
|
|
57
65
|
const { runtime, wikiSync, gitClient } = ctx.deps;
|
|
58
66
|
await wikiSync.inheritIdentity();
|
|
@@ -64,19 +72,20 @@ export async function runPullCommand(ctx) {
|
|
|
64
72
|
if (err instanceof WikiPullConflict) {
|
|
65
73
|
createLogger("wiki", runtime).error(
|
|
66
74
|
"pull",
|
|
67
|
-
"rebase conflict
|
|
75
|
+
"rebase conflict from local divergence. Resolve it by hand or push first",
|
|
68
76
|
);
|
|
69
77
|
return { ok: false, code: 1 };
|
|
70
78
|
}
|
|
71
79
|
throw err;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
// Tier-2 sweep on the just-rebased tree.
|
|
75
|
-
// to no detections
|
|
82
|
+
// Tier-2 sweep on the just-rebased tree. This detects only. Any failure
|
|
83
|
+
// degrades to no detections. It never throws into the flow. It never changes
|
|
84
|
+
// the exit code.
|
|
76
85
|
try {
|
|
77
86
|
const wikiDir = resolveWikiRoot(runtime, ctx.options);
|
|
78
|
-
// `--today` (ISO date) overrides the wall clock for deterministic tests
|
|
79
|
-
//
|
|
87
|
+
// `--today` (ISO date) overrides the wall clock for deterministic tests.
|
|
88
|
+
// A malformed value falls back to the runtime clock instead of NaN.
|
|
80
89
|
const today = ctx.options.today
|
|
81
90
|
? Date.parse(ctx.options.today)
|
|
82
91
|
: Number.NaN;
|
package/src/conflict-markers.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
// Structural detector for unresolved git conflict markers
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Structural detector for unresolved git conflict markers. The wiki audit's
|
|
2
|
+
// `conflict.markers` rule (audit/conflict-markers-rule.js) and the WikiSync
|
|
3
|
+
// pre-push guard (wiki-sync.js) both use it. One home so the two layers cannot
|
|
4
4
|
// drift on what counts as a marker.
|
|
5
5
|
//
|
|
6
|
-
// Detection is line-anchored and structural
|
|
7
|
-
// fire on markers legitimately quoted in prose:
|
|
6
|
+
// Detection is line-anchored and structural. It is not a naive grep, so it
|
|
7
|
+
// does not fire on markers legitimately quoted in prose:
|
|
8
8
|
//
|
|
9
9
|
// - Open (`<<<<<<<`) and close (`>>>>>>>`) marker lines fire UNCONDITIONALLY,
|
|
10
10
|
// per file. A seal rotation can sever one conflict block across two sealed
|
|
11
|
-
// files (the open in one, the separator + close in the other)
|
|
12
|
-
// complete
|
|
11
|
+
// files (the open in one, the separator + close in the other). A matcher for
|
|
12
|
+
// a complete in-file block would miss both, so each marker form stands
|
|
13
13
|
// alone. The single-space-or-end-of-line guard after the 7-char run admits
|
|
14
14
|
// the stash-pop label forms (`<<<<<<< Updated upstream`,
|
|
15
|
-
// `>>>>>>> Stashed changes`) and the branch/sha label forms
|
|
15
|
+
// `>>>>>>> Stashed changes`) and the branch/sha label forms. It rejects
|
|
16
16
|
// longer `<`/`>` runs.
|
|
17
17
|
// - The separator (`=======`) fires ONLY while a conflict block is open in the
|
|
18
18
|
// same file (block-conditioned). A lone separator with no open above it is
|
|
19
|
-
// indistinguishable from a setext-heading underline
|
|
19
|
+
// indistinguishable from a setext-heading underline. It is a deliberate
|
|
20
20
|
// accepted non-detection.
|
|
21
|
-
// - In a `fenceExempt` (prose) surface,
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// `^` never matches. STATUS.md and non-markdown push targets
|
|
25
|
-
// `fenceExempt:false
|
|
26
|
-
// legitimate, so fence state never suppresses.
|
|
21
|
+
// - In a `fenceExempt` (prose) surface, a fenced code block suppresses the
|
|
22
|
+
// occurrences inside it, because a fence quotes content. The column-1 anchor
|
|
23
|
+
// itself handles a marker quoted in a backtick code SPAN. A span sits
|
|
24
|
+
// mid-line, so `^` never matches. STATUS.md and non-markdown push targets
|
|
25
|
+
// pass `fenceExempt:false`. Their fenced rows are data, where a marker is
|
|
26
|
+
// never legitimate, so fence state never suppresses.
|
|
27
27
|
|
|
28
28
|
const OPEN_RE = /^<{7}( |$)/;
|
|
29
29
|
const CLOSE_RE = /^>{7}( |$)/;
|
|
30
30
|
const SEPARATOR_RE = /^={7}\s*$/;
|
|
31
31
|
// A fenced-code delimiter: three or more backticks or tildes, up to three
|
|
32
|
-
// leading spaces of indentation (CommonMark). The
|
|
33
|
-
//
|
|
32
|
+
// leading spaces of indentation (CommonMark). The scanner ignores the info
|
|
33
|
+
// string after the run. A delimiter line is never itself a marker.
|
|
34
34
|
const FENCE_RE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -50,9 +50,9 @@ export function scanConflictMarkers(text, { fenceExempt = true } = {}) {
|
|
|
50
50
|
let insideFence = false;
|
|
51
51
|
let openDepth = 0;
|
|
52
52
|
for (let i = 0; i < lines.length; i++) {
|
|
53
|
-
// Strip a trailing CR so a CRLF checkout
|
|
54
|
-
// bare marker (`<<<<<<<\r`) would otherwise escape the
|
|
55
|
-
// a CRLF corruption block would publish undetected.
|
|
53
|
+
// Strip a trailing CR so this matches a CRLF checkout and an LF checkout
|
|
54
|
+
// the same way. A bare marker (`<<<<<<<\r`) would otherwise escape the
|
|
55
|
+
// `( |$)` anchor, and a CRLF corruption block would publish undetected.
|
|
56
56
|
const line = lines[i].replace(/\r$/, "");
|
|
57
57
|
if (FENCE_RE.test(line)) {
|
|
58
58
|
insideFence = !insideFence;
|
|
@@ -69,7 +69,7 @@ export function scanConflictMarkers(text, { fenceExempt = true } = {}) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Classify a single line as a marker kind, or null. The separator is
|
|
72
|
-
// block-conditioned
|
|
72
|
+
// block-conditioned. It counts only while a conflict block is open.
|
|
73
73
|
function classify(line, openDepth) {
|
|
74
74
|
if (OPEN_RE.test(line)) return "open";
|
|
75
75
|
if (CLOSE_RE.test(line)) return "close";
|