@forwardimpact/libwiki 0.2.25 → 0.2.26
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/LICENSE +21 -201
- package/README.md +5 -3
- package/bin/fit-wiki.js +1 -1
- package/package.json +1 -1
- package/src/audit/admission.js +53 -0
- package/src/audit/conflict-markers-rule.js +24 -0
- package/src/audit/grammar.js +97 -0
- package/src/audit/rules.js +96 -5
- package/src/audit/scopes.js +108 -3
- package/src/block-renderer.js +19 -5
- package/src/boot.js +39 -1
- package/src/cli-definition.js +17 -16
- package/src/commands/audit.js +6 -1
- package/src/commands/boot.js +7 -2
- package/src/commands/claim.js +138 -32
- package/src/commands/fix.js +13 -5
- package/src/commands/inbox.js +7 -8
- package/src/commands/init.js +6 -0
- package/src/commands/log.js +47 -25
- package/src/commands/memo.js +10 -9
- package/src/commands/refresh.js +1 -0
- package/src/commands/rotate.js +40 -19
- package/src/commands/sync.js +54 -6
- package/src/conflict-markers.js +78 -0
- package/src/constants.js +31 -1
- package/src/gitattributes.js +40 -0
- package/src/index.js +2 -1
- package/src/integrity.js +288 -0
- package/src/lane-files.js +62 -0
- package/src/marker-scanner.js +2 -0
- package/src/secret-gate.js +177 -0
- package/src/util/agent-flag.js +31 -0
- package/src/weekly-log.js +146 -37
- package/src/wiki-sync.js +528 -11
package/src/weekly-log.js
CHANGED
|
@@ -4,9 +4,14 @@ import { countLines, countWords } from "./budget.js";
|
|
|
4
4
|
import {
|
|
5
5
|
WEEKLY_LOG_LINE_BUDGET,
|
|
6
6
|
WEEKLY_LOG_PART_NAME_RE,
|
|
7
|
+
WEEKLY_LOG_SEAM_RE,
|
|
7
8
|
WEEKLY_LOG_WORD_BUDGET,
|
|
8
9
|
} from "./constants.js";
|
|
9
10
|
|
|
11
|
+
// Block seam inside a day-section: a `### ` heading at line start. The finer
|
|
12
|
+
// grain rotation falls to when a lone day-section alone exceeds a budget.
|
|
13
|
+
const BLOCK_SEAM_RE = /^### /;
|
|
14
|
+
|
|
10
15
|
// ISO week computation lives in libutil's calendar util (the one place a
|
|
11
16
|
// `new Date` is allowed); re-exported here for the existing public surface.
|
|
12
17
|
export { isoWeek } from "@forwardimpact/libutil";
|
|
@@ -48,10 +53,11 @@ function defaultH1(agent, isoWeekStr) {
|
|
|
48
53
|
return `# ${agentTitle(agent)} — ${isoWeekStr}\n`;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
/** Describe a lone over-cap
|
|
56
|
+
/** Describe a lone over-cap section as a residue at its part index. The
|
|
57
|
+
* section label is a day date (`## ` seam) or a block heading (`### ` seam). */
|
|
52
58
|
function residueOf(sec, partIndex, measure) {
|
|
53
59
|
const { lines, words } = measure(sec.text);
|
|
54
|
-
return { section: sec.
|
|
60
|
+
return { section: sec.label, lines, words, partIndex };
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/**
|
|
@@ -93,7 +99,7 @@ function packSections(sections, prologue, budget) {
|
|
|
93
99
|
};
|
|
94
100
|
for (const sec of sections) {
|
|
95
101
|
if (overBudget(sec.text)) {
|
|
96
|
-
// Irreducible lone
|
|
102
|
+
// Irreducible lone section: flush the open part, then seal it alone.
|
|
97
103
|
flush();
|
|
98
104
|
residue ??= residueOf(sec, partBodies.length, measure);
|
|
99
105
|
partBodies.push(sec.text);
|
|
@@ -111,6 +117,37 @@ function packSections(sections, prologue, budget) {
|
|
|
111
117
|
return { partBodies, residue };
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Slice `body` at `seamRe` seam offsets into `{label, text}` sections, the
|
|
122
|
+
* label being the seam's full matched heading line (date or `### ` heading).
|
|
123
|
+
* Returns `{prologue, sections}` where the prologue is everything above the
|
|
124
|
+
* first seam (the whole body when there are no seams). Concatenating the
|
|
125
|
+
* prologue and every section's text reproduces `body` byte-for-byte.
|
|
126
|
+
*/
|
|
127
|
+
function findSections(body, seamRe) {
|
|
128
|
+
const re = new RegExp(seamRe.source, "gm");
|
|
129
|
+
const seams = [];
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = re.exec(body)) !== null) {
|
|
132
|
+
const eol = body.indexOf("\n", match.index);
|
|
133
|
+
const headingLine = body.slice(match.index, eol === -1 ? body.length : eol);
|
|
134
|
+
// A captured group (the day seam's date) labels the section; otherwise the
|
|
135
|
+
// full heading line (a `### ` block heading) is the label.
|
|
136
|
+
const label = match[1] ?? headingLine;
|
|
137
|
+
seams.push({ offset: match.index, label });
|
|
138
|
+
}
|
|
139
|
+
if (seams.length === 0) return { prologue: body, sections: [] };
|
|
140
|
+
const prologue = body.slice(0, seams[0].offset);
|
|
141
|
+
const sections = seams.map((s, i) => ({
|
|
142
|
+
label: s.label,
|
|
143
|
+
text: body.slice(
|
|
144
|
+
s.offset,
|
|
145
|
+
i + 1 < seams.length ? seams[i + 1].offset : body.length,
|
|
146
|
+
),
|
|
147
|
+
}));
|
|
148
|
+
return { prologue, sections };
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
/**
|
|
115
152
|
* Split an over-budget weekly-log source at its `## YYYY-MM-DD` day-section
|
|
116
153
|
* seams into an ordered list of conforming parts. Pure — no I/O.
|
|
@@ -123,9 +160,13 @@ function packSections(sections, prologue, budget) {
|
|
|
123
160
|
* are greedily packed left-to-right under both the line- and word-budget, with
|
|
124
161
|
* each candidate part measured H1-included so its own H1 is charged.
|
|
125
162
|
*
|
|
126
|
-
* When a
|
|
127
|
-
*
|
|
128
|
-
*
|
|
163
|
+
* When a lone day-section alone exceeds a budget, it is re-bisected at its
|
|
164
|
+
* `### ` block seams (one grain finer) and the resulting block-parts replace it,
|
|
165
|
+
* so a single over-cap day no longer forces a hand-split. Only a single `### `
|
|
166
|
+
* block that alone exceeds a budget — or an over-cap seamless prologue — remains
|
|
167
|
+
* an irreducible residue: it is sealed as its own (over-budget) part and named
|
|
168
|
+
* in `residue` (the residue's `section` then names the block heading, not a
|
|
169
|
+
* date); the rest still packs normally.
|
|
129
170
|
*
|
|
130
171
|
* @param {string} text - The full weekly-log source (H1 + body).
|
|
131
172
|
* @param {string} agent - Agent profile id (e.g. "staff-engineer").
|
|
@@ -157,37 +198,68 @@ export function bisectWeeklyLog(text, agent, isoWeekStr) {
|
|
|
157
198
|
};
|
|
158
199
|
};
|
|
159
200
|
|
|
201
|
+
const budget = { overBudget, measure };
|
|
160
202
|
// Locate the day-section seams (date at line-start, trailing suffix
|
|
161
203
|
// tolerated, e.g. `## 2026-05-19 (third activation)`).
|
|
162
|
-
const
|
|
163
|
-
const seams = [];
|
|
164
|
-
let match;
|
|
165
|
-
while ((match = seamRe.exec(body)) !== null) {
|
|
166
|
-
seams.push({ offset: match.index, date: match[1] });
|
|
167
|
-
}
|
|
204
|
+
const { prologue, sections } = findSections(body, WEEKLY_LOG_SEAM_RE);
|
|
168
205
|
|
|
169
206
|
// Zero day-sections: the whole body is the prologue and its own single part,
|
|
170
207
|
// flagged as a residue when it alone exceeds a budget.
|
|
171
|
-
if (
|
|
172
|
-
return finish([body], prologueResidue(body,
|
|
208
|
+
if (sections.length === 0) {
|
|
209
|
+
return finish([body], prologueResidue(body, budget));
|
|
173
210
|
}
|
|
174
211
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const { partBodies, residue } = packSections(sections, prologue, {
|
|
185
|
-
overBudget,
|
|
186
|
-
measure,
|
|
187
|
-
});
|
|
212
|
+
const { partBodies, residue } = packSections(sections, prologue, budget);
|
|
213
|
+
// A lone day-section that alone exceeds a budget is the only residue
|
|
214
|
+
// `packSections` can produce here (the prologue rides part 1). Re-bisect that
|
|
215
|
+
// day at its `### ` block seams and splice the block-parts in for it; the
|
|
216
|
+
// surfaced residue becomes the inner one (an over-cap block) or null.
|
|
217
|
+
if (residue !== null) {
|
|
218
|
+
const sub = resplitDaySection(partBodies, residue, budget);
|
|
219
|
+
return finish(sub.partBodies, sub.residue);
|
|
220
|
+
}
|
|
188
221
|
return finish(partBodies, residue);
|
|
189
222
|
}
|
|
190
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Re-bisect the lone over-cap day-section that `packSections` flagged as the
|
|
226
|
+
* residue: split that part's body at its `### ` block seams and splice the
|
|
227
|
+
* resulting block-bodies into `partBodies` in its place. The day-section's body
|
|
228
|
+
* carries its own `## ` heading as a prologue above the first `### ` block, so
|
|
229
|
+
* that heading rides the first block-part. Returns the spliced `partBodies` and
|
|
230
|
+
* the inner residue (a single over-cap `### ` block, or null when every block
|
|
231
|
+
* now conforms), re-indexed to its position in the spliced list.
|
|
232
|
+
*/
|
|
233
|
+
function resplitDaySection(partBodies, residue, budget) {
|
|
234
|
+
const dayBody = partBodies[residue.partIndex];
|
|
235
|
+
// Only a day-section (its body opens with a `## ` heading) is re-split at the
|
|
236
|
+
// block grain; an over-cap seamless prologue has no day to descend into and
|
|
237
|
+
// stays the terminal residue.
|
|
238
|
+
if (!dayBody.startsWith("## ")) return { partBodies, residue };
|
|
239
|
+
const { prologue, sections } = findSections(dayBody, BLOCK_SEAM_RE);
|
|
240
|
+
// No `### ` block seam inside the day: nothing finer to cut — keep the day as
|
|
241
|
+
// the irreducible residue (criterion 3's terminal case at the day grain).
|
|
242
|
+
if (sections.length === 0) return { partBodies, residue };
|
|
243
|
+
const { partBodies: blockBodies, residue: blockResidue } = packSections(
|
|
244
|
+
sections,
|
|
245
|
+
prologue,
|
|
246
|
+
budget,
|
|
247
|
+
);
|
|
248
|
+
const spliced = [
|
|
249
|
+
...partBodies.slice(0, residue.partIndex),
|
|
250
|
+
...blockBodies,
|
|
251
|
+
...partBodies.slice(residue.partIndex + 1),
|
|
252
|
+
];
|
|
253
|
+
if (blockResidue === null) return { partBodies: spliced, residue: null };
|
|
254
|
+
return {
|
|
255
|
+
partBodies: spliced,
|
|
256
|
+
residue: {
|
|
257
|
+
...blockResidue,
|
|
258
|
+
partIndex: residue.partIndex + blockResidue.partIndex,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
191
263
|
/**
|
|
192
264
|
* Stage every write to `${path}.tmp`, then commit by renaming each `leading`
|
|
193
265
|
* write onto its path (tracked for rollback) and the `anchor` write LAST — the
|
|
@@ -261,11 +333,20 @@ function atomicSeal(filePath, parts, agent, isoWeekStr, fs) {
|
|
|
261
333
|
* into one-or-more conforming parts), or `{status:"incomplete",parts,residue}`
|
|
262
334
|
* (a lone day-section exceeds a budget and is named).
|
|
263
335
|
*
|
|
264
|
-
*
|
|
336
|
+
* A `noop` return carries a `reason` — `"missing"` (no file; no size measured),
|
|
337
|
+
* `"floor"` (header-only/empty body; nothing to seal), or `"under-budget"`
|
|
338
|
+
* (under both budgets without `--force`) — plus the measured `lines`/`words`
|
|
339
|
+
* for the two reasons that read the file, so the CLI guard need not re-read it.
|
|
340
|
+
* "Over budget" is decided here over *either* budget (lines or words), so a
|
|
341
|
+
* caller never needs `force: true` to seal a word-over/line-under log.
|
|
342
|
+
*
|
|
343
|
+
* @returns {{status: "noop"|"sealed"|"incomplete", reason?: "missing"|"floor"|"under-budget", lines?: number, words?: number, fromPath: string, parts?: string[], residue?: {path: string, section: string, lines: number, words: number}}}
|
|
265
344
|
* @param {string} wikiRoot
|
|
266
345
|
* @param {string} agent
|
|
267
346
|
* @param {string} today - ISO date string.
|
|
268
|
-
* @param {number} [
|
|
347
|
+
* @param {{lines?: number, words?: number}} [delta={}] - Projected post-append
|
|
348
|
+
* line/word delta; the trigger fires when the current file plus this delta
|
|
349
|
+
* would breach either budget. Force-rotate callers pass `{}`.
|
|
269
350
|
* @param {{force?: boolean}} [options]
|
|
270
351
|
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
271
352
|
*/
|
|
@@ -273,24 +354,52 @@ export function rotateIfOverBudget(
|
|
|
273
354
|
wikiRoot,
|
|
274
355
|
agent,
|
|
275
356
|
today,
|
|
276
|
-
|
|
357
|
+
delta = {},
|
|
277
358
|
options = {},
|
|
278
359
|
fs,
|
|
279
360
|
) {
|
|
280
361
|
const filePath = weeklyLogPath(wikiRoot, agent, today);
|
|
281
362
|
const { force = false } = options;
|
|
282
|
-
if (!fs.existsSync(filePath))
|
|
363
|
+
if (!fs.existsSync(filePath)) {
|
|
364
|
+
return { status: "noop", reason: "missing", fromPath: filePath };
|
|
365
|
+
}
|
|
283
366
|
const text = fs.readFileSync(filePath, "utf-8");
|
|
367
|
+
const lines = countLines(text);
|
|
368
|
+
const words = countWords(text);
|
|
284
369
|
// A header-only (or empty) log has nothing to seal. Without this floor,
|
|
285
370
|
// force-rotating a freshly-reset main would mint an empty `(part 1 of 1)`
|
|
286
|
-
// file and reset the main again — once per invocation, forever.
|
|
371
|
+
// file and reset the main again — once per invocation, forever. The floor
|
|
372
|
+
// holds even under `--force`, so it is checked before the force branch.
|
|
287
373
|
const nl = text.indexOf("\n");
|
|
288
374
|
if ((nl === -1 ? "" : text.slice(nl + 1)).trim() === "") {
|
|
289
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
status: "noop",
|
|
377
|
+
reason: "floor",
|
|
378
|
+
lines,
|
|
379
|
+
words,
|
|
380
|
+
fromPath: filePath,
|
|
381
|
+
};
|
|
290
382
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
383
|
+
// Over either budget (lines or words), decided once here in core: a
|
|
384
|
+
// word-over/line-under log seals without `--force`. The projection folds in
|
|
385
|
+
// the caller's append delta so a pre-append rotate fires on the post-append
|
|
386
|
+
// size. The `noop`/`under-budget` arm carries the measured size so the CLI
|
|
387
|
+
// handler can report the resolved target without re-reading the file.
|
|
388
|
+
const { lines: dLines = 0, words: dWords = 0 } = delta;
|
|
389
|
+
const projectedLines = lines + dLines;
|
|
390
|
+
const projectedWords = words + dWords;
|
|
391
|
+
if (
|
|
392
|
+
!force &&
|
|
393
|
+
projectedLines <= WEEKLY_LOG_LINE_BUDGET &&
|
|
394
|
+
projectedWords <= WEEKLY_LOG_WORD_BUDGET
|
|
395
|
+
) {
|
|
396
|
+
return {
|
|
397
|
+
status: "noop",
|
|
398
|
+
reason: "under-budget",
|
|
399
|
+
lines,
|
|
400
|
+
words,
|
|
401
|
+
fromPath: filePath,
|
|
402
|
+
};
|
|
294
403
|
}
|
|
295
404
|
const isoWeekStr = isoWeekString(today);
|
|
296
405
|
const { parts, residue } = bisectWeeklyLog(text, agent, isoWeekStr);
|
|
@@ -388,7 +497,7 @@ export function rebisectOverBudgetPart(partPath, fs) {
|
|
|
388
497
|
// surface a residue (synthesised from the file when the bisector did not name
|
|
389
498
|
// one) so the caller's re-audit re-flags it.
|
|
390
499
|
if (parts.length === 1) {
|
|
391
|
-
const seam = text.match(
|
|
500
|
+
const seam = text.match(new RegExp(WEEKLY_LOG_SEAM_RE.source, "m"));
|
|
392
501
|
const r = residue ?? {
|
|
393
502
|
section: seam ? seam[1] : "prologue",
|
|
394
503
|
lines,
|