@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/audit/scopes.js
CHANGED
|
@@ -39,8 +39,8 @@ function listMdFiles(wikiRoot, fs) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Recursively collect every *.csv under `<wikiRoot>/metrics/`. The real layout
|
|
42
|
-
// is `metrics/<skill>/<year>.csv` (two levels), so the walk recurses
|
|
43
|
-
//
|
|
42
|
+
// is `metrics/<skill>/<year>.csv` (two levels), so the walk recurses and does
|
|
43
|
+
// not assume a fixed depth. The walk uses readdirSync + statSync (rather than
|
|
44
44
|
// `withFileTypes` Dirents) so it runs unchanged under the in-memory mock fs.
|
|
45
45
|
function listCsvFiles(wikiRoot, fs) {
|
|
46
46
|
const metricsRoot = path.join(wikiRoot, "metrics");
|
|
@@ -57,7 +57,7 @@ function listCsvFiles(wikiRoot, fs) {
|
|
|
57
57
|
return found;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Load a metrics CSV as an audit subject
|
|
60
|
+
// Load a metrics CSV as an audit subject. `rows` is the array of line strings,
|
|
61
61
|
// so a rule indexes `rows[i]` (a string) and `i + 1` is its line number.
|
|
62
62
|
function loadCsv(filePath, fs) {
|
|
63
63
|
return {
|
|
@@ -97,8 +97,8 @@ function loadFile(filePath, fs) {
|
|
|
97
97
|
function classifyFile(filePath, fs) {
|
|
98
98
|
const base = path.basename(filePath);
|
|
99
99
|
if (EXCLUDED_BASES.has(base)) return null;
|
|
100
|
-
// STATUS.md
|
|
101
|
-
//
|
|
100
|
+
// buildContext loads STATUS.md separately with readOptional. The dedicated
|
|
101
|
+
// `status-row` scope audits it. So skip the per-file classification.
|
|
102
102
|
if (base === "STATUS.md") return null;
|
|
103
103
|
if (NON_SUMMARY_PREFIXES.some((p) => base.startsWith(p))) return null;
|
|
104
104
|
if (WEEKLY_LOG_NAME_RE.test(base)) {
|
|
@@ -109,23 +109,23 @@ function classifyFile(filePath, fs) {
|
|
|
109
109
|
}
|
|
110
110
|
const subject = loadFile(filePath, fs);
|
|
111
111
|
// Carry surface: a `<agent>-carries.md` whose H1 matches the Carry H1 RE.
|
|
112
|
-
// Both axes must match (filename prefix and H1),
|
|
113
|
-
// classifier. The two H1 REs end in distinct literals (`— Carries`
|
|
114
|
-
// `— Summary`) so the branches cannot cross-capture regardless of order
|
|
115
|
-
//
|
|
112
|
+
// Both axes must match (filename prefix and H1), like the summary
|
|
113
|
+
// classifier. The two H1 REs end in distinct literals (`— Carries` and
|
|
114
|
+
// `— Summary`) so the branches cannot cross-capture regardless of order.
|
|
115
|
+
// A name-match with an H1-miss stays unclassified, like a malformed summary.
|
|
116
116
|
if (CARRY_SURFACE_NAME_RE.test(base)) {
|
|
117
117
|
if (CARRY_SURFACE_H1_RE.test(subject.firstLine)) {
|
|
118
118
|
return { kind: "carry-surface", subject };
|
|
119
119
|
}
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
|
-
//
|
|
123
|
-
// unclassified
|
|
122
|
+
// A file that does not match a summary or weekly-log shape stays
|
|
123
|
+
// unclassified. The audit skips stray files.
|
|
124
124
|
if (!SUMMARY_H1_RE.test(subject.firstLine)) return null;
|
|
125
125
|
return { kind: "summary", subject };
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// Read a file if present
|
|
128
|
+
// Read a file if present. An absent file yields empty text so callers audit
|
|
129
129
|
// "missing" uniformly. The common { path, text, exists } shape backs the
|
|
130
130
|
// MEMORY.md, STATUS.md, and storyboard context loads.
|
|
131
131
|
function readOptional(filePath, fs) {
|
|
@@ -139,7 +139,8 @@ function readOptional(filePath, fs) {
|
|
|
139
139
|
|
|
140
140
|
// MEMORY.md carries the same line/word budget rules as the prose surfaces, so
|
|
141
141
|
// its subject needs the `lines`/`words` counters those check builders read.
|
|
142
|
-
//
|
|
142
|
+
// The loader counts them off the canonical budget.js pair, like every other
|
|
143
|
+
// budgeted surface.
|
|
143
144
|
function loadMemory(filePath, fs) {
|
|
144
145
|
const base = readOptional(filePath, fs);
|
|
145
146
|
return {
|
|
@@ -150,11 +151,11 @@ function loadMemory(filePath, fs) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
/**
|
|
153
|
-
* Parse the rows inside STATUS.md's fenced block into audit subjects.
|
|
154
|
-
* outside the ``` fence (header prose) and blank lines
|
|
155
|
-
* carries a `kind` from {@link parseStatusRowId} (`"spec"`,
|
|
156
|
-
* `null` for an unrecognized id)
|
|
157
|
-
* `id`/`phase`/`status` fields
|
|
154
|
+
* Parse the rows inside STATUS.md's fenced block into audit subjects. The
|
|
155
|
+
* parser skips lines outside the ``` fence (header prose) and blank lines.
|
|
156
|
+
* Each row carries a `kind` from {@link parseStatusRowId} (`"spec"`,
|
|
157
|
+
* `"experiment"`, or `null` for an unrecognized id). Spec-shaped rules read
|
|
158
|
+
* the positional `id`/`phase`/`status` fields. Experiment rules read `cells`.
|
|
158
159
|
* @param {string} statusText - The full STATUS.md contents.
|
|
159
160
|
* @returns {Array<{lineNo: number, text: string, cells: string[], id: string, phase: string, status: string, kind: string|null}>}
|
|
160
161
|
*/
|
|
@@ -171,9 +172,9 @@ function parseStatusRows(statusText) {
|
|
|
171
172
|
if (!inFence || line.trim() === "") continue;
|
|
172
173
|
const cells = line.split("\t");
|
|
173
174
|
// Classify by id prefix so a malformed `exp:` row (e.g. wrong cell count)
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
175
|
+
// still routes to the experiment rules, which flag it. It does not slip
|
|
176
|
+
// through the spec-shaped rules. parseStatusRowId returns the structured
|
|
177
|
+
// fields only for a well-formed row. The rules read `cells`.
|
|
177
178
|
const isExp = typeof cells[0] === "string" && cells[0].startsWith("exp:");
|
|
178
179
|
const parsed = parseStatusRowId(cells[0], cells);
|
|
179
180
|
rows.push({
|
|
@@ -276,12 +277,12 @@ const SCOPE_RESOLVERS = {
|
|
|
276
277
|
|
|
277
278
|
// Normalize every audited surface into a uniform `{ path, text, fenceExempt }`
|
|
278
279
|
// subject for the conflict-marker scan. The per-file subjects (summaries,
|
|
279
|
-
// weekly logs and sealed parts, storyboard) carry `fileLines
|
|
280
|
+
// weekly logs and sealed parts, storyboard) carry `fileLines`. MEMORY.md and
|
|
280
281
|
// STATUS.md carry `text` (readOptional shape). `fenceExempt` is true for prose
|
|
281
|
-
// surfaces, where a fence quotes content
|
|
282
|
-
// rows are data
|
|
283
|
-
// contract).
|
|
284
|
-
// empty text and
|
|
282
|
+
// surfaces, where a fence quotes content. It is false for STATUS.md, whose
|
|
283
|
+
// fenced rows are data. A marker there is never legitimate (per-surface fence
|
|
284
|
+
// contract). A file absent from disk (no MEMORY/STATUS/storyboard) yields
|
|
285
|
+
// empty text and produces no findings.
|
|
285
286
|
function conflictScanSubjects(ctx) {
|
|
286
287
|
const subjects = [];
|
|
287
288
|
const fileScopes = ["summary", "weekly-log-main", "weekly-log-part"];
|
|
@@ -320,12 +321,12 @@ export function resolveScope(scopeKey, ctx) {
|
|
|
320
321
|
}
|
|
321
322
|
|
|
322
323
|
/**
|
|
323
|
-
* Build the admission slice
|
|
324
|
-
* `rootSummaryAgents` set that gates `<agent>/` sidecar directories. The
|
|
325
|
-
*
|
|
326
|
-
* `admission` scope can classify sidecar directories against it.
|
|
324
|
+
* Build the admission slice. It holds the tracked-file universe plus the
|
|
325
|
+
* `rootSummaryAgents` set that gates `<agent>/` sidecar directories. The
|
|
326
|
+
* function derives the agent set first (a root-level summary-class file's
|
|
327
|
+
* stem) so the `admission` scope can classify sidecar directories against it.
|
|
327
328
|
*
|
|
328
|
-
* Returns the empty universe when `subprocess` is absent
|
|
329
|
+
* Returns the empty universe when `subprocess` is absent. Callers that only
|
|
329
330
|
* read `.subjects` (the rotation pre-pass) skip the git read and the tree walk
|
|
330
331
|
* entirely, and produce no `admission` findings.
|
|
331
332
|
*/
|
|
@@ -342,9 +343,9 @@ function buildAdmission(wikiRoot, fs, subprocess) {
|
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
/**
|
|
345
|
-
* Build the audit context
|
|
346
|
+
* Build the audit context. It classifies and loads every wiki file once.
|
|
346
347
|
* @param {{wikiRoot: string, today: string, fs: object, subprocess: object}} options
|
|
347
|
-
* `fs` is the sync filesystem surface (`runtime.fsSync`)
|
|
348
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`). `subprocess` is
|
|
348
349
|
* `runtime.subprocess` (its `runSync` backs the admission scope's git read).
|
|
349
350
|
*/
|
|
350
351
|
export function buildContext({ wikiRoot, today, fs, subprocess }) {
|
package/src/audit/status-row.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { STATUS_ID_REGEX } from "../status.js";
|
|
2
2
|
|
|
3
|
-
// Validate every row inside wiki/STATUS.md's code fence.
|
|
4
|
-
//
|
|
3
|
+
// Validate every row inside wiki/STATUS.md's code fence. The `status-row`
|
|
4
|
+
// scope in scopes.js resolves the rows. Each subject carries
|
|
5
5
|
// `{ cells, id, phase, status, kind, text }`. Two row kinds share the fence:
|
|
6
6
|
//
|
|
7
7
|
// spec `{id}<TAB>{phase}<TAB>{status}` — three cells
|
|
@@ -40,7 +40,7 @@ export const STATUS_ROW_RULES = [
|
|
|
40
40
|
when: hasThreeCells,
|
|
41
41
|
check: (s) => (STATUS_ID_REGEX.test(s.id) ? null : { id: s.id }),
|
|
42
42
|
message: (_s, r) => `Bad id '${r.id}' (expected ^\\d{4}(/[a-z0-9-]+)?$)`,
|
|
43
|
-
hint: "spec ids are four digits
|
|
43
|
+
hint: "spec ids are four digits. a sub-row appends `/<unit>` (e.g. 1370/libutil)",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
id: "status-row.phase",
|
|
@@ -73,11 +73,10 @@ export const STATUS_ROW_RULES = [
|
|
|
73
73
|
hint: "each experiment row is `exp:{issue}<TAB>{state}<TAB>{pin}<TAB>{plan-ref}`",
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// STATUS_ID_REGEX / parseStatusRowId.
|
|
76
|
+
// scopes.js classifies an experiment-kind row by its `exp:` id prefix, so
|
|
77
|
+
// the spec `id-format` rule skips it. This rule enforces the `exp:\d+` id,
|
|
78
|
+
// so a non-numeric issue (e.g. `exp:abc`) flags and does not audit clean.
|
|
79
|
+
// That keeps the audit aligned with STATUS_ID_REGEX / parseStatusRowId.
|
|
81
80
|
id: "status-row.exp-id-format",
|
|
82
81
|
scope: "status-row",
|
|
83
82
|
severity: "fail",
|
|
@@ -101,10 +100,10 @@ export const STATUS_ROW_RULES = [
|
|
|
101
100
|
scope: "status-row",
|
|
102
101
|
severity: "fail",
|
|
103
102
|
when: hasFourCells,
|
|
104
|
-
// The pin is decidable per state, with no "ever approved" inference
|
|
105
|
-
// `registered` row has no pin (`-`)
|
|
106
|
-
// head
|
|
107
|
-
// not
|
|
103
|
+
// The pin is decidable per state, with no "ever approved" inference. A
|
|
104
|
+
// `registered` row has no pin (`-`). An `approved` row pins the 40-hex
|
|
105
|
+
// head. A `cancelled` row may carry the retained pin or `-`, because it
|
|
106
|
+
// may or may not pass through `approved` first. So the check accepts both.
|
|
108
107
|
check: (s) => {
|
|
109
108
|
const [, state, pin] = s.cells;
|
|
110
109
|
if (state === "registered") {
|
|
@@ -118,11 +117,11 @@ export const STATUS_ROW_RULES = [
|
|
|
118
117
|
? null
|
|
119
118
|
: { state, pin, want: "`-` or a 40-hex SHA" };
|
|
120
119
|
}
|
|
121
|
-
return null; //
|
|
120
|
+
return null; // exp-state already flags a bad state
|
|
122
121
|
},
|
|
123
122
|
message: (_s, r) =>
|
|
124
123
|
`Bad pin '${r.pin}' for state '${r.state}' (expected ${r.want})`,
|
|
125
|
-
hint: "registered pins
|
|
124
|
+
hint: "registered pins `-`. approved pins a 40-hex SHA. cancelled pins either",
|
|
126
125
|
},
|
|
127
126
|
{
|
|
128
127
|
id: "status-row.exp-planref",
|
|
@@ -131,6 +130,6 @@ export const STATUS_ROW_RULES = [
|
|
|
131
130
|
when: hasFourCells,
|
|
132
131
|
check: (s) => (/^#\d+$/.test(s.cells[3]) ? null : { planRef: s.cells[3] }),
|
|
133
132
|
message: (_s, r) => `Bad plan-ref '${r.planRef}' (expected #NNN)`,
|
|
134
|
-
hint: "the plan-ref names the issue
|
|
133
|
+
hint: "the plan-ref names the issue that carries the execution plan, e.g. #NNN",
|
|
135
134
|
},
|
|
136
135
|
];
|
package/src/block-renderer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { analyze, renderChart, MIN_POINTS } from "@forwardimpact/libxmr";
|
|
3
3
|
|
|
4
|
-
/** Error
|
|
4
|
+
/** Error the renderer throws when an XmR block has no CSV or no metric. */
|
|
5
5
|
export class BlockRenderError extends Error {
|
|
6
6
|
/** Create a BlockRenderError with the given reason string. */
|
|
7
7
|
constructor(reason) {
|
|
@@ -11,11 +11,12 @@ export class BlockRenderError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Render an XmR chart block for a metric
|
|
14
|
+
* Render an XmR chart block for a metric. Read its CSV and produce the
|
|
15
15
|
* markdown lines.
|
|
16
16
|
* @param {{metric: string, csvPath: string, projectRoot: string, fs: object, priorReadAnchor?: string|null}} options
|
|
17
|
-
* `fs` is the sync filesystem surface (`runtime.fsSync`). `priorReadAnchor
|
|
18
|
-
*
|
|
17
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`). `priorReadAnchor`
|
|
18
|
+
* stamps per-signal provenance when the caller supplies it. The Signals line
|
|
19
|
+
* surfaces that provenance.
|
|
19
20
|
*/
|
|
20
21
|
export function renderBlock({
|
|
21
22
|
metric,
|
package/src/boot.js
CHANGED
|
@@ -130,10 +130,11 @@ function bulletItem(threshold, agent) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Advance the agent-section scan for one storyboard line that is NOT inside the
|
|
133
|
-
// materialized block. Returns the next `inAgent` state
|
|
134
|
-
//
|
|
135
|
-
// scan
|
|
136
|
-
// would run past the agent sections and misattribute
|
|
133
|
+
// materialized block. Returns the next `inAgent` state. When the scan finds an
|
|
134
|
+
// h3 bullet for the agent that boots, it pushes that item. An h2 ends the
|
|
135
|
+
// agent-section scan, because team-wide sections follow the last agent h3.
|
|
136
|
+
// Without this the scan would run past the agent sections and misattribute
|
|
137
|
+
// team-wide bullets.
|
|
137
138
|
function scanAgentLine(line, agent, inAgent, items) {
|
|
138
139
|
if (/^## /.test(line)) return false;
|
|
139
140
|
const h3Match = line.match(/^### (.+)$/);
|
|
@@ -152,8 +153,9 @@ function parseStoryboardItems(text, agent) {
|
|
|
152
153
|
let inBlock = false;
|
|
153
154
|
for (const line of text.split("\n")) {
|
|
154
155
|
// The materialized block carries `- #N [agent] …` bullets that the agent
|
|
155
|
-
// scan must never capture
|
|
156
|
-
// (Without it the legacy scan double-counted these as the last agent's
|
|
156
|
+
// scan must never capture. Track it so the bullet loop skips inside it.
|
|
157
|
+
// (Without it the legacy scan double-counted these as the last agent's
|
|
158
|
+
// bullets.)
|
|
157
159
|
if (AGENT_EXPERIMENTS_OPEN_RE.test(line)) {
|
|
158
160
|
inBlock = true;
|
|
159
161
|
inAgent = false;
|
|
@@ -204,7 +206,7 @@ function countInbox(text) {
|
|
|
204
206
|
/**
|
|
205
207
|
* Remaining budget for a budgeted surface: current value, cap, and headroom for
|
|
206
208
|
* both the line and word budget. An absent file (empty text) reports zero usage
|
|
207
|
-
* and full headroom so a writer sees the ceiling before
|
|
209
|
+
* and full headroom so a writer sees the ceiling before they compose.
|
|
208
210
|
*/
|
|
209
211
|
function headroom(text, lineCap, wordCap) {
|
|
210
212
|
const lines = countLines(text);
|
|
@@ -237,7 +239,7 @@ function mapClaim(c) {
|
|
|
237
239
|
/**
|
|
238
240
|
* Build the boot digest JSON object.
|
|
239
241
|
* @param {{wikiRoot: string, agent: string, today: string, fs: object}} options
|
|
240
|
-
* `fs` is the sync filesystem surface (`runtime.fsSync`)
|
|
242
|
+
* `fs` is the sync filesystem surface (`runtime.fsSync`). `today` is an ISO
|
|
241
243
|
* date string.
|
|
242
244
|
*/
|
|
243
245
|
export function buildDigest({ wikiRoot, agent, today, fs }) {
|
package/src/budget-gate.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Post-landing, pre-push budget re-validation on the size (word/line) axis.
|
|
2
2
|
//
|
|
3
3
|
// The wiki landing flow re-runs the audit's budget predicates over the
|
|
4
|
-
// outgoing tree between landing and push
|
|
4
|
+
// outgoing tree between landing and push. It refuses a push that introduces
|
|
5
5
|
// or deepens a per-file budget breach this writer's push would publish. The
|
|
6
|
-
// gate reuses the audit's budget rules by reference
|
|
7
|
-
// objects named by `BUDGET_RULE_IDS
|
|
8
|
-
// over-cap predicate) plus the same `countWords` / `countLines` the audit
|
|
9
|
-
// builds its subjects from. It never re-defines a budget
|
|
10
|
-
// the `runRules` engine
|
|
11
|
-
// cap
|
|
6
|
+
// gate reuses the audit's budget rules by reference. It resolves the rule
|
|
7
|
+
// objects named by `BUDGET_RULE_IDS`. It then calls each rule's own `check`
|
|
8
|
+
// (the over-cap predicate) plus the same `countWords` / `countLines` the audit
|
|
9
|
+
// builds its subjects from. It never re-defines a budget. It never routes
|
|
10
|
+
// through the `runRules` engine, which drops the numeric value and emits
|
|
11
|
+
// nothing under cap. It never edits. It refuses, which keeps commits local.
|
|
12
12
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { BUDGET_RULE_IDS, RULES } from "./audit/rules.js";
|
|
@@ -16,9 +16,10 @@ import { buildContext, resolveScope } from "./audit/scopes.js";
|
|
|
16
16
|
import { countLines, countWords } from "./budget.js";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES
|
|
20
|
-
* the count axis its id implies.
|
|
21
|
-
* so a rule rename surfaces here
|
|
19
|
+
* Resolve `BUDGET_RULE_IDS` to their rule objects in `RULES`. Tag each one with
|
|
20
|
+
* the count axis its id implies. It throws if a named id is missing from
|
|
21
|
+
* `RULES`, so a rule rename surfaces here and does not silently drop a
|
|
22
|
+
* predicate.
|
|
22
23
|
* @returns {Array<{id: string, scope: string, axis: 'words'|'lines', check: Function}>}
|
|
23
24
|
*/
|
|
24
25
|
export function budgetRules() {
|
|
@@ -36,10 +37,10 @@ export function budgetRules() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* Enumerate which wiki files are budgeted
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Enumerate which wiki files are budgeted. Reuse the audit's classification.
|
|
41
|
+
* Subjects carry an absolute `path`. Reduce each one to the `<file>` half of
|
|
42
|
+
* `git show <ref>:<file>`, relative to `wikiRoot`. This function reads no count
|
|
43
|
+
* off the working-dir subject. It reads only the file identity and its scope.
|
|
43
44
|
* @param {object} ctx - An audit context from `buildContext`.
|
|
44
45
|
* @param {string} wikiRoot - The wiki clone directory the paths are relative to.
|
|
45
46
|
* @returns {Array<{relPath: string, scope: string}>}
|
|
@@ -56,12 +57,12 @@ export function budgetedFiles(ctx, wikiRoot) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Measure the budget predicates for the tree at `ref`.
|
|
60
|
-
* file's blob
|
|
61
|
-
* counters,
|
|
60
|
+
* Measure the budget predicates for the tree at `ref`. Read each budgeted
|
|
61
|
+
* file's blob through the cwd-bound `showFile`. Count it once with the audit's
|
|
62
|
+
* counters. Then, for every budget rule on that file's scope, record the axis
|
|
62
63
|
* value and whether the rule's own `check` flags it over cap. An absent path
|
|
63
|
-
* at the ref counts as 0
|
|
64
|
-
* posture
|
|
64
|
+
* at the ref counts as 0. This matches the audit's "missing counts as empty"
|
|
65
|
+
* posture. An unreadable ref makes `showFile` throw, and the throw propagates.
|
|
65
66
|
* @param {(ref: string, file: string) => Promise<string|null>} showFile
|
|
66
67
|
* @param {string} ref - The tree-ish to measure (e.g. "HEAD", a SHA).
|
|
67
68
|
* @param {Array<{relPath: string, scope: string}>} budgeted
|
|
@@ -88,15 +89,16 @@ export async function measureRef(showFile, ref, budgeted) {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
* Compare the outgoing tree against the two push-input baselines
|
|
92
|
+
* Compare the outgoing tree against the two push-input baselines. Return the
|
|
92
93
|
* per-file/per-predicate refusal delta. For each (file, rule) the baseline is
|
|
93
|
-
* the worse (higher) of the session-base and origin-tip values
|
|
94
|
-
*
|
|
95
|
-
* cap AND strictly exceeds that baseline
|
|
96
|
-
* a foreign breach the writer did not worsen passes.
|
|
97
|
-
* file listed in `exemptSummaryFiles`
|
|
98
|
-
* memo-delivery seam
|
|
99
|
-
* enforce a contradiction
|
|
94
|
+
* the worse (higher) of the session-base and origin-tip values. An absent
|
|
95
|
+
* measurement counts as 0. A predicate refuses iff the outgoing value is over
|
|
96
|
+
* cap AND strictly exceeds that baseline. So equal-or-better states pass, and
|
|
97
|
+
* a foreign breach the writer did not worsen passes. The gate surfaces a
|
|
98
|
+
* `summary.*` breach on a file listed in `exemptSummaryFiles` instead of
|
|
99
|
+
* refusing it. Those files are the memo-delivery seam. A block on a delivery
|
|
100
|
+
* into deficient headroom would enforce a contradiction. The memo-headroom
|
|
101
|
+
* measures exist to resolve that contradiction.
|
|
100
102
|
*
|
|
101
103
|
* @param {object} args
|
|
102
104
|
* @param {Map<string, Map<string, {value: number, overCap: boolean}>>} args.outgoing
|
|
@@ -137,14 +139,14 @@ export function revalidateBudgets({
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
|
-
* Run the gate end to end over the outgoing tree.
|
|
141
|
-
*
|
|
142
|
-
* and the two push-input baselines through the one `measureRef` path
|
|
143
|
-
*
|
|
144
|
-
* `showFile` throw, which aborts the gate WITHOUT refusing
|
|
145
|
-
* refuses a regression it can prove
|
|
146
|
-
* proceeds
|
|
147
|
-
* a foreign pre-existing breach.
|
|
142
|
+
* Run the gate end to end over the outgoing tree. Build the audit context.
|
|
143
|
+
* Enumerate the budgeted files. Measure the committed `HEAD` (what publishes)
|
|
144
|
+
* and the two push-input baselines through the one `measureRef` path. Then
|
|
145
|
+
* compute the per-file/per-predicate delta. An unreadable baseline ref makes
|
|
146
|
+
* `showFile` throw, which aborts the gate WITHOUT refusing. The gate only
|
|
147
|
+
* refuses a regression it can prove. So a read failure surfaces and the push
|
|
148
|
+
* proceeds. The gate does not fabricate a value-0 baseline that would wrongly
|
|
149
|
+
* block a foreign pre-existing breach.
|
|
148
150
|
*
|
|
149
151
|
* @param {object} args
|
|
150
152
|
* @param {(ref: string, file: string) => Promise<string|null>} args.showFile
|
|
@@ -181,7 +183,8 @@ export async function runBudgetGate({
|
|
|
181
183
|
}
|
|
182
184
|
originTip = await measureRef(showFile, originRef, budgeted);
|
|
183
185
|
} catch {
|
|
184
|
-
// Cannot prove a regression (unreadable ref) ⇒ do not refuse
|
|
186
|
+
// Cannot prove a regression (unreadable ref) ⇒ do not refuse. This is the
|
|
187
|
+
// fail-visible posture.
|
|
185
188
|
return { refusals: [], surfaced: [] };
|
|
186
189
|
}
|
|
187
190
|
return revalidateBudgets({
|
package/src/budget.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Canonical line- and word-counters for the budgeted wiki surfaces. The audit
|
|
2
2
|
// (`audit/scopes.js`) and the rotation primitive's bisecting seal
|
|
3
|
-
// (`weekly-log.js`) both import this one pair
|
|
4
|
-
//
|
|
3
|
+
// (`weekly-log.js`) both import this one pair. So no audit that counts
|
|
4
|
+
// differently can later flag a part the seal accepts as conforming.
|
|
5
5
|
|
|
6
|
-
/** Count lines
|
|
6
|
+
/** Count lines. Do not count a trailing newline as an empty final line. */
|
|
7
7
|
export function countLines(text) {
|
|
8
8
|
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
9
9
|
}
|
package/src/cli-definition.js
CHANGED
|
@@ -14,13 +14,13 @@ import { runFixCommand } from "./commands/fix.js";
|
|
|
14
14
|
import { runLedgerCommand } from "./commands/ledger.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Build the `gemba-wiki` libcli definition.
|
|
18
|
-
* the environment
|
|
19
|
-
* (`--from` for `memo`)
|
|
20
|
-
* ambient agent identity.
|
|
21
|
-
* the bin's `packageJsonUrl`. Each subcommand carries a `handler
|
|
22
|
-
*
|
|
23
|
-
* the per-command handler with a frozen `ctx`.
|
|
17
|
+
* Build the `gemba-wiki` libcli definition. This module never resolves agent
|
|
18
|
+
* identity from the environment. Agent-scoped subcommands require an explicit
|
|
19
|
+
* `--agent` (`--from` for `memo`). They fail closed without it, so this module
|
|
20
|
+
* carries no ambient agent identity. libcli's `createCli` resolves the version
|
|
21
|
+
* from the bin's `packageJsonUrl`. Each subcommand carries a `handler`. A
|
|
22
|
+
* command that takes subcommands also carries `args`/`argsUsage`, so
|
|
23
|
+
* `cli.dispatch` can route to the per-command handler with a frozen `ctx`.
|
|
24
24
|
*
|
|
25
25
|
* @returns {object} The libcli definition.
|
|
26
26
|
*/
|
|
@@ -48,12 +48,12 @@ export function createDefinition() {
|
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
50
|
name: "gemba-wiki",
|
|
51
|
-
description: "
|
|
51
|
+
description: "Manage the wiki lifecycle for the Kata agent system",
|
|
52
52
|
commands: [
|
|
53
53
|
{
|
|
54
54
|
name: "boot",
|
|
55
55
|
description:
|
|
56
|
-
"Print on-boot digest (priorities, claims, storyboard items) as JSON",
|
|
56
|
+
"Print the on-boot digest (priorities, claims, storyboard items) as JSON",
|
|
57
57
|
handler: runBootCommand,
|
|
58
58
|
options: {
|
|
59
59
|
...agentOpt,
|
|
@@ -101,9 +101,12 @@ export function createDefinition() {
|
|
|
101
101
|
...todayOpt,
|
|
102
102
|
target: {
|
|
103
103
|
type: "string",
|
|
104
|
-
description: "
|
|
104
|
+
description: "Target to claim (spec id, PR id, etc.)",
|
|
105
|
+
},
|
|
106
|
+
branch: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Branch that carries the work",
|
|
105
109
|
},
|
|
106
|
-
branch: { type: "string", description: "Branch carrying the work" },
|
|
107
110
|
pr: { type: "string", description: "Optional PR id" },
|
|
108
111
|
"expires-at": {
|
|
109
112
|
type: "string",
|
|
@@ -142,7 +145,7 @@ export function createDefinition() {
|
|
|
142
145
|
},
|
|
143
146
|
owner: {
|
|
144
147
|
type: "string",
|
|
145
|
-
description: "Owner field
|
|
148
|
+
description: "Owner field for promote (default: --agent)",
|
|
146
149
|
},
|
|
147
150
|
},
|
|
148
151
|
},
|
|
@@ -190,7 +193,7 @@ export function createDefinition() {
|
|
|
190
193
|
"dry-run": {
|
|
191
194
|
type: "boolean",
|
|
192
195
|
description:
|
|
193
|
-
"Print the issue body and intended action
|
|
196
|
+
"Print the issue body and intended action. Do not call gh",
|
|
194
197
|
},
|
|
195
198
|
},
|
|
196
199
|
},
|
|
@@ -217,7 +220,7 @@ export function createDefinition() {
|
|
|
217
220
|
to: {
|
|
218
221
|
type: "string",
|
|
219
222
|
description:
|
|
220
|
-
'Target agent name, or "all" to broadcast (
|
|
223
|
+
'Target agent name, or "all" to broadcast (skips the sender)',
|
|
221
224
|
},
|
|
222
225
|
message: {
|
|
223
226
|
type: "string",
|
|
@@ -287,7 +290,7 @@ export function createDefinition() {
|
|
|
287
290
|
type: "string",
|
|
288
291
|
multiple: true,
|
|
289
292
|
description:
|
|
290
|
-
"Pathspec(s)
|
|
293
|
+
"Pathspec(s) that limit the write-set. Omit to land the session's dirty set",
|
|
291
294
|
},
|
|
292
295
|
},
|
|
293
296
|
},
|
|
@@ -330,7 +333,7 @@ export function createDefinition() {
|
|
|
330
333
|
gapped: {
|
|
331
334
|
type: "boolean",
|
|
332
335
|
description:
|
|
333
|
-
"Render double-allocation losers as a gap
|
|
336
|
+
"Render double-allocation losers as a gap instead of a renumber",
|
|
334
337
|
},
|
|
335
338
|
issue: {
|
|
336
339
|
type: "string",
|
package/src/commands/audit.js
CHANGED
|
@@ -11,8 +11,8 @@ import { resolveProjectRoot } from "../util/wiki-dir.js";
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Run the wiki audit and return its findings plus the resolved project root.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* `runAuditCommand` emits the findings. `runCurateCommand` routes them to an
|
|
15
|
+
* issue. Both share this function, so the two cannot drift.
|
|
16
16
|
* @param {import("@forwardimpact/libcli").InvocationContext} ctx
|
|
17
17
|
* @returns {{ findings: object[], projectRoot: string }}
|
|
18
18
|
*/
|
|
@@ -32,7 +32,7 @@ export function auditWiki(ctx) {
|
|
|
32
32
|
return { findings: runRules(RULES, auditCtx, { resolveScope }), projectRoot };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Run the wiki audit and emit findings.
|
|
35
|
+
/** Run the wiki audit and emit findings. Use --format json for JSON. */
|
|
36
36
|
export function runAuditCommand(ctx) {
|
|
37
37
|
const { runtime } = ctx.deps;
|
|
38
38
|
const options = ctx.options;
|
package/src/commands/boot.js
CHANGED
|
@@ -40,7 +40,7 @@ function renderMarkdown(digest) {
|
|
|
40
40
|
return lines.join("\n");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Print the on-boot digest for the
|
|
43
|
+
/** Print the on-boot digest for the agent that runs it. JSON by default. --format markdown renders prose. */
|
|
44
44
|
export function runBootCommand(ctx) {
|
|
45
45
|
const { runtime } = ctx.deps;
|
|
46
46
|
const options = ctx.options;
|