@forwardimpact/libwiki 0.2.20 → 0.2.22
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/package.json +1 -1
- package/src/audit/rules.js +19 -13
- package/src/commands/rotate.js +7 -1
- package/src/weekly-log.js +7 -0
package/package.json
CHANGED
package/src/audit/rules.js
CHANGED
|
@@ -75,16 +75,22 @@ const columnCount = (expected) => (s) =>
|
|
|
75
75
|
const exists = (s) => (s.exists ? null : {});
|
|
76
76
|
const expired = (s, ctx) => (s.expires_at < ctx.today ? {} : null);
|
|
77
77
|
|
|
78
|
+
// The heading must equal `requiredLine` exactly — a suffixed variant like
|
|
79
|
+
// "### Decision — <summary>" does not satisfy it, but is reported as a
|
|
80
|
+
// near miss so the writer fixes the heading instead of hunting for a
|
|
81
|
+
// "missing" line that is right there.
|
|
78
82
|
function entryHasDecision(lines, startIdx, requiredLine, stopRe) {
|
|
79
83
|
let seen = 0;
|
|
84
|
+
let nearMiss = null;
|
|
80
85
|
for (let j = startIdx + 1; j < lines.length && seen < 5; j++) {
|
|
81
86
|
const ln = lines[j].trim();
|
|
82
87
|
if (ln === "") continue;
|
|
83
88
|
seen++;
|
|
84
|
-
if (ln === requiredLine) return true;
|
|
85
|
-
if (
|
|
89
|
+
if (ln === requiredLine) return { found: true };
|
|
90
|
+
if (nearMiss === null && ln.startsWith(requiredLine)) nearMiss = ln;
|
|
91
|
+
if (stopRe.test(lines[j])) break;
|
|
86
92
|
}
|
|
87
|
-
return false;
|
|
93
|
+
return { found: false, nearMiss };
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
const decisionWithin5 =
|
|
@@ -92,12 +98,9 @@ const decisionWithin5 =
|
|
|
92
98
|
(s) => {
|
|
93
99
|
const offenders = [];
|
|
94
100
|
for (let i = 0; i < s.fileLines.length; i++) {
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
) {
|
|
99
|
-
offenders.push({ lineNo: i + 1 });
|
|
100
|
-
}
|
|
101
|
+
if (!entryRe.test(s.fileLines[i])) continue;
|
|
102
|
+
const res = entryHasDecision(s.fileLines, i, requiredLine, stopRe);
|
|
103
|
+
if (!res.found) offenders.push({ lineNo: i + 1, nearMiss: res.nearMiss });
|
|
101
104
|
}
|
|
102
105
|
return offenders.length === 0 ? null : offenders;
|
|
103
106
|
};
|
|
@@ -254,7 +257,7 @@ export const RULES = [
|
|
|
254
257
|
remediation: "rotate",
|
|
255
258
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
256
259
|
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
257
|
-
hint: "run `bunx fit-wiki rotate
|
|
260
|
+
hint: "run `bunx fit-wiki rotate --agent <agent>` (agent = this filename's prefix) to seal this file as a sealed part and start a fresh weekly log",
|
|
258
261
|
},
|
|
259
262
|
{
|
|
260
263
|
id: "weekly-log.word-budget",
|
|
@@ -263,7 +266,7 @@ export const RULES = [
|
|
|
263
266
|
remediation: "rotate",
|
|
264
267
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
265
268
|
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
266
|
-
hint: "run `bunx fit-wiki rotate
|
|
269
|
+
hint: "run `bunx fit-wiki rotate --agent <agent>` (agent = this filename's prefix) to seal this file as a sealed part and start a fresh weekly log",
|
|
267
270
|
},
|
|
268
271
|
{
|
|
269
272
|
id: "weekly-log.h1-agent-matches-filename",
|
|
@@ -283,8 +286,11 @@ export const RULES = [
|
|
|
283
286
|
requiredLine: DECISION_HEADING,
|
|
284
287
|
stopRe: /^##\s/,
|
|
285
288
|
}),
|
|
286
|
-
message: () =>
|
|
287
|
-
|
|
289
|
+
message: (_s, r) =>
|
|
290
|
+
r.nearMiss
|
|
291
|
+
? `Entry opens with '${r.nearMiss}'; the heading must be exactly '${DECISION_HEADING}' — move the suffix into the body`
|
|
292
|
+
: `Entry lacks a line that is exactly '${DECISION_HEADING}'`,
|
|
293
|
+
hint: `open each '## YYYY-MM-DD' entry with a line containing exactly '${DECISION_HEADING}' (no suffix — the check is an exact match); put the one-line summary in the body below it, drawn from the entry's own narrative — do not invent rationale the entry does not support`,
|
|
288
294
|
},
|
|
289
295
|
|
|
290
296
|
// -- Weekly logs (sealed parts) --
|
package/src/commands/rotate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rotateIfOverBudget } from "../weekly-log.js";
|
|
1
|
+
import { rotateIfOverBudget, weeklyLogPath } from "../weekly-log.js";
|
|
2
2
|
import { currentDayIso } from "../util/clock.js";
|
|
3
3
|
import { resolveWikiRoot } from "../util/wiki-dir.js";
|
|
4
4
|
|
|
@@ -16,6 +16,12 @@ export function runRotateCommand(ctx) {
|
|
|
16
16
|
}
|
|
17
17
|
const wikiRoot = resolveWikiRoot(runtime, options);
|
|
18
18
|
const today = options.today || currentDayIso(runtime);
|
|
19
|
+
// Name the resolved target before any seal: the file follows from agent +
|
|
20
|
+
// current week, not from any audit finding, so an env-default agent can
|
|
21
|
+
// silently select a different file than the one the operator has in mind.
|
|
22
|
+
runtime.proc.stdout.write(
|
|
23
|
+
`target → ${weeklyLogPath(wikiRoot, agent, today)}\n`,
|
|
24
|
+
);
|
|
19
25
|
|
|
20
26
|
let result;
|
|
21
27
|
try {
|
package/src/weekly-log.js
CHANGED
|
@@ -281,6 +281,13 @@ export function rotateIfOverBudget(
|
|
|
281
281
|
const { force = false } = options;
|
|
282
282
|
if (!fs.existsSync(filePath)) return { status: "noop", fromPath: filePath };
|
|
283
283
|
const text = fs.readFileSync(filePath, "utf-8");
|
|
284
|
+
// A header-only (or empty) log has nothing to seal. Without this floor,
|
|
285
|
+
// 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.
|
|
287
|
+
const nl = text.indexOf("\n");
|
|
288
|
+
if ((nl === -1 ? "" : text.slice(nl + 1)).trim() === "") {
|
|
289
|
+
return { status: "noop", fromPath: filePath };
|
|
290
|
+
}
|
|
284
291
|
const current = countLines(text);
|
|
285
292
|
if (!force && current + appendLines <= WEEKLY_LOG_LINE_BUDGET) {
|
|
286
293
|
return { status: "noop", fromPath: filePath };
|