@forwardimpact/libwiki 0.2.7 → 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 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-1060 --branch claude/spec-1060
57
- npx fit-wiki release --agent X --target spec-1060
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-1060 --branch claude/...",
226
- "fit-wiki release --agent staff-engineer --target spec-1060",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libwiki",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Wiki lifecycle primitives — stable memory for agent teams so coordination persists across sessions.",
5
5
  "keywords": [
6
6
  "wiki",
@@ -188,15 +188,16 @@ export const RULES = [
188
188
  scope: "summary",
189
189
  severity: "fail",
190
190
  check: matches(/^\*\*Last run\*\*:/m),
191
- message: (s) => `sections: ${s.path} missing '**Last run**:' line`,
191
+ message: () => "Missing '**Last run**:' line",
192
+ hint: "add a '**Last run**: <date> — <one-line state>' line directly after the H1",
192
193
  },
193
194
  {
194
195
  id: "summary.first-h2-inbox",
195
196
  scope: "summary",
196
197
  severity: "fail",
197
198
  check: firstH2Is("Message Inbox"),
198
- message: (s, r) =>
199
- `sections: ${s.path} first H2 is '${r.observed}', expected 'Message Inbox'`,
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",
200
201
  },
201
202
  {
202
203
  id: "summary.memo-inbox-marker",
@@ -204,31 +205,32 @@ export const RULES = [
204
205
  severity: "fail",
205
206
  when: (s) => s.h2s.includes("Message Inbox"),
206
207
  check: containsLine(MEMO_INBOX_MARKER),
207
- message: (s) => `sections: ${s.path} missing <!-- memo:inbox --> marker`,
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",
208
210
  },
209
211
  {
210
212
  id: "summary.open-blockers-last",
211
213
  scope: "summary",
212
214
  severity: "fail",
213
215
  check: nothingAfterH2("Open Blockers"),
214
- message: (s, r) =>
215
- `sections: ${s.path} '${r.observed}' appears after 'Open Blockers'`,
216
+ message: (_s, r) => `'${r.observed}' appears after 'Open Blockers'`,
217
+ hint: "move '## Open Blockers' to the end of the file",
216
218
  },
217
219
  {
218
220
  id: "summary.line-budget",
219
221
  scope: "summary",
220
222
  severity: "fail",
221
223
  check: lineBudget(SUMMARY_LINE_BUDGET),
222
- message: (s, r) =>
223
- `budget: ${s.path} has ${r.value} lines (limit ${SUMMARY_LINE_BUDGET})`,
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",
224
226
  },
225
227
  {
226
228
  id: "summary.word-budget",
227
229
  scope: "summary",
228
230
  severity: "fail",
229
231
  check: wordBudget(SUMMARY_WORD_BUDGET),
230
- message: (s, r) =>
231
- `budget: ${s.path} has ${r.value} words (limit ${SUMMARY_WORD_BUDGET})`,
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",
232
234
  },
233
235
  {
234
236
  id: "summary.h1-agent-matches-filename",
@@ -236,7 +238,8 @@ export const RULES = [
236
238
  severity: "fail",
237
239
  check: summaryAgentMismatch,
238
240
  message: (s, r) =>
239
- `sections: ${s.path} H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
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",
240
243
  },
241
244
 
242
245
  // -- Weekly logs (main) --
@@ -246,23 +249,24 @@ export const RULES = [
246
249
  scope: "weekly-log-main",
247
250
  severity: "fail",
248
251
  check: firstLineMatches(WEEKLY_LOG_H1_RE),
249
- message: (s) => `weekly-log: ${s.path} missing valid H1 heading`,
252
+ message: () => "Missing valid H1 heading",
253
+ hint: "set the H1 to '# <agent> — YYYY-Www'",
250
254
  },
251
255
  {
252
256
  id: "weekly-log.line-budget",
253
257
  scope: "weekly-log-main",
254
258
  severity: "fail",
255
259
  check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
256
- message: (s, r) =>
257
- `weekly-log: ${s.path} has ${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
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",
258
262
  },
259
263
  {
260
264
  id: "weekly-log.word-budget",
261
265
  scope: "weekly-log-main",
262
266
  severity: "fail",
263
267
  check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
264
- message: (s, r) =>
265
- `weekly-log: ${s.path} has ${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
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",
266
270
  },
267
271
  {
268
272
  id: "weekly-log.h1-agent-matches-filename",
@@ -270,7 +274,8 @@ export const RULES = [
270
274
  severity: "fail",
271
275
  check: weeklyAgentMismatch,
272
276
  message: (s, r) =>
273
- `weekly-log: ${s.path} H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
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",
274
279
  },
275
280
  {
276
281
  id: "decision-block.heading-within-5",
@@ -281,8 +286,8 @@ export const RULES = [
281
286
  requiredLine: DECISION_HEADING,
282
287
  stopRe: /^##\s/,
283
288
  }),
284
- message: (s, r) =>
285
- `decision-block: ${s.path}:${r.lineNo} entry lacks leading '### Decision'`,
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",
286
291
  },
287
292
 
288
293
  // -- Weekly logs (sealed parts) --
@@ -292,23 +297,24 @@ export const RULES = [
292
297
  scope: "weekly-log-part",
293
298
  severity: "fail",
294
299
  check: firstLineMatches(WEEKLY_LOG_H1_RE),
295
- message: (s) => `weekly-log: ${s.path} missing valid H1 heading`,
300
+ message: () => "Missing valid H1 heading",
301
+ hint: "set the H1 to '# <agent> — YYYY-Www (part N of M)'",
296
302
  },
297
303
  {
298
304
  id: "weekly-log-part.line-budget",
299
305
  scope: "weekly-log-part",
300
306
  severity: "fail",
301
307
  check: lineBudget(WEEKLY_LOG_LINE_BUDGET),
302
- message: (s, r) =>
303
- `weekly-log: ${s.path} has ${r.value} lines (limit ${WEEKLY_LOG_LINE_BUDGET})`,
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",
304
310
  },
305
311
  {
306
312
  id: "weekly-log-part.word-budget",
307
313
  scope: "weekly-log-part",
308
314
  severity: "fail",
309
315
  check: wordBudget(WEEKLY_LOG_WORD_BUDGET),
310
- message: (s, r) =>
311
- `weekly-log: ${s.path} has ${r.value} words (limit ${WEEKLY_LOG_WORD_BUDGET})`,
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",
312
318
  },
313
319
  {
314
320
  id: "weekly-log-part.h1-agent-matches-filename",
@@ -316,7 +322,8 @@ export const RULES = [
316
322
  severity: "fail",
317
323
  check: weeklyAgentMismatch,
318
324
  message: (s, r) =>
319
- `weekly-log: ${s.path} H1 title slug '${r.titleSlug}' does not match filename prefix '${s.agentPrefix}'`,
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",
320
327
  },
321
328
 
322
329
  // -- MEMORY.md --
@@ -326,7 +333,8 @@ export const RULES = [
326
333
  scope: "memory",
327
334
  severity: "fail",
328
335
  check: exists,
329
- message: (s) => `memory: ${s.path} not found`,
336
+ message: () => "MEMORY.md not found",
337
+ hint: "run `bunx fit-wiki init` to scaffold the canonical sections",
330
338
  },
331
339
  {
332
340
  id: "memory.priority-heading",
@@ -334,7 +342,8 @@ export const RULES = [
334
342
  severity: "fail",
335
343
  when: memoryExists,
336
344
  check: matches(PRIORITY_INDEX_HEADING_RE),
337
- message: () => `memory: missing '${PRIORITY_INDEX_HEADING}' heading`,
345
+ message: () => `Missing '${PRIORITY_INDEX_HEADING}' heading`,
346
+ hint: "add the heading before the cross-cutting priorities table",
338
347
  },
339
348
  {
340
349
  id: "memory.priority-table-header",
@@ -342,7 +351,8 @@ export const RULES = [
342
351
  severity: "fail",
343
352
  when: memoryExists,
344
353
  check: matches(PRIORITY_HEADER_RE),
345
- message: () => "memory: missing priority table header row",
354
+ message: () => "Missing priority table header row",
355
+ hint: "add '| Item | Agents | Owner | Status | Added |' under the priority heading",
346
356
  },
347
357
  {
348
358
  id: "memory.priority-separator-row",
@@ -350,8 +360,8 @@ export const RULES = [
350
360
  severity: "fail",
351
361
  when: memoryHasPriorityHeader,
352
362
  check: matches(PRIORITY_SEPARATOR_RE),
353
- message: () =>
354
- "memory: missing priority table separator row (| --- | --- | --- | --- | --- |)",
363
+ message: () => "Missing priority table separator row",
364
+ hint: "add '| --- | --- | --- | --- | --- |' directly below the header row",
355
365
  },
356
366
  {
357
367
  id: "memory.active-claims-table-header",
@@ -359,8 +369,8 @@ export const RULES = [
359
369
  severity: "fail",
360
370
  when: memoryHasClaimsHeading,
361
371
  check: matches(CLAIMS_HEADER_RE),
362
- message: () =>
363
- `active-claims: header mismatch (expected ${ACTIVE_CLAIMS_TABLE_HEADER})`,
372
+ message: () => `Active claims header mismatch`,
373
+ hint: `expected header row: '${ACTIVE_CLAIMS_TABLE_HEADER}'`,
364
374
  },
365
375
  {
366
376
  id: "memory.active-claims-separator-row",
@@ -368,8 +378,8 @@ export const RULES = [
368
378
  severity: "fail",
369
379
  when: memoryHasClaimsHeader,
370
380
  check: matches(CLAIMS_SEPARATOR_RE),
371
- message: () =>
372
- "active-claims: missing separator row (| --- | --- | --- | --- | --- | --- |)",
381
+ message: () => "Missing active-claims separator row",
382
+ hint: "add '| --- | --- | --- | --- | --- | --- |' directly below the claims header",
373
383
  },
374
384
 
375
385
  // -- Table rows --
@@ -379,32 +389,32 @@ export const RULES = [
379
389
  scope: "priority-row",
380
390
  severity: "fail",
381
391
  check: columnCount(5),
382
- message: (s, r) =>
383
- `priority-row: row at line ${s.lineNo} has ${r.actual} cells (expected ${r.expected})`,
392
+ message: (_s, r) => `${r.actual} cells (expected ${r.expected})`,
393
+ hint: "every priority row needs 5 cells: Item, Agents, Owner, Status, Added",
384
394
  },
385
395
  {
386
396
  id: "claims-row.claimed-at-format",
387
397
  scope: "claims-row",
388
398
  severity: "fail",
389
399
  check: fieldMatches("claimed_at", ISO_DATE_RE),
390
- message: (s, r) =>
391
- `active-claims: bad claimed_at '${r.value}' for ${s.agent}/${s.target}`,
400
+ message: (s, r) => `Bad claimed_at '${r.value}' for ${s.agent}/${s.target}`,
401
+ hint: "claimed_at must be ISO YYYY-MM-DD",
392
402
  },
393
403
  {
394
404
  id: "claims-row.expires-at-format",
395
405
  scope: "claims-row",
396
406
  severity: "fail",
397
407
  check: fieldMatches("expires_at", ISO_DATE_RE),
398
- message: (s, r) =>
399
- `active-claims: bad expires_at '${r.value}' for ${s.agent}/${s.target}`,
408
+ message: (s, r) => `Bad expires_at '${r.value}' for ${s.agent}/${s.target}`,
409
+ hint: "expires_at must be ISO YYYY-MM-DD",
400
410
  },
401
411
  {
402
412
  id: "expired-claim",
403
413
  scope: "claims-row",
404
414
  severity: "warn",
405
415
  check: expired,
406
- message: (s) =>
407
- `expired-claim: ${s.agent}/${s.target} expired ${s.expires_at}`,
416
+ message: (s) => `${s.agent}/${s.target} expired ${s.expires_at}`,
417
+ hint: "run `bunx fit-wiki release --expired` to clear expired claims",
408
418
  },
409
419
 
410
420
  // -- Storyboards --
@@ -414,8 +424,8 @@ export const RULES = [
414
424
  scope: "storyboard",
415
425
  severity: "fail",
416
426
  check: exists,
417
- message: (s) =>
418
- `storyboard: ${s.path} (current month ${s.yearMonth}) not found`,
427
+ message: (s) => `Current-month storyboard (${s.yearMonth}) not found`,
428
+ hint: "create it from `.claude/skills/kata-session/references/storyboard-template.md`",
419
429
  },
420
430
  {
421
431
  id: "storyboard.agent-h3-required",
@@ -423,7 +433,8 @@ export const RULES = [
423
433
  severity: "fail",
424
434
  when: storyboardExists,
425
435
  check: allRequiredLines(AGENT_H3_REQUIREMENTS),
426
- message: (s, r) => `storyboard: ${s.path} missing '### ${r.label}' H3`,
436
+ message: (_s, r) => `Missing '### ${r.label}' H3`,
437
+ hint: "every domain agent gets an H3 under '## Current Condition'",
427
438
  },
428
439
  {
429
440
  id: "storyboard.line-budget",
@@ -431,8 +442,8 @@ export const RULES = [
431
442
  severity: "fail",
432
443
  when: storyboardExists,
433
444
  check: lineBudget(STORYBOARD_LINE_BUDGET),
434
- message: (s, r) =>
435
- `storyboard: ${s.path} has ${r.value} lines (limit ${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",
436
447
  },
437
448
  {
438
449
  id: "storyboard.word-budget",
@@ -440,8 +451,8 @@ export const RULES = [
440
451
  severity: "fail",
441
452
  when: storyboardExists,
442
453
  check: wordBudget(STORYBOARD_WORD_BUDGET),
443
- message: (s, r) =>
444
- `storyboard: ${s.path} has ${r.value} words (limit ${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",
445
456
  },
446
457
  {
447
458
  id: "storyboard.markers-balanced.xmr",
@@ -453,8 +464,9 @@ export const RULES = [
453
464
  closeRe: XMR_CLOSE_RE,
454
465
  label: "xmr",
455
466
  }),
456
- message: (s, r) =>
457
- `storyboard: ${s.path}:${r.lineNo} ${r.reason} xmr marker${r.label ? ` (${r.label})` : ""}`,
467
+ message: (_s, r) =>
468
+ `${r.reason} xmr marker${r.label ? ` (${r.label})` : ""}`,
469
+ hint: "every '<!-- xmr:metric:csv -->' needs a matching '<!-- /xmr -->'",
458
470
  },
459
471
  {
460
472
  id: "storyboard.markers-balanced.issues",
@@ -466,8 +478,9 @@ export const RULES = [
466
478
  closeRe: ISSUE_CLOSE_RE,
467
479
  label: "issue-list",
468
480
  }),
469
- message: (s, r) =>
470
- `storyboard: ${s.path}:${r.lineNo} ${r.reason} issue-list marker${r.label ? ` (${r.label})` : ""}`,
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",
471
484
  },
472
485
 
473
486
  // -- Stray files --
@@ -477,7 +490,7 @@ export const RULES = [
477
490
  scope: "stray-file",
478
491
  severity: "fail",
479
492
  check: always,
480
- message: (s) =>
481
- `stray-file: ${s.path} does not match any known scope (summary, weekly log, or excluded prefix)`,
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",
482
495
  },
483
496
  ];
@@ -161,8 +161,12 @@ const SCOPE_RESOLVERS = {
161
161
  "weekly-log-part": (ctx) => ctx.subjects["weekly-log-part"],
162
162
  memory: (ctx) => [ctx.memory],
163
163
  "claims-row": (ctx) =>
164
- parseClaims(ctx.memory.text).map((c) => ({ path: null, ...c })),
165
- "priority-row": (ctx) => parsePriorityRows(ctx.memory.text),
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
+ })),
166
170
  storyboard: (ctx) => [ctx.storyboard],
167
171
  "stray-file": (ctx) => ctx.subjects.stray,
168
172
  };
@@ -1,10 +1,13 @@
1
1
  import fsAsync from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { Finder } from "@forwardimpact/libutil";
4
- import { runAudit } from "../audit/engine.js";
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 = runAudit(RULES, ctx);
20
+ const findings = runRules(RULES, ctx, { resolveScope });
18
21
 
19
22
  process.stdout.write(
20
- values.format === "json" ? emitJson(findings) : emitText(findings),
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/index.js CHANGED
@@ -35,5 +35,5 @@ export {
35
35
  appendEntry,
36
36
  } from "./weekly-log.js";
37
37
  export { buildDigest } from "./boot.js";
38
- export { runAudit } from "./audit/engine.js";
39
38
  export { RULES } from "./audit/rules.js";
39
+ export { resolveScope as resolveAuditScope } from "./audit/scopes.js";
@@ -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
- }
@@ -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
- }