@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/weekly-log.js
CHANGED
|
@@ -8,12 +8,13 @@ import {
|
|
|
8
8
|
WEEKLY_LOG_WORD_BUDGET,
|
|
9
9
|
} from "./constants.js";
|
|
10
10
|
|
|
11
|
-
// Block seam inside a day-section: a `### ` heading at line start.
|
|
12
|
-
//
|
|
11
|
+
// Block seam inside a day-section: a `### ` heading at line start. Rotation
|
|
12
|
+
// falls to this finer grain when a lone day-section alone exceeds a budget.
|
|
13
13
|
const BLOCK_SEAM_RE = /^### /;
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
// `new Date
|
|
15
|
+
// libutil's calendar util computes the ISO week. It is the one place that may
|
|
16
|
+
// call `new Date`. This module re-exports the function for the existing
|
|
17
|
+
// public surface.
|
|
17
18
|
export { isoWeek } from "@forwardimpact/libutil";
|
|
18
19
|
|
|
19
20
|
/** Return the path of the current weekly log file for an agent. */
|
|
@@ -27,10 +28,10 @@ function partPathAt(filePath, n) {
|
|
|
27
28
|
return path.join(dir, `${base}-part${n}.md`);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
// Find `count` part slots that are each verified free
|
|
31
|
-
// `-partN.md` (e.g. a
|
|
32
|
-
// Every returned slot is unoccupied, so the seal never overwrites a
|
|
33
|
-
// part on commit nor unlinks one on rollback.
|
|
31
|
+
// Find `count` part slots that are each verified free. Skip any occupied
|
|
32
|
+
// `-partN.md` (e.g. a gap in the numbers after someone deleted a middle part
|
|
33
|
+
// by hand). Every returned slot is unoccupied, so the seal never overwrites a
|
|
34
|
+
// pre-existing part on commit nor unlinks one on rollback.
|
|
34
35
|
function nextFreeSlots(filePath, count, fs) {
|
|
35
36
|
const slots = [];
|
|
36
37
|
let n = 1;
|
|
@@ -61,9 +62,9 @@ function residueOf(sec, partIndex, measure) {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* An over-cap prologue cannot merge with any day-section
|
|
65
|
-
* grows it
|
|
66
|
-
* first residue so it
|
|
65
|
+
* An over-cap prologue cannot merge with any day-section, because more content
|
|
66
|
+
* only grows it. So it always seals as its own part 0. Flag it up front as the
|
|
67
|
+
* first residue so it never ships silently over budget.
|
|
67
68
|
*/
|
|
68
69
|
function prologueResidue(prologue, { overBudget, measure }) {
|
|
69
70
|
if (prologue.length === 0 || !overBudget(prologue)) return null;
|
|
@@ -72,23 +73,25 @@ function prologueResidue(prologue, { overBudget, measure }) {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
|
-
* Greedily pack day-sections into part bodies under both budgets
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* so the only over-cap part bodies are the ones `residue`
|
|
76
|
+
* Greedily pack day-sections into part bodies under both budgets. The prologue
|
|
77
|
+
* rides with part 1. A chunk that alone exceeds a budget seals as its own part,
|
|
78
|
+
* and the packer records it as the (first) residue. Such a chunk is a lone
|
|
79
|
+
* day-section or an over-cap prologue. Packed runs and single sections stay
|
|
80
|
+
* under both budgets, so the only over-cap part bodies are the ones `residue`
|
|
81
|
+
* accounts for.
|
|
80
82
|
* @param {Array<{date: string, text: string}>} sections
|
|
81
|
-
* @param {string} prologue - Content above the first seam
|
|
83
|
+
* @param {string} prologue - Content above the first seam. It rides with
|
|
84
|
+
* part 1.
|
|
82
85
|
* @param {{overBudget: (s: string) => boolean, measure: (s: string) => {lines: number, words: number}}} budget
|
|
83
86
|
* @returns {{partBodies: string[], residue: null | {section: string, lines: number, words: number, partIndex: number}}}
|
|
84
87
|
*/
|
|
85
88
|
function packSections(sections, prologue, budget) {
|
|
86
89
|
const { overBudget, measure } = budget;
|
|
87
90
|
const partBodies = [];
|
|
88
|
-
// The
|
|
89
|
-
// before
|
|
91
|
+
// The packer always pushes an over-budget prologue first (part 0). Record it
|
|
92
|
+
// before you pack, so a later lone-section residue cannot displace it.
|
|
90
93
|
let residue = prologueResidue(prologue, budget);
|
|
91
|
-
let open = prologue; // body of the part
|
|
94
|
+
let open = prologue; // body of the part the packer fills now
|
|
92
95
|
let opened = prologue.length > 0;
|
|
93
96
|
const flush = () => {
|
|
94
97
|
if (opened) {
|
|
@@ -99,7 +102,7 @@ function packSections(sections, prologue, budget) {
|
|
|
99
102
|
};
|
|
100
103
|
for (const sec of sections) {
|
|
101
104
|
if (overBudget(sec.text)) {
|
|
102
|
-
// Irreducible lone section
|
|
105
|
+
// Irreducible lone section. Flush the open part. Then seal it alone.
|
|
103
106
|
flush();
|
|
104
107
|
residue ??= residueOf(sec, partBodies.length, measure);
|
|
105
108
|
partBodies.push(sec.text);
|
|
@@ -118,10 +121,10 @@ function packSections(sections, prologue, budget) {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/**
|
|
121
|
-
* Slice `body` at `seamRe` seam offsets into `{label, text}` sections
|
|
122
|
-
* label
|
|
124
|
+
* Slice `body` at `seamRe` seam offsets into `{label, text}` sections. Each
|
|
125
|
+
* label is the seam's full matched heading line (date or `### ` heading).
|
|
123
126
|
* Returns `{prologue, sections}` where the prologue is everything above the
|
|
124
|
-
* first seam (the whole body when there are no seams).
|
|
127
|
+
* first seam (the whole body when there are no seams). A concatenation of the
|
|
125
128
|
* prologue and every section's text reproduces `body` byte-for-byte.
|
|
126
129
|
*/
|
|
127
130
|
function findSections(body, seamRe) {
|
|
@@ -131,7 +134,7 @@ function findSections(body, seamRe) {
|
|
|
131
134
|
while ((match = re.exec(body)) !== null) {
|
|
132
135
|
const eol = body.indexOf("\n", match.index);
|
|
133
136
|
const headingLine = body.slice(match.index, eol === -1 ? body.length : eol);
|
|
134
|
-
// A captured group (the day seam's date) labels the section
|
|
137
|
+
// A captured group (the day seam's date) labels the section. Otherwise the
|
|
135
138
|
// full heading line (a `### ` block heading) is the label.
|
|
136
139
|
const label = match[1] ?? headingLine;
|
|
137
140
|
seams.push({ offset: match.index, label });
|
|
@@ -150,23 +153,27 @@ function findSections(body, seamRe) {
|
|
|
150
153
|
|
|
151
154
|
/**
|
|
152
155
|
* Split an over-budget weekly-log source at its `## YYYY-MM-DD` day-section
|
|
153
|
-
* seams into an ordered list of conforming parts.
|
|
156
|
+
* seams into an ordered list of conforming parts. The function is pure. It does
|
|
157
|
+
* no I/O.
|
|
154
158
|
*
|
|
155
|
-
* The first line of `text` is the original H1
|
|
156
|
-
* per-part H1s, never
|
|
157
|
-
* line is the body
|
|
158
|
-
*
|
|
159
|
-
* The prologue (any content above
|
|
160
|
-
*
|
|
161
|
-
*
|
|
159
|
+
* The first line of `text` is the original H1. The function consumes it and
|
|
160
|
+
* replaces it with per-part H1s, so it never appears in a part body. Everything
|
|
161
|
+
* after that first line is the body. The function slices the body at the
|
|
162
|
+
* day-section seam byte offsets, so a concatenation of the parts' bodies
|
|
163
|
+
* reproduces the original body byte-for-byte. The prologue (any content above
|
|
164
|
+
* the first seam) rides with part 1. The function packs sections greedily from
|
|
165
|
+
* left to right under both the line- and word-budget. It measures each
|
|
166
|
+
* candidate part with the H1 included, so the measurement charges the part's
|
|
167
|
+
* own H1.
|
|
162
168
|
*
|
|
163
|
-
* When a lone day-section alone exceeds a budget,
|
|
164
|
-
* `### ` block seams (one grain finer)
|
|
165
|
-
*
|
|
166
|
-
* block that alone exceeds a budget
|
|
167
|
-
*
|
|
168
|
-
* in `residue
|
|
169
|
-
*
|
|
169
|
+
* When a lone day-section alone exceeds a budget, the function re-bisects it at
|
|
170
|
+
* its `### ` block seams (one grain finer). The block-parts then replace it, so
|
|
171
|
+
* a single over-cap day no longer forces a hand-split. Only these remain an
|
|
172
|
+
* irreducible residue: a single `### ` block that alone exceeds a budget, or an
|
|
173
|
+
* over-cap seamless prologue. The function seals such a chunk as its own
|
|
174
|
+
* (over-budget) part and names it in `residue`. The residue's `section` then
|
|
175
|
+
* names the block heading. It does not name a date. The rest still packs
|
|
176
|
+
* normally.
|
|
170
177
|
*
|
|
171
178
|
* @param {string} text - The full weekly-log source (H1 + body).
|
|
172
179
|
* @param {string} agent - Agent profile id (e.g. "staff-engineer").
|
|
@@ -177,9 +184,9 @@ export function bisectWeeklyLog(text, agent, isoWeekStr) {
|
|
|
177
184
|
const nl = text.indexOf("\n");
|
|
178
185
|
const body = nl === -1 ? "" : text.slice(nl + 1);
|
|
179
186
|
const title = agentTitle(agent);
|
|
180
|
-
// `(part N of M)` costs the same 1 line and 4 word-tokens
|
|
181
|
-
// digits in N/M
|
|
182
|
-
//
|
|
187
|
+
// `(part N of M)` costs the same 1 line and 4 word-tokens whatever the
|
|
188
|
+
// digits in N/M are. So a fixed template measures every part exactly. It
|
|
189
|
+
// does not need M before the packer finishes.
|
|
183
190
|
const h1Template = `# ${title} — ${isoWeekStr} (part 1 of 1)`;
|
|
184
191
|
const measure = (chunk) => {
|
|
185
192
|
const rendered = `${h1Template}\n${chunk}`;
|
|
@@ -199,12 +206,12 @@ export function bisectWeeklyLog(text, agent, isoWeekStr) {
|
|
|
199
206
|
};
|
|
200
207
|
|
|
201
208
|
const budget = { overBudget, measure };
|
|
202
|
-
// Locate the day-section seams
|
|
203
|
-
//
|
|
209
|
+
// Locate the day-section seams. The date sits at line-start. The regex
|
|
210
|
+
// tolerates a trailing suffix, e.g. `## 2026-05-19 (third activation)`.
|
|
204
211
|
const { prologue, sections } = findSections(body, WEEKLY_LOG_SEAM_RE);
|
|
205
212
|
|
|
206
|
-
// Zero day-sections
|
|
207
|
-
//
|
|
213
|
+
// Zero day-sections. The whole body is the prologue and its own single part.
|
|
214
|
+
// The function flags it as a residue when it alone exceeds a budget.
|
|
208
215
|
if (sections.length === 0) {
|
|
209
216
|
return finish([body], prologueResidue(body, budget));
|
|
210
217
|
}
|
|
@@ -212,7 +219,7 @@ export function bisectWeeklyLog(text, agent, isoWeekStr) {
|
|
|
212
219
|
const { partBodies, residue } = packSections(sections, prologue, budget);
|
|
213
220
|
// A lone day-section that alone exceeds a budget is the only residue
|
|
214
221
|
// `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
|
|
222
|
+
// day at its `### ` block seams and splice the block-parts in for it. The
|
|
216
223
|
// surfaced residue becomes the inner one (an over-cap block) or null.
|
|
217
224
|
if (residue !== null) {
|
|
218
225
|
const sub = resplitDaySection(partBodies, residue, budget);
|
|
@@ -223,22 +230,24 @@ export function bisectWeeklyLog(text, agent, isoWeekStr) {
|
|
|
223
230
|
|
|
224
231
|
/**
|
|
225
232
|
* Re-bisect the lone over-cap day-section that `packSections` flagged as the
|
|
226
|
-
* residue
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
233
|
+
* residue. Split that part's body at its `### ` block seams. Then splice the
|
|
234
|
+
* block-bodies into `partBodies` in its place. The day-section's body carries
|
|
235
|
+
* its own `## ` heading as a prologue above the first `### ` block, so that
|
|
236
|
+
* heading rides the first block-part. Returns the spliced `partBodies` and the
|
|
237
|
+
* inner residue (a single over-cap `### ` block, or null when every block now
|
|
238
|
+
* conforms). The function re-indexes that residue to its position in the
|
|
239
|
+
* spliced list.
|
|
232
240
|
*/
|
|
233
241
|
function resplitDaySection(partBodies, residue, budget) {
|
|
234
242
|
const dayBody = partBodies[residue.partIndex];
|
|
235
|
-
//
|
|
236
|
-
// block grain
|
|
237
|
-
// stays the terminal residue.
|
|
243
|
+
// The function re-splits only a day-section (its body opens with a `## `
|
|
244
|
+
// heading) at the block grain. An over-cap seamless prologue has no day to
|
|
245
|
+
// descend into and stays the terminal residue.
|
|
238
246
|
if (!dayBody.startsWith("## ")) return { partBodies, residue };
|
|
239
247
|
const { prologue, sections } = findSections(dayBody, BLOCK_SEAM_RE);
|
|
240
|
-
// No `### ` block seam inside the day
|
|
241
|
-
// the irreducible residue (criterion 3's terminal case at the day
|
|
248
|
+
// No `### ` block seam inside the day. Nothing finer is left to cut. Keep the
|
|
249
|
+
// day as the irreducible residue (criterion 3's terminal case at the day
|
|
250
|
+
// grain).
|
|
242
251
|
if (sections.length === 0) return { partBodies, residue };
|
|
243
252
|
const { partBodies: blockBodies, residue: blockResidue } = packSections(
|
|
244
253
|
sections,
|
|
@@ -261,13 +270,14 @@ function resplitDaySection(partBodies, residue, budget) {
|
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
/**
|
|
264
|
-
* Stage every write to `${path}.tmp
|
|
265
|
-
*
|
|
266
|
-
* single point of no return. The anchor is the live/source
|
|
267
|
-
* rename it still holds its original bytes
|
|
268
|
-
* committed leading path and remaining temp and
|
|
269
|
-
* path
|
|
270
|
-
* rollback unlinks them). Returns the
|
|
273
|
+
* Stage every write to `${path}.tmp`. Then commit. Rename each `leading` write
|
|
274
|
+
* onto its path (tracked for rollback). Rename the `anchor` write LAST. That
|
|
275
|
+
* last rename is the single point of no return. The anchor is the live/source
|
|
276
|
+
* file. Until its rename it still holds its original bytes. So a failure
|
|
277
|
+
* anywhere unlinks every committed leading path and remaining temp, and
|
|
278
|
+
* re-throws. The anchor's path, contents, and inode stay untouched. Leading
|
|
279
|
+
* paths must be verified-free slots (a rollback unlinks them). Returns the
|
|
280
|
+
* leading paths, in order.
|
|
271
281
|
*
|
|
272
282
|
* @param {Array<{path: string, content: string}>} leading - Committed first.
|
|
273
283
|
* @param {{path: string, content: string}} anchor - Committed last.
|
|
@@ -305,9 +315,9 @@ function commitAtomic(leading, anchor, fs) {
|
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
/**
|
|
308
|
-
* Seal a bisected weekly log
|
|
309
|
-
* fresh empty main over `filePath` (the anchor, committed last).
|
|
310
|
-
* `-partN.md` slot paths in part order.
|
|
318
|
+
* Seal a bisected weekly log. Write each part to a fresh `-partN.md` slot.
|
|
319
|
+
* Write a fresh empty main over `filePath` (the anchor, committed last).
|
|
320
|
+
* Returns the `-partN.md` slot paths in part order.
|
|
311
321
|
*
|
|
312
322
|
* @param {string} filePath - The current weekly-log path.
|
|
313
323
|
* @param {Array<{h1: string, body: string}>} parts - Ordered parts to seal.
|
|
@@ -327,25 +337,26 @@ function atomicSeal(filePath, parts, agent, isoWeekStr, fs) {
|
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
/**
|
|
330
|
-
* Rotate the current weekly log
|
|
331
|
-
* budget-conforming parts
|
|
340
|
+
* Rotate the current weekly log. The function bisects an over-budget source and
|
|
341
|
+
* seals it into budget-conforming parts. Returns a tagged union:
|
|
332
342
|
* `{status:"noop"}` (no rotation needed), `{status:"sealed",parts}` (sealed
|
|
333
343
|
* into one-or-more conforming parts), or `{status:"incomplete",parts,residue}`
|
|
334
|
-
* (a lone day-section exceeds a budget and
|
|
344
|
+
* (a lone day-section exceeds a budget, and the result names it).
|
|
335
345
|
*
|
|
336
|
-
* A `noop` return carries a `reason
|
|
337
|
-
* `"floor"` (header-only
|
|
338
|
-
* (under both budgets without `--force`)
|
|
339
|
-
* for the two reasons that read the file,
|
|
340
|
-
*
|
|
341
|
-
* caller never needs `force: true`
|
|
346
|
+
* A `noop` return carries a `reason`. The reason is `"missing"` (no file, no
|
|
347
|
+
* size measured), `"floor"` (header-only or empty body, nothing to seal), or
|
|
348
|
+
* `"under-budget"` (under both budgets without `--force`). The return also
|
|
349
|
+
* carries the measured `lines`/`words` for the two reasons that read the file,
|
|
350
|
+
* so the CLI guard need not re-read it. This function decides "over budget"
|
|
351
|
+
* over *either* budget (lines or words), so a caller never needs `force: true`
|
|
352
|
+
* to seal a word-over/line-under log.
|
|
342
353
|
*
|
|
343
354
|
* @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}}}
|
|
344
355
|
* @param {string} wikiRoot
|
|
345
356
|
* @param {string} agent
|
|
346
357
|
* @param {string} today - ISO date string.
|
|
347
358
|
* @param {{lines?: number, words?: number}} [delta={}] - Projected post-append
|
|
348
|
-
* line/word delta
|
|
359
|
+
* line/word delta. The trigger fires when the current file plus this delta
|
|
349
360
|
* would breach either budget. Force-rotate callers pass `{}`.
|
|
350
361
|
* @param {{force?: boolean}} [options]
|
|
351
362
|
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
@@ -366,10 +377,11 @@ export function rotateIfOverBudget(
|
|
|
366
377
|
const text = fs.readFileSync(filePath, "utf-8");
|
|
367
378
|
const lines = countLines(text);
|
|
368
379
|
const words = countWords(text);
|
|
369
|
-
// A header-only (or empty) log has nothing to seal. Without this floor,
|
|
370
|
-
// force-
|
|
371
|
-
// file and reset the main again
|
|
372
|
-
// holds even under `--force`, so
|
|
380
|
+
// A header-only (or empty) log has nothing to seal. Without this floor, a
|
|
381
|
+
// force-rotate of a freshly-reset main would mint an empty `(part 1 of 1)`
|
|
382
|
+
// file and reset the main again. That would repeat once per invocation,
|
|
383
|
+
// forever. The floor holds even under `--force`, so the code checks it
|
|
384
|
+
// before the force branch.
|
|
373
385
|
const nl = text.indexOf("\n");
|
|
374
386
|
if ((nl === -1 ? "" : text.slice(nl + 1)).trim() === "") {
|
|
375
387
|
return {
|
|
@@ -380,11 +392,11 @@ export function rotateIfOverBudget(
|
|
|
380
392
|
fromPath: filePath,
|
|
381
393
|
};
|
|
382
394
|
}
|
|
383
|
-
//
|
|
395
|
+
// Core decides "over either budget" (lines or words) once, here. So a
|
|
384
396
|
// word-over/line-under log seals without `--force`. The projection folds in
|
|
385
397
|
// the caller's append delta so a pre-append rotate fires on the post-append
|
|
386
398
|
// size. The `noop`/`under-budget` arm carries the measured size so the CLI
|
|
387
|
-
// handler can report the resolved target
|
|
399
|
+
// handler can report the resolved target and need not re-read the file.
|
|
388
400
|
const { lines: dLines = 0, words: dWords = 0 } = delta;
|
|
389
401
|
const projectedLines = lines + dLines;
|
|
390
402
|
const projectedWords = words + dWords;
|
|
@@ -422,11 +434,11 @@ export function rotateIfOverBudget(
|
|
|
422
434
|
|
|
423
435
|
/**
|
|
424
436
|
* Derive the agent, ISO week, and MAIN-log path from a sealed part's path. The
|
|
425
|
-
* week comes from the filename
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* WEEKLY_LOG_PART_NAME_RE with the audit's file classifier so
|
|
429
|
-
* drift on the filename convention.
|
|
437
|
+
* week comes from the filename, because a part may belong to a past week
|
|
438
|
+
* instead of today. `nextFreeSlots` must base new sibling slots on the main-log
|
|
439
|
+
* path. It must not use the part path. Returns null for a non-conforming
|
|
440
|
+
* filename. Shares WEEKLY_LOG_PART_NAME_RE with the audit's file classifier so
|
|
441
|
+
* the two cannot drift on the filename convention.
|
|
430
442
|
*/
|
|
431
443
|
function parsePartPath(partPath) {
|
|
432
444
|
const m = path.basename(partPath).match(WEEKLY_LOG_PART_NAME_RE);
|
|
@@ -441,8 +453,8 @@ function parsePartPath(partPath) {
|
|
|
441
453
|
}
|
|
442
454
|
|
|
443
455
|
/**
|
|
444
|
-
* Reseal a re-bisected part
|
|
445
|
-
* anchor, committed last)
|
|
456
|
+
* Reseal a re-bisected part. The first sub-part overwrites the source slot (the
|
|
457
|
+
* anchor, committed last). The rest claim fresh sibling slots of the main-log
|
|
446
458
|
* path. `nextFreeSlots` skips occupied slots (including the source's own), so a
|
|
447
459
|
* commit never clobbers a sibling nor a rollback unlinks a pre-existing one.
|
|
448
460
|
* Returns `[partPath, ...newSlots]` in part order.
|
|
@@ -463,16 +475,18 @@ function atomicResealPart(partPath, mainLogPath, parts, fs) {
|
|
|
463
475
|
|
|
464
476
|
/**
|
|
465
477
|
* Re-bisect a single over-budget sealed weekly-log PART in place. Agent and ISO
|
|
466
|
-
* week come from the part filename. A part within both budgets is a noop
|
|
467
|
-
* whose body cannot be reduced
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* overwrites `partPath` (slot reused) and the
|
|
471
|
-
* sibling slots, with full rollback (source
|
|
478
|
+
* week come from the part filename. A part within both budgets is a noop. A
|
|
479
|
+
* part whose body cannot be reduced stays BYTE-IDENTICAL. The function reports
|
|
480
|
+
* it `incomplete` with a residue, so the re-audit re-flags it for a human. Such
|
|
481
|
+
* a part is a lone over-cap day-section or an over-cap zero-seam body.
|
|
482
|
+
* Otherwise the first sub-part overwrites `partPath` (slot reused) and the
|
|
483
|
+
* remaining sub-parts land on fresh sibling slots, with full rollback (source
|
|
484
|
+
* untouched on any failure).
|
|
472
485
|
*
|
|
473
486
|
* The produced sub-parts carry `bisectWeeklyLog`'s `(part i of M)` H1s, where M
|
|
474
|
-
* is LOCAL to this part's split
|
|
475
|
-
*
|
|
487
|
+
* is LOCAL to this part's split. M is not a global count of the week's parts.
|
|
488
|
+
* The function never renumbers sibling parts (the audit does not validate the
|
|
489
|
+
* numbers).
|
|
476
490
|
*
|
|
477
491
|
* @param {string} partPath - Absolute path to an `<agent>-YYYY-Www-partN.md`.
|
|
478
492
|
* @param {object} fs - Sync filesystem surface (`runtime.fsSync`).
|
|
@@ -493,7 +507,7 @@ export function rebisectOverBudgetPart(partPath, fs) {
|
|
|
493
507
|
parsed.agent,
|
|
494
508
|
parsed.isoWeekStr,
|
|
495
509
|
);
|
|
496
|
-
// A single produced part has no splittable seam
|
|
510
|
+
// A single produced part has no splittable seam. Leave the file untouched and
|
|
497
511
|
// surface a residue (synthesised from the file when the bisector did not name
|
|
498
512
|
// one) so the caller's re-audit re-flags it.
|
|
499
513
|
if (parts.length === 1) {
|