@forwardimpact/libwiki 0.2.6 → 0.2.8
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 +2 -2
- package/bin/fit-wiki.js +2 -2
- package/package.json +1 -1
- package/src/audit/rules.js +92 -55
- package/src/audit/scopes.js +8 -2
- package/src/block-renderer.js +2 -9
- package/src/commands/audit.js +14 -6
- package/src/commands/refresh.js +39 -4
- package/src/constants.js +9 -5
- package/src/index.js +1 -1
- package/src/issue-list-renderer.js +25 -8
- package/src/marker-scanner.js +8 -4
- package/src/audit/engine.js +0 -36
- package/src/audit/format.js +0 -39
package/README.md
CHANGED
|
@@ -53,8 +53,8 @@ line budget would be exceeded.
|
|
|
53
53
|
### `claim` / `release` — coordinate work
|
|
54
54
|
|
|
55
55
|
```sh
|
|
56
|
-
npx fit-wiki claim --agent X --target spec-
|
|
57
|
-
npx fit-wiki release --agent X --target spec-
|
|
56
|
+
npx fit-wiki claim --agent X --target spec-NNNN --branch claude/spec-NNNN
|
|
57
|
+
npx fit-wiki release --agent X --target spec-NNNN
|
|
58
58
|
npx fit-wiki release --agent X --expired
|
|
59
59
|
```
|
|
60
60
|
|
package/bin/fit-wiki.js
CHANGED
|
@@ -222,8 +222,8 @@ const definition = {
|
|
|
222
222
|
examples: [
|
|
223
223
|
"fit-wiki boot --agent staff-engineer",
|
|
224
224
|
'fit-wiki log decision --agent staff-engineer --surveyed "..." --chosen "..." --rationale "..."',
|
|
225
|
-
"fit-wiki claim --agent staff-engineer --target spec-
|
|
226
|
-
"fit-wiki release --agent staff-engineer --target spec-
|
|
225
|
+
"fit-wiki claim --agent staff-engineer --target spec-NNNN --branch claude/...",
|
|
226
|
+
"fit-wiki release --agent staff-engineer --target spec-NNNN",
|
|
227
227
|
"fit-wiki inbox list --agent staff-engineer",
|
|
228
228
|
"fit-wiki rotate --agent staff-engineer",
|
|
229
229
|
"fit-wiki audit",
|
package/package.json
CHANGED
package/src/audit/rules.js
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
DECISION_HEADING,
|
|
5
5
|
MEMO_INBOX_MARKER,
|
|
6
6
|
PRIORITY_INDEX_HEADING,
|
|
7
|
+
STORYBOARD_LINE_BUDGET,
|
|
8
|
+
STORYBOARD_WORD_BUDGET,
|
|
7
9
|
SUMMARY_LINE_BUDGET,
|
|
8
10
|
SUMMARY_WORD_BUDGET,
|
|
9
11
|
WEEKLY_LOG_LINE_BUDGET,
|
|
@@ -23,11 +25,15 @@ const CLAIMS_HEADER_RE =
|
|
|
23
25
|
const CLAIMS_SEPARATOR_RE =
|
|
24
26
|
/^\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|\s*---\s*\|/m;
|
|
25
27
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Marker regexes (mirror of marker-scanner.js): tolerate optional trailing
|
|
29
|
+
// text inside the marker so an open marker can carry an inline notice like
|
|
30
|
+
// "Do not edit. Auto-generated." without breaking the audit's balance check.
|
|
31
|
+
const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
32
|
+
const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
28
33
|
const ISSUE_OPEN_RE =
|
|
29
|
-
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?\s*-->\s*$/;
|
|
30
|
-
const ISSUE_CLOSE_RE =
|
|
34
|
+
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
35
|
+
const ISSUE_CLOSE_RE =
|
|
36
|
+
/^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
31
37
|
|
|
32
38
|
// improvement-coach is the storyboard facilitator and carries no domain
|
|
33
39
|
// metrics; only the five domain agents need their own H3.
|
|
@@ -182,15 +188,16 @@ export const RULES = [
|
|
|
182
188
|
scope: "summary",
|
|
183
189
|
severity: "fail",
|
|
184
190
|
check: matches(/^\*\*Last run\*\*:/m),
|
|
185
|
-
message: (
|
|
191
|
+
message: () => "Missing '**Last run**:' line",
|
|
192
|
+
hint: "add a '**Last run**: <date> — <one-line state>' line directly after the H1",
|
|
186
193
|
},
|
|
187
194
|
{
|
|
188
195
|
id: "summary.first-h2-inbox",
|
|
189
196
|
scope: "summary",
|
|
190
197
|
severity: "fail",
|
|
191
198
|
check: firstH2Is("Message Inbox"),
|
|
192
|
-
message: (
|
|
193
|
-
|
|
199
|
+
message: (_s, r) => `First H2 is '${r.observed}', expected 'Message Inbox'`,
|
|
200
|
+
hint: "move '## Message Inbox' to be the first H2 in the file",
|
|
194
201
|
},
|
|
195
202
|
{
|
|
196
203
|
id: "summary.memo-inbox-marker",
|
|
@@ -198,31 +205,32 @@ export const RULES = [
|
|
|
198
205
|
severity: "fail",
|
|
199
206
|
when: (s) => s.h2s.includes("Message Inbox"),
|
|
200
207
|
check: containsLine(MEMO_INBOX_MARKER),
|
|
201
|
-
message: (
|
|
208
|
+
message: () => `Missing ${MEMO_INBOX_MARKER} marker`,
|
|
209
|
+
hint: "add the marker directly below the '## Message Inbox' heading so `fit-wiki memo` can find it",
|
|
202
210
|
},
|
|
203
211
|
{
|
|
204
212
|
id: "summary.open-blockers-last",
|
|
205
213
|
scope: "summary",
|
|
206
214
|
severity: "fail",
|
|
207
215
|
check: nothingAfterH2("Open Blockers"),
|
|
208
|
-
message: (
|
|
209
|
-
|
|
216
|
+
message: (_s, r) => `'${r.observed}' appears after 'Open Blockers'`,
|
|
217
|
+
hint: "move '## Open Blockers' to the end of the file",
|
|
210
218
|
},
|
|
211
219
|
{
|
|
212
220
|
id: "summary.line-budget",
|
|
213
221
|
scope: "summary",
|
|
214
222
|
severity: "fail",
|
|
215
223
|
check: lineBudget(SUMMARY_LINE_BUDGET),
|
|
216
|
-
message: (
|
|
217
|
-
|
|
224
|
+
message: (_s, r) => `${r.value} lines (limit ${SUMMARY_LINE_BUDGET})`,
|
|
225
|
+
hint: "trim history into the weekly log; the summary holds settled state, not history",
|
|
218
226
|
},
|
|
219
227
|
{
|
|
220
228
|
id: "summary.word-budget",
|
|
221
229
|
scope: "summary",
|
|
222
230
|
severity: "fail",
|
|
223
231
|
check: wordBudget(SUMMARY_WORD_BUDGET),
|
|
224
|
-
message: (
|
|
225
|
-
|
|
232
|
+
message: (_s, r) => `${r.value} words (limit ${SUMMARY_WORD_BUDGET})`,
|
|
233
|
+
hint: "trim history into the weekly log; the summary holds settled state, not history",
|
|
226
234
|
},
|
|
227
235
|
{
|
|
228
236
|
id: "summary.h1-agent-matches-filename",
|
|
@@ -230,7 +238,8 @@ export const RULES = [
|
|
|
230
238
|
severity: "fail",
|
|
231
239
|
check: summaryAgentMismatch,
|
|
232
240
|
message: (s, r) =>
|
|
233
|
-
`
|
|
241
|
+
`H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
|
|
242
|
+
hint: "rename either the H1 ('# <agent> — Summary') or the file so they agree",
|
|
234
243
|
},
|
|
235
244
|
|
|
236
245
|
// -- Weekly logs (main) --
|
|
@@ -240,23 +249,24 @@ export const RULES = [
|
|
|
240
249
|
scope: "weekly-log-main",
|
|
241
250
|
severity: "fail",
|
|
242
251
|
check: firstLineMatches(WEEKLY_LOG_H1_RE),
|
|
243
|
-
message: (
|
|
252
|
+
message: () => "Missing valid H1 heading",
|
|
253
|
+
hint: "set the H1 to '# <agent> — YYYY-Www'",
|
|
244
254
|
},
|
|
245
255
|
{
|
|
246
256
|
id: "weekly-log.line-budget",
|
|
247
257
|
scope: "weekly-log-main",
|
|
248
258
|
severity: "fail",
|
|
249
259
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
250
|
-
message: (
|
|
251
|
-
|
|
260
|
+
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
261
|
+
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
252
262
|
},
|
|
253
263
|
{
|
|
254
264
|
id: "weekly-log.word-budget",
|
|
255
265
|
scope: "weekly-log-main",
|
|
256
266
|
severity: "fail",
|
|
257
267
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
258
|
-
message: (
|
|
259
|
-
|
|
268
|
+
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
269
|
+
hint: "run `bunx fit-wiki rotate` to seal this file as a sealed part and start a fresh weekly log",
|
|
260
270
|
},
|
|
261
271
|
{
|
|
262
272
|
id: "weekly-log.h1-agent-matches-filename",
|
|
@@ -264,7 +274,8 @@ export const RULES = [
|
|
|
264
274
|
severity: "fail",
|
|
265
275
|
check: weeklyAgentMismatch,
|
|
266
276
|
message: (s, r) =>
|
|
267
|
-
`
|
|
277
|
+
`H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
|
|
278
|
+
hint: "rename either the H1 or the file so they agree",
|
|
268
279
|
},
|
|
269
280
|
{
|
|
270
281
|
id: "decision-block.heading-within-5",
|
|
@@ -275,8 +286,8 @@ export const RULES = [
|
|
|
275
286
|
requiredLine: DECISION_HEADING,
|
|
276
287
|
stopRe: /^##\s/,
|
|
277
288
|
}),
|
|
278
|
-
message: (
|
|
279
|
-
|
|
289
|
+
message: () => "Entry lacks leading '### Decision'",
|
|
290
|
+
hint: "open each '## YYYY-MM-DD' entry with a '### Decision' subheading; use `bunx fit-wiki log decision` to do this mechanically",
|
|
280
291
|
},
|
|
281
292
|
|
|
282
293
|
// -- Weekly logs (sealed parts) --
|
|
@@ -286,23 +297,24 @@ export const RULES = [
|
|
|
286
297
|
scope: "weekly-log-part",
|
|
287
298
|
severity: "fail",
|
|
288
299
|
check: firstLineMatches(WEEKLY_LOG_H1_RE),
|
|
289
|
-
message: (
|
|
300
|
+
message: () => "Missing valid H1 heading",
|
|
301
|
+
hint: "set the H1 to '# <agent> — YYYY-Www (part N of M)'",
|
|
290
302
|
},
|
|
291
303
|
{
|
|
292
304
|
id: "weekly-log-part.line-budget",
|
|
293
305
|
scope: "weekly-log-part",
|
|
294
306
|
severity: "fail",
|
|
295
307
|
check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
|
|
296
|
-
message: (
|
|
297
|
-
|
|
308
|
+
message: (_s, r) => `${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
|
|
309
|
+
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
|
298
310
|
},
|
|
299
311
|
{
|
|
300
312
|
id: "weekly-log-part.word-budget",
|
|
301
313
|
scope: "weekly-log-part",
|
|
302
314
|
severity: "fail",
|
|
303
315
|
check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
|
|
304
|
-
message: (
|
|
305
|
-
|
|
316
|
+
message: (_s, r) => `${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
|
|
317
|
+
hint: "sealed parts should already be at-or-under the cap; if not, the rotation that produced this part needs investigation",
|
|
306
318
|
},
|
|
307
319
|
{
|
|
308
320
|
id: "weekly-log-part.h1-agent-matches-filename",
|
|
@@ -310,7 +322,8 @@ export const RULES = [
|
|
|
310
322
|
severity: "fail",
|
|
311
323
|
check: weeklyAgentMismatch,
|
|
312
324
|
message: (s, r) =>
|
|
313
|
-
`
|
|
325
|
+
`H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
|
|
326
|
+
hint: "rename either the H1 or the file so they agree",
|
|
314
327
|
},
|
|
315
328
|
|
|
316
329
|
// -- MEMORY.md --
|
|
@@ -320,7 +333,8 @@ export const RULES = [
|
|
|
320
333
|
scope: "memory",
|
|
321
334
|
severity: "fail",
|
|
322
335
|
check: exists,
|
|
323
|
-
message: (
|
|
336
|
+
message: () => "MEMORY.md not found",
|
|
337
|
+
hint: "run `bunx fit-wiki init` to scaffold the canonical sections",
|
|
324
338
|
},
|
|
325
339
|
{
|
|
326
340
|
id: "memory.priority-heading",
|
|
@@ -328,7 +342,8 @@ export const RULES = [
|
|
|
328
342
|
severity: "fail",
|
|
329
343
|
when: memoryExists,
|
|
330
344
|
check: matches(PRIORITY_INDEX_HEADING_RE),
|
|
331
|
-
message: () => `
|
|
345
|
+
message: () => `Missing '${PRIORITY_INDEX_HEADING}' heading`,
|
|
346
|
+
hint: "add the heading before the cross-cutting priorities table",
|
|
332
347
|
},
|
|
333
348
|
{
|
|
334
349
|
id: "memory.priority-table-header",
|
|
@@ -336,7 +351,8 @@ export const RULES = [
|
|
|
336
351
|
severity: "fail",
|
|
337
352
|
when: memoryExists,
|
|
338
353
|
check: matches(PRIORITY_HEADER_RE),
|
|
339
|
-
message: () => "
|
|
354
|
+
message: () => "Missing priority table header row",
|
|
355
|
+
hint: "add '| Item | Agents | Owner | Status | Added |' under the priority heading",
|
|
340
356
|
},
|
|
341
357
|
{
|
|
342
358
|
id: "memory.priority-separator-row",
|
|
@@ -344,8 +360,8 @@ export const RULES = [
|
|
|
344
360
|
severity: "fail",
|
|
345
361
|
when: memoryHasPriorityHeader,
|
|
346
362
|
check: matches(PRIORITY_SEPARATOR_RE),
|
|
347
|
-
message: () =>
|
|
348
|
-
|
|
363
|
+
message: () => "Missing priority table separator row",
|
|
364
|
+
hint: "add '| --- | --- | --- | --- | --- |' directly below the header row",
|
|
349
365
|
},
|
|
350
366
|
{
|
|
351
367
|
id: "memory.active-claims-table-header",
|
|
@@ -353,8 +369,8 @@ export const RULES = [
|
|
|
353
369
|
severity: "fail",
|
|
354
370
|
when: memoryHasClaimsHeading,
|
|
355
371
|
check: matches(CLAIMS_HEADER_RE),
|
|
356
|
-
message: () =>
|
|
357
|
-
|
|
372
|
+
message: () => `Active claims header mismatch`,
|
|
373
|
+
hint: `expected header row: '${ACTIVE_CLAIMS_TABLE_HEADER}'`,
|
|
358
374
|
},
|
|
359
375
|
{
|
|
360
376
|
id: "memory.active-claims-separator-row",
|
|
@@ -362,8 +378,8 @@ export const RULES = [
|
|
|
362
378
|
severity: "fail",
|
|
363
379
|
when: memoryHasClaimsHeader,
|
|
364
380
|
check: matches(CLAIMS_SEPARATOR_RE),
|
|
365
|
-
message: () =>
|
|
366
|
-
|
|
381
|
+
message: () => "Missing active-claims separator row",
|
|
382
|
+
hint: "add '| --- | --- | --- | --- | --- | --- |' directly below the claims header",
|
|
367
383
|
},
|
|
368
384
|
|
|
369
385
|
// -- Table rows --
|
|
@@ -373,32 +389,32 @@ export const RULES = [
|
|
|
373
389
|
scope: "priority-row",
|
|
374
390
|
severity: "fail",
|
|
375
391
|
check: columnCount(5),
|
|
376
|
-
message: (
|
|
377
|
-
|
|
392
|
+
message: (_s, r) => `${r.actual} cells (expected ${r.expected})`,
|
|
393
|
+
hint: "every priority row needs 5 cells: Item, Agents, Owner, Status, Added",
|
|
378
394
|
},
|
|
379
395
|
{
|
|
380
396
|
id: "claims-row.claimed-at-format",
|
|
381
397
|
scope: "claims-row",
|
|
382
398
|
severity: "fail",
|
|
383
399
|
check: fieldMatches("claimed_at", ISO_DATE_RE),
|
|
384
|
-
message: (s, r) =>
|
|
385
|
-
|
|
400
|
+
message: (s, r) => `Bad claimed_at '${r.value}' for ${s.agent}/${s.target}`,
|
|
401
|
+
hint: "claimed_at must be ISO YYYY-MM-DD",
|
|
386
402
|
},
|
|
387
403
|
{
|
|
388
404
|
id: "claims-row.expires-at-format",
|
|
389
405
|
scope: "claims-row",
|
|
390
406
|
severity: "fail",
|
|
391
407
|
check: fieldMatches("expires_at", ISO_DATE_RE),
|
|
392
|
-
message: (s, r) =>
|
|
393
|
-
|
|
408
|
+
message: (s, r) => `Bad expires_at '${r.value}' for ${s.agent}/${s.target}`,
|
|
409
|
+
hint: "expires_at must be ISO YYYY-MM-DD",
|
|
394
410
|
},
|
|
395
411
|
{
|
|
396
412
|
id: "expired-claim",
|
|
397
413
|
scope: "claims-row",
|
|
398
414
|
severity: "warn",
|
|
399
415
|
check: expired,
|
|
400
|
-
message: (s) =>
|
|
401
|
-
|
|
416
|
+
message: (s) => `${s.agent}/${s.target} expired ${s.expires_at}`,
|
|
417
|
+
hint: "run `bunx fit-wiki release --expired` to clear expired claims",
|
|
402
418
|
},
|
|
403
419
|
|
|
404
420
|
// -- Storyboards --
|
|
@@ -408,8 +424,8 @@ export const RULES = [
|
|
|
408
424
|
scope: "storyboard",
|
|
409
425
|
severity: "fail",
|
|
410
426
|
check: exists,
|
|
411
|
-
message: (s) =>
|
|
412
|
-
|
|
427
|
+
message: (s) => `Current-month storyboard (${s.yearMonth}) not found`,
|
|
428
|
+
hint: "create it from `.claude/skills/kata-session/references/storyboard-template.md`",
|
|
413
429
|
},
|
|
414
430
|
{
|
|
415
431
|
id: "storyboard.agent-h3-required",
|
|
@@ -417,7 +433,26 @@ export const RULES = [
|
|
|
417
433
|
severity: "fail",
|
|
418
434
|
when: storyboardExists,
|
|
419
435
|
check: allRequiredLines(AGENT_H3_REQUIREMENTS),
|
|
420
|
-
message: (
|
|
436
|
+
message: (_s, r) => `Missing '### ${r.label}' H3`,
|
|
437
|
+
hint: "every domain agent gets an H3 under '## Current Condition'",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: "storyboard.line-budget",
|
|
441
|
+
scope: "storyboard",
|
|
442
|
+
severity: "fail",
|
|
443
|
+
when: storyboardExists,
|
|
444
|
+
check: lineBudget(STORYBOARD_LINE_BUDGET),
|
|
445
|
+
message: (_s, r) => `${r.value} lines (limit ${STORYBOARD_LINE_BUDGET})`,
|
|
446
|
+
hint: "see per-section word budgets in storyboard-template.md; retire prior-session Headlines/Notes/Next-review entries to weekly logs",
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "storyboard.word-budget",
|
|
450
|
+
scope: "storyboard",
|
|
451
|
+
severity: "fail",
|
|
452
|
+
when: storyboardExists,
|
|
453
|
+
check: wordBudget(STORYBOARD_WORD_BUDGET),
|
|
454
|
+
message: (_s, r) => `${r.value} words (limit ${STORYBOARD_WORD_BUDGET})`,
|
|
455
|
+
hint: "see per-section word budgets in storyboard-template.md; retire prior-session Headlines/Notes/Next-review entries to weekly logs",
|
|
421
456
|
},
|
|
422
457
|
{
|
|
423
458
|
id: "storyboard.markers-balanced.xmr",
|
|
@@ -429,8 +464,9 @@ export const RULES = [
|
|
|
429
464
|
closeRe: XMR_CLOSE_RE,
|
|
430
465
|
label: "xmr",
|
|
431
466
|
}),
|
|
432
|
-
message: (
|
|
433
|
-
|
|
467
|
+
message: (_s, r) =>
|
|
468
|
+
`${r.reason} xmr marker${r.label ? ` (${r.label})` : ""}`,
|
|
469
|
+
hint: "every '<!-- xmr:metric:csv -->' needs a matching '<!-- /xmr -->'",
|
|
434
470
|
},
|
|
435
471
|
{
|
|
436
472
|
id: "storyboard.markers-balanced.issues",
|
|
@@ -442,8 +478,9 @@ export const RULES = [
|
|
|
442
478
|
closeRe: ISSUE_CLOSE_RE,
|
|
443
479
|
label: "issue-list",
|
|
444
480
|
}),
|
|
445
|
-
message: (
|
|
446
|
-
|
|
481
|
+
message: (_s, r) =>
|
|
482
|
+
`${r.reason} issue-list marker${r.label ? ` (${r.label})` : ""}`,
|
|
483
|
+
hint: "every '<!-- obstacles:* -->' or '<!-- experiments:* -->' needs a matching close marker",
|
|
447
484
|
},
|
|
448
485
|
|
|
449
486
|
// -- Stray files --
|
|
@@ -453,7 +490,7 @@ export const RULES = [
|
|
|
453
490
|
scope: "stray-file",
|
|
454
491
|
severity: "fail",
|
|
455
492
|
check: always,
|
|
456
|
-
message: (
|
|
457
|
-
|
|
493
|
+
message: () => "Does not match any known scope",
|
|
494
|
+
hint: "rename to a recognized scope (summary, weekly log, weekly-log part) or remove the file",
|
|
458
495
|
},
|
|
459
496
|
];
|
package/src/audit/scopes.js
CHANGED
|
@@ -107,6 +107,8 @@ function loadStoryboard(wikiRoot, today) {
|
|
|
107
107
|
fileLines: text.split("\n"),
|
|
108
108
|
exists,
|
|
109
109
|
yearMonth: `${yyyy}-M${mm}`,
|
|
110
|
+
lines: countLines(text),
|
|
111
|
+
words: countWords(text),
|
|
110
112
|
};
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -159,8 +161,12 @@ const SCOPE_RESOLVERS = {
|
|
|
159
161
|
"weekly-log-part": (ctx) => ctx.subjects["weekly-log-part"],
|
|
160
162
|
memory: (ctx) => [ctx.memory],
|
|
161
163
|
"claims-row": (ctx) =>
|
|
162
|
-
parseClaims(ctx.memory.text).map((c) => ({ path:
|
|
163
|
-
"priority-row": (ctx) =>
|
|
164
|
+
parseClaims(ctx.memory.text).map((c) => ({ ...c, path: ctx.memory.path })),
|
|
165
|
+
"priority-row": (ctx) =>
|
|
166
|
+
parsePriorityRows(ctx.memory.text).map((r) => ({
|
|
167
|
+
...r,
|
|
168
|
+
path: ctx.memory.path,
|
|
169
|
+
})),
|
|
164
170
|
storyboard: (ctx) => [ctx.storyboard],
|
|
165
171
|
"stray-file": (ctx) => ctx.subjects.stray,
|
|
166
172
|
};
|
package/src/block-renderer.js
CHANGED
|
@@ -32,11 +32,8 @@ export function renderBlock({
|
|
|
32
32
|
throw new BlockRenderError(`metric-not-found: ${metric}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const latestValue = m.latest?.value ?? m.values[m.values.length - 1] ?? "—";
|
|
36
|
-
const status = m.status;
|
|
37
|
-
|
|
38
35
|
let chartLines;
|
|
39
|
-
if (status === "insufficient_data") {
|
|
36
|
+
if (m.status === "insufficient_data") {
|
|
40
37
|
chartLines = [
|
|
41
38
|
`Insufficient data: ${m.n} points (need at least ${MIN_POINTS}).`,
|
|
42
39
|
];
|
|
@@ -45,16 +42,12 @@ export function renderBlock({
|
|
|
45
42
|
chartLines = chartText.split("\n");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
const signalLine = formatSignals(m.signals);
|
|
49
|
-
|
|
50
45
|
return [
|
|
51
|
-
`**Latest:** ${latestValue} · **Status:** ${status}`,
|
|
52
|
-
"",
|
|
53
46
|
"```",
|
|
54
47
|
...chartLines,
|
|
55
48
|
"```",
|
|
56
49
|
"",
|
|
57
|
-
`**Signals:** ${
|
|
50
|
+
`**Signals:** ${formatSignals(m.signals)}`,
|
|
58
51
|
];
|
|
59
52
|
}
|
|
60
53
|
|
package/src/commands/audit.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import fsAsync from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Finder,
|
|
5
|
+
emitFindingsJson,
|
|
6
|
+
emitFindingsText,
|
|
7
|
+
runRules,
|
|
8
|
+
} from "@forwardimpact/libutil";
|
|
5
9
|
import { RULES } from "../audit/rules.js";
|
|
6
|
-
import { buildContext } from "../audit/scopes.js";
|
|
7
|
-
import { emitJson, emitText } from "../audit/format.js";
|
|
10
|
+
import { buildContext, resolveScope } from "../audit/scopes.js";
|
|
8
11
|
|
|
9
12
|
/** Run the wiki audit and emit findings. JSON via --format json. */
|
|
10
13
|
export function runAuditCommand(values, _args, _cli) {
|
|
@@ -14,10 +17,15 @@ export function runAuditCommand(values, _args, _cli) {
|
|
|
14
17
|
const today = values.today || new Date().toISOString().slice(0, 10);
|
|
15
18
|
|
|
16
19
|
const ctx = buildContext({ wikiRoot, today });
|
|
17
|
-
const findings =
|
|
20
|
+
const findings = runRules(RULES, ctx, { resolveScope });
|
|
18
21
|
|
|
19
22
|
process.stdout.write(
|
|
20
|
-
values.format === "json"
|
|
23
|
+
values.format === "json"
|
|
24
|
+
? emitFindingsJson(findings)
|
|
25
|
+
: emitFindingsText(findings, {
|
|
26
|
+
cwd: projectRoot,
|
|
27
|
+
passMessage: "wiki audit passed",
|
|
28
|
+
}),
|
|
21
29
|
);
|
|
22
30
|
|
|
23
31
|
if (findings.some((f) => f.level === "fail")) process.exit(1);
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import fsAsync from "node:fs/promises";
|
|
4
5
|
import { Finder } from "@forwardimpact/libutil";
|
|
6
|
+
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
5
7
|
import { scanMarkers } from "../marker-scanner.js";
|
|
6
8
|
import { renderBlock, BlockRenderError } from "../block-renderer.js";
|
|
7
|
-
import { renderIssueList } from "../issue-list-renderer.js";
|
|
9
|
+
import { renderIssueList, parseRepoSlug } from "../issue-list-renderer.js";
|
|
8
10
|
|
|
9
11
|
function currentStoryboardPath() {
|
|
10
12
|
const now = new Date();
|
|
@@ -13,7 +15,17 @@ function currentStoryboardPath() {
|
|
|
13
15
|
return `wiki/storyboard-${yyyy}-M${mm}.md`;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
function
|
|
18
|
+
function deriveParentRepo(parentDir) {
|
|
19
|
+
if (process.env.FIT_GH_REPO) return process.env.FIT_GH_REPO;
|
|
20
|
+
const r = spawnSync("git", ["-C", parentDir, "remote", "get-url", "origin"], {
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
stdio: "pipe",
|
|
23
|
+
});
|
|
24
|
+
if (r.status !== 0) return null;
|
|
25
|
+
return parseRepoSlug(r.stdout);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function renderForBlock(block, projectRoot, ghContext) {
|
|
17
29
|
if (block.kind === "xmr") {
|
|
18
30
|
return renderBlock({
|
|
19
31
|
metric: block.metric,
|
|
@@ -26,6 +38,9 @@ function renderForBlock(block, projectRoot) {
|
|
|
26
38
|
topic: block.topic,
|
|
27
39
|
state: block.state,
|
|
28
40
|
window: block.window,
|
|
41
|
+
cwd: ghContext.cwd,
|
|
42
|
+
repo: ghContext.repo,
|
|
43
|
+
token: ghContext.token,
|
|
29
44
|
});
|
|
30
45
|
}
|
|
31
46
|
return null;
|
|
@@ -40,7 +55,7 @@ function spliceBlock(lines, block, rendered) {
|
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/** Re-render XmR chart blocks and issue-list blocks in a storyboard file. */
|
|
43
|
-
export function runRefreshCommand(values, args, _cli) {
|
|
58
|
+
export async function runRefreshCommand(values, args, _cli) {
|
|
44
59
|
const logger = { debug() {} };
|
|
45
60
|
const finder = new Finder(fsAsync, logger, process);
|
|
46
61
|
const projectRoot = finder.findProjectRoot(process.cwd());
|
|
@@ -53,13 +68,33 @@ export function runRefreshCommand(values, args, _cli) {
|
|
|
53
68
|
const blocks = scanMarkers(text);
|
|
54
69
|
if (blocks.length === 0) return;
|
|
55
70
|
|
|
71
|
+
const config = await createScriptConfig("wiki");
|
|
72
|
+
let token = null;
|
|
73
|
+
try {
|
|
74
|
+
token = config.ghToken();
|
|
75
|
+
} catch {
|
|
76
|
+
// Missing token is non-fatal; issue-list renders will fail with a stderr
|
|
77
|
+
// warning and the block will collapse to the notice line.
|
|
78
|
+
}
|
|
79
|
+
// Spawn `gh` from the project root so it resolves the monorepo's origin
|
|
80
|
+
// instead of whatever git context the caller's cwd happens to be in (the
|
|
81
|
+
// wiki sibling repo, a subagent worktree, a service dir, etc.). Also
|
|
82
|
+
// resolve an explicit owner/repo slug so `gh` works when origin has been
|
|
83
|
+
// rewritten to a proxy URL (sandbox environments) — `FIT_GH_REPO` env
|
|
84
|
+
// overrides the parsed origin.
|
|
85
|
+
const ghContext = {
|
|
86
|
+
cwd: projectRoot,
|
|
87
|
+
repo: deriveParentRepo(projectRoot),
|
|
88
|
+
token,
|
|
89
|
+
};
|
|
90
|
+
|
|
56
91
|
const lines = text.split("\n");
|
|
57
92
|
let spliced = false;
|
|
58
93
|
|
|
59
94
|
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
60
95
|
const block = blocks[i];
|
|
61
96
|
try {
|
|
62
|
-
const rendered = renderForBlock(block, projectRoot);
|
|
97
|
+
const rendered = renderForBlock(block, projectRoot, ghContext);
|
|
63
98
|
if (!rendered) continue;
|
|
64
99
|
spliceBlock(lines, block, rendered);
|
|
65
100
|
spliced = true;
|
package/src/constants.js
CHANGED
|
@@ -13,10 +13,14 @@ export const PRIORITY_INDEX_TABLE_HEADER =
|
|
|
13
13
|
"| Item | Agents | Owner | Status | Added |";
|
|
14
14
|
export const DECISION_HEADING = "### Decision";
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// Unified budgets for the three audited surfaces (summary, weekly-log,
|
|
17
|
+
// storyboard). They share the same numeric limits today so the
|
|
18
|
+
// context-tax floor is symmetric across surfaces; each surface keeps
|
|
19
|
+
// its own audit rule pair so the limits can diverge later if the
|
|
20
|
+
// context-tax model says one surface should be looser or tighter.
|
|
21
|
+
export const SUMMARY_LINE_BUDGET = 496;
|
|
22
|
+
export const SUMMARY_WORD_BUDGET = 2048;
|
|
19
23
|
export const WEEKLY_LOG_LINE_BUDGET = 496;
|
|
20
|
-
export const SUMMARY_LINE_BUDGET = 72;
|
|
21
24
|
export const WEEKLY_LOG_WORD_BUDGET = 6400;
|
|
22
|
-
export const
|
|
25
|
+
export const STORYBOARD_LINE_BUDGET = 496;
|
|
26
|
+
export const STORYBOARD_WORD_BUDGET = 6400;
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
function defaultGh(args) {
|
|
3
|
+
function defaultGh(args, options) {
|
|
4
|
+
const env = options?.token
|
|
5
|
+
? { ...process.env, GH_TOKEN: options.token }
|
|
6
|
+
: undefined;
|
|
4
7
|
return spawnSync("gh", args, {
|
|
5
8
|
encoding: "utf-8",
|
|
6
9
|
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
cwd: options?.cwd,
|
|
11
|
+
env,
|
|
7
12
|
});
|
|
8
13
|
}
|
|
9
14
|
|
|
@@ -13,18 +18,30 @@ function daysAgo(today, n) {
|
|
|
13
18
|
return d.toISOString().slice(0, 10);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
/**
|
|
21
|
+
/** Parse `owner/repo` from a git origin URL. Tolerates http(s), ssh, and proxy-rewritten URLs (e.g. `http://host/git/owner/repo`) by taking the last two path segments after stripping `.git`. Returns null when nothing parseable is found. */
|
|
22
|
+
export function parseRepoSlug(originUrl) {
|
|
23
|
+
if (!originUrl) return null;
|
|
24
|
+
const stripped = originUrl.trim().replace(/\.git$/, "");
|
|
25
|
+
const match = stripped.match(/([^/:]+)\/([^/:]+)$/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
return `${match[1]}/${match[2]}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render an issue-list block for an obstacles/experiments marker. Returns markdown lines. `cwd` should be the parent monorepo's project root so `gh` resolves the correct origin; `repo` is an explicit `owner/name` slug used when the origin remote is unparseable by `gh` (e.g. sandbox proxy URLs); `token` is the resolved GH token (e.g. via `Config.ghToken()`). */
|
|
17
31
|
export function renderIssueList({
|
|
18
32
|
topic,
|
|
19
33
|
state,
|
|
20
34
|
window,
|
|
35
|
+
cwd,
|
|
36
|
+
repo,
|
|
37
|
+
token,
|
|
21
38
|
today = new Date(),
|
|
22
39
|
gh = defaultGh,
|
|
23
40
|
}) {
|
|
24
41
|
const ghState = state === "closed" ? "closed" : "open";
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
const args = ["issue", "list"];
|
|
43
|
+
if (repo) args.push("--repo", repo);
|
|
44
|
+
args.push(
|
|
28
45
|
"--label",
|
|
29
46
|
topic.replace(/s$/, ""),
|
|
30
47
|
"--state",
|
|
@@ -33,7 +50,8 @@ export function renderIssueList({
|
|
|
33
50
|
"number,title,labels,closedAt",
|
|
34
51
|
"--limit",
|
|
35
52
|
"100",
|
|
36
|
-
|
|
53
|
+
);
|
|
54
|
+
const result = gh(args, { cwd, token });
|
|
37
55
|
if (result.status !== 0) {
|
|
38
56
|
process.stderr.write(
|
|
39
57
|
`refresh: gh issue list failed for ${topic}:${state}\n`,
|
|
@@ -62,8 +80,7 @@ export function renderIssueList({
|
|
|
62
80
|
|
|
63
81
|
const lines = [];
|
|
64
82
|
for (const issue of issues) {
|
|
65
|
-
|
|
66
|
-
lines.push(`- **${tag} #${issue.number} — ${issue.title}**`);
|
|
83
|
+
lines.push(`- #${issue.number} ${issue.title}`);
|
|
67
84
|
}
|
|
68
85
|
return lines;
|
|
69
86
|
}
|
package/src/marker-scanner.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Markers tolerate optional trailing text after the tag (typically an inline
|
|
2
|
+
// "Do not edit. Generated from fit-wiki refresh." notice), so an open or close
|
|
3
|
+
// marker can carry its own warning without needing a separate notice line.
|
|
4
|
+
const XMR_OPEN_RE = /^<!--\s*xmr:([^:\s]+):(\S+)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
2
5
|
const ISSUE_OPEN_RE =
|
|
3
|
-
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?\s*-->\s*$/;
|
|
4
|
-
const XMR_CLOSE_RE = /^<!--\s*\/xmr
|
|
5
|
-
const ISSUE_CLOSE_RE =
|
|
6
|
+
/^<!--\s*(obstacles|experiments):(open|closed)(?::(\d+d))?(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
7
|
+
const XMR_CLOSE_RE = /^<!--\s*\/xmr(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
8
|
+
const ISSUE_CLOSE_RE =
|
|
9
|
+
/^<!--\s*\/(obstacles|experiments)(?:\s+[^>]*?)?\s*-->\s*$/;
|
|
6
10
|
|
|
7
11
|
function openLabel(open) {
|
|
8
12
|
return open.kind === "xmr" ? open.metric : open.topic;
|
package/src/audit/engine.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { resolveScope } from "./scopes.js";
|
|
2
|
-
|
|
3
|
-
function groupByScope(rules) {
|
|
4
|
-
const groups = new Map();
|
|
5
|
-
for (const rule of rules) {
|
|
6
|
-
if (!groups.has(rule.scope)) groups.set(rule.scope, []);
|
|
7
|
-
groups.get(rule.scope).push(rule);
|
|
8
|
-
}
|
|
9
|
-
return groups;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function applyRule(rule, subject, ctx) {
|
|
13
|
-
if (rule.when && !rule.when(subject, ctx)) return [];
|
|
14
|
-
const result = rule.check(subject, ctx);
|
|
15
|
-
if (result == null) return [];
|
|
16
|
-
const items = Array.isArray(result) ? result : [result];
|
|
17
|
-
return items.map((item) => ({
|
|
18
|
-
id: rule.id,
|
|
19
|
-
level: rule.severity,
|
|
20
|
-
path: subject.path ?? null,
|
|
21
|
-
message: rule.message(subject, item, ctx),
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Apply the declarative rule catalogue to the wiki context. */
|
|
26
|
-
export function runAudit(rules, ctx) {
|
|
27
|
-
const findings = [];
|
|
28
|
-
for (const [scopeKey, scopeRules] of groupByScope(rules)) {
|
|
29
|
-
for (const subject of resolveScope(scopeKey, ctx)) {
|
|
30
|
-
for (const rule of scopeRules) {
|
|
31
|
-
findings.push(...applyRule(rule, subject, ctx));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return findings;
|
|
36
|
-
}
|
package/src/audit/format.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
function partition(findings) {
|
|
2
|
-
const failures = [];
|
|
3
|
-
const warnings = [];
|
|
4
|
-
for (const f of findings) {
|
|
5
|
-
if (f.level === "warn") warnings.push(f);
|
|
6
|
-
else failures.push(f);
|
|
7
|
-
}
|
|
8
|
-
return { failures, warnings };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Render findings as `WARN`/`FAIL` lines followed by a `RESULT:` trailer. */
|
|
12
|
-
export function emitText(findings) {
|
|
13
|
-
const { failures, warnings } = partition(findings);
|
|
14
|
-
const lines = [];
|
|
15
|
-
for (const w of warnings) lines.push(`WARN ${w.message}`);
|
|
16
|
-
for (const f of failures) lines.push(`FAIL ${f.message}`);
|
|
17
|
-
lines.push(
|
|
18
|
-
failures.length === 0
|
|
19
|
-
? "RESULT: pass"
|
|
20
|
-
: `RESULT: fail (${failures.length} checks failed)`,
|
|
21
|
-
);
|
|
22
|
-
return lines.join("\n") + "\n";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Render findings as a JSON document. */
|
|
26
|
-
export function emitJson(findings) {
|
|
27
|
-
const { failures, warnings } = partition(findings);
|
|
28
|
-
return (
|
|
29
|
-
JSON.stringify(
|
|
30
|
-
{
|
|
31
|
-
result: failures.length === 0 ? "pass" : "fail",
|
|
32
|
-
failures,
|
|
33
|
-
warnings,
|
|
34
|
-
},
|
|
35
|
-
null,
|
|
36
|
-
2,
|
|
37
|
-
) + "\n"
|
|
38
|
-
);
|
|
39
|
-
}
|