@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -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 (stopRe.test(lines[j])) return false;
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
- entryRe.test(s.fileLines[i]) &&
97
- !entryHasDecision(s.fileLines, i, requiredLine, stopRe)
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` to seal this file as a sealed part and start a fresh weekly log",
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` to seal this file as a sealed part and start a fresh weekly log",
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: () => "Entry lacks leading '### Decision'",
287
- hint: "open each '## YYYY-MM-DD' entry with a '### Decision' subheading that summarises the decision recorded in the entry's own narrative — do not invent rationale the entry does not support",
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) --
@@ -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 };