memento-mori-jester 0.1.59 → 0.1.60

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to Memento Mori Jester are tracked here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.1.60
8
+
9
+ - Added rule-family slices to `npm run fixtures:report` so maintainers can compare built-in, structural, custom, configured sensitive-domain, and blocked-command coverage.
10
+ - Added preset slices and curation-next guidance to fixture report text and `--json` output.
11
+ - Updated fixture-report tests and docs so the coverage dashboard stays useful as the fixture suite grows.
12
+
7
13
  ## 0.1.59
8
14
 
9
15
  - Added quiet-pass fixtures for remaining sparse built-in and structural rules including missing verification, confidence theater, TypeScript suppressions, large removals, wildcard file operations, destructive commands, and untested finals.
package/README.md CHANGED
@@ -501,7 +501,7 @@ Use the false-positive template for noisy cautions or blocks. Include `jester su
501
501
 
502
502
  Maintainers can use [docs/MAINTAINER_TRIAGE.md](docs/MAINTAINER_TRIAGE.md) to turn useful false-positive reports into redacted fixtures.
503
503
  Run `npm run fixtures:check` before merging fixture changes; it catches duplicate IDs, missing rule metadata, weak descriptions, unsafe-looking content, and duplicate content.
504
- Run `npm run fixtures:report` to see fixture coverage by rule, preset, kind, verdict, and quiet-pass boundaries before choosing the next fixture.
504
+ Run `npm run fixtures:report` to see fixture coverage by rule, rule family, preset slice, kind, verdict, quiet-pass boundaries, and curation-next guidance before choosing the next fixture.
505
505
 
506
506
  For vulnerabilities, private code exposure, or credential-handling concerns, follow [SECURITY.md](SECURITY.md) instead of opening a public issue with sensitive details.
507
507
 
package/ROADMAP.md CHANGED
@@ -6,6 +6,7 @@ Memento Mori Jester is usable today as a CLI, MCP server, GitHub Action, and git
6
6
 
7
7
  ## Recently Shipped
8
8
 
9
+ - Fixture report rule-family slices, preset slices, and curation-next guidance in v0.1.60 so maintainers can see which fixture areas need real-world examples next.
9
10
  - Quiet-pass boundaries for remaining sparse built-in and structural rules in v0.1.59 so the fixture report now has no rules without quiet-pass coverage.
10
11
  - Quiet-pass boundaries for thin custom/preset rules in v0.1.58 so preset blocked commands, sensitive-domain checks, and custom stack rules now have safe near-miss examples.
11
12
  - Completed preset-kind fixture coverage in v0.1.57 so `default`, `node`, `python`, `web`, `api`, `infra`, `ai`, and `security` now all have plan, command, diff, and final examples.
@@ -49,7 +50,7 @@ Memento Mori Jester is usable today as a CLI, MCP server, GitHub Action, and git
49
50
  ## Product Ideas
50
51
 
51
52
  - Add more framework-specific false-positive examples from real reports so tuning guidance keeps getting sharper.
52
- - Add fixture report slices by rule family and preset so maintainers can spot which areas need real-world curation next.
53
+ - Add a Markdown export for fixture reports so maintainers can paste coverage snapshots into issues or release notes.
53
54
 
54
55
  ## Quality And Safety
55
56
 
package/docs/DEMO.md CHANGED
@@ -355,6 +355,8 @@ Preset packs:
355
355
 
356
356
  The fixture suite in `examples/fixtures/preset-review-cases.json` captures small real-usage examples with expected `pass`, `caution`, or `block` verdicts. It also includes quiet-pass `absentRuleIds` examples that prove noisy rules stay silent for safe near-misses, stack-specific coverage for every built-in preset, and quiet-pass boundaries across built-in, structural, custom, and preset/config-derived rules. These examples are run by `npm test`, so preset tuning changes stay visible.
357
357
 
358
+ Maintainers can run `npm run fixtures:report` to see coverage by verdict, kind, preset, rule family, and preset slice. The report also includes a `Curation next` section that points at the next useful fixture batch, such as thin rules, no-pass evidence, rule-family gaps, or lower-count presets.
359
+
358
360
  Maintainers can use `docs/MAINTAINER_TRIAGE.md` to turn useful false-positive reports into redacted fixture cases.
359
361
 
360
362
  ## 14. Framework CI Examples
@@ -83,7 +83,7 @@ node .\dist\cli.js tune coverage
83
83
  ```
84
84
 
85
85
  5. Fix any duplicate IDs, missing expected rule metadata, weak descriptions, unsafe content, or duplicate content reported by `fixtures:check`.
86
- 6. Use `fixtures:report` to check whether the change improves pass-case, quiet-pass, preset, kind, or verdict coverage.
86
+ 6. Use `fixtures:report` to check whether the change improves pass-case, quiet-pass, preset, kind, rule-family, or verdict coverage. Start with the report's `Curation next` section when deciding which fixture batch to add first.
87
87
  7. Check whether support/confidence changed in the expected direction.
88
88
  8. If the fixture changes verdict behavior, mention the exact rule impact in `CHANGELOG.md`.
89
89
 
@@ -53,7 +53,7 @@ This checklist defines what "production grade" means for Memento Mori Jester rig
53
53
  - `SECURITY.md` routes vulnerability reports away from public issues and asks for redacted diagnostics.
54
54
  - `docs/MAINTAINER_TRIAGE.md` explains how to turn useful false-positive reports into fixture coverage before changing rule logic.
55
55
  - `npm run fixtures:check` validates fixture IDs, metadata, unsafe-looking content, duplicate content, and explicit expected/absent rule intent.
56
- - `npm run fixtures:report` shows fixture coverage by rule, preset, kind, verdict, and quiet-pass rule boundaries so maintainers can pick the next fixture target.
56
+ - `npm run fixtures:report` shows fixture coverage by rule, rule family, preset slice, kind, verdict, and quiet-pass rule boundaries so maintainers can pick the next fixture target.
57
57
  - npm publish has a manual workflow fallback, but the normal release path is tag-driven trusted publishing.
58
58
 
59
59
  ## Static Guard
@@ -0,0 +1,30 @@
1
+ # Memento Mori Jester v0.1.60
2
+
3
+ This release makes `npm run fixtures:report` a more useful maintenance dashboard for fixture curation. It does not change review behavior, rule matching, MCP tools, presets, or release automation.
4
+
5
+ ## What Changed
6
+
7
+ - Added `ruleFamilySlices` to fixture report JSON and a `By rule family` section to text output.
8
+ - Added `presetSlices` to fixture report JSON and a `Preset slices` section to text output.
9
+ - Added `curationNext` guidance so maintainers can quickly see whether to add thin-rule, no-pass, rule-family, or lower-count preset examples next.
10
+ - Updated fixture-report tests and maintainer docs around the richer report output.
11
+
12
+ ## Public Interface
13
+
14
+ - No CLI review behavior changes.
15
+ - No config schema changes.
16
+ - No MCP, playground, GitHub Action, or npm publishing changes.
17
+ - `npm run fixtures:report -- --json` now includes additional stable fields: `ruleFamilySlices`, `presetSlices`, and `curationNext`.
18
+
19
+ ## Release Validation
20
+
21
+ ```powershell
22
+ npm.cmd test
23
+ npm.cmd run demo:svg:check
24
+ npm.cmd run fixtures:report
25
+ npm.cmd run fixtures:report -- --json
26
+ npm.cmd run pack:dry
27
+ git diff --check
28
+ node .\dist\cli.js tune coverage --no-config
29
+ git diff | node .\dist\cli.js diff --fail-on block --subject "v0.1.60 fixture report curation slices"
30
+ ```
@@ -48,4 +48,6 @@ Do not add secrets, private code, customer data, complete logs, or machine-speci
48
48
 
49
49
  `npm run fixtures:check` validates duplicate IDs, missing expected rule metadata, weak descriptions, unsafe-looking fixture content, and duplicate content before the fixture suite becomes tuning evidence.
50
50
 
51
- `npm run fixtures:report` summarizes coverage by rule, preset, review kind, verdict, and quiet-pass rule boundaries. Use it to find rules without pass-case coverage, rules without quiet-pass coverage, thin rule coverage, preset/kind gaps, and quiet pass fixtures.
51
+ `npm run fixtures:report` summarizes coverage by rule, rule family, preset slice, review kind, verdict, and quiet-pass rule boundaries. Use it to find rules without pass-case coverage, rules without quiet-pass coverage, thin rule coverage, preset/kind gaps, quiet pass fixtures, and the next curation target.
52
+
53
+ The `Curation next` section is a maintainer shortcut: start there when deciding whether the next fixture batch should focus on thin rules, no-pass evidence, a specific rule family, or lower-count presets. The `--json` output includes the same `ruleFamilySlices`, `presetSlices`, and `curationNext` fields for scripts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memento-mori-jester",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "A local court-jester sidecar for AI coding agents: review plans, commands, diffs, and final claims before they get too pleased with themselves.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -8,6 +8,18 @@ const allowedPresets = ["default", "node", "python", "web", "api", "infra", "ai"
8
8
  const allowedKinds = ["plan", "command", "diff", "final"];
9
9
  const allowedVerdicts = ["pass", "caution", "block"];
10
10
  const sampleLimit = 3;
11
+ const structuralRuleIds = new Set([
12
+ "large-removal",
13
+ "missing-verification-step",
14
+ "wildcard-file-operation"
15
+ ]);
16
+ const ruleFamilyOrder = [
17
+ "built-in",
18
+ "structural",
19
+ "custom",
20
+ "configured-sensitive-domain",
21
+ "blocked-command"
22
+ ];
11
23
 
12
24
  const args = new Set(process.argv.slice(2));
13
25
  const json = args.has("--json");
@@ -43,6 +55,7 @@ function buildFixtureReport(rawFixtures) {
43
55
  const byVerdict = zeroCounts(allowedVerdicts);
44
56
  const rules = new Map();
45
57
  const quietPassRules = new Map();
58
+ const presetSlices = new Map(allowedPresets.map((preset) => [preset, createPresetSlice(preset)]));
46
59
  const quietPassFixtures = [];
47
60
 
48
61
  let totalWeight = 0;
@@ -72,9 +85,24 @@ function buildFixtureReport(rawFixtures) {
72
85
  edgeCaseFixtures += 1;
73
86
  }
74
87
 
88
+ const presetSlice = presetSlices.get(preset) ?? createPresetSlice(preset);
89
+ presetSlice.total += 1;
90
+ presetSlice.weight += weight;
91
+ presetSlice.byKind[kind] = (presetSlice.byKind[kind] ?? 0) + 1;
92
+ presetSlice.byVerdict[verdict] = (presetSlice.byVerdict[verdict] ?? 0) + 1;
93
+ presetSlice.expectedRuleReferences += expectedRuleIds.length;
94
+ if (verdict === "pass") {
95
+ presetSlice.quietPassRuleReferences += absentRuleIds.length;
96
+ }
97
+ if (edgeCase) {
98
+ presetSlice.edgeCases += 1;
99
+ }
100
+
75
101
  if (verdict === "pass" && expectedRuleIds.length === 0) {
76
102
  quietPassFixtures.push(sample);
103
+ presetSlice.quietPassFixtures += 1;
77
104
  }
105
+ presetSlices.set(preset, presetSlice);
78
106
 
79
107
  if (verdict === "pass") {
80
108
  for (const ruleId of absentRuleIds) {
@@ -154,11 +182,14 @@ function buildFixtureReport(rawFixtures) {
154
182
  .sort((a, b) => a.total - b.total || a.ruleId.localeCompare(b.ruleId))
155
183
  .map(ruleGapSummary),
156
184
  presetKindGaps: presetKindGaps(rawFixtures),
157
- quietPassRuleCoverage: quietPassRuleSummaries,
158
- quietPassFixtures: quietPassFixtures
159
- .slice()
160
- .sort((a, b) => a.id.localeCompare(b.id))
185
+ quietPassRuleCoverage: quietPassRuleSummaries,
186
+ quietPassFixtures: quietPassFixtures
187
+ .slice()
188
+ .sort((a, b) => a.id.localeCompare(b.id))
161
189
  };
190
+ const ruleFamilySlices = buildRuleFamilySlices(ruleSummaries, quietPassRuleSummaries);
191
+ const presetSliceSummaries = buildPresetSlices(presetSlices);
192
+ const curationNext = buildCurationNext(gaps, ruleFamilySlices, presetSliceSummaries);
162
193
 
163
194
  return {
164
195
  totalFixtures: rawFixtures.length,
@@ -167,6 +198,9 @@ function buildFixtureReport(rawFixtures) {
167
198
  byVerdict: orderedCounts(byVerdict, allowedVerdicts),
168
199
  byKind: orderedCounts(byKind, allowedKinds),
169
200
  byPreset: orderedCounts(byPreset, allowedPresets),
201
+ ruleFamilySlices,
202
+ presetSlices: presetSliceSummaries,
203
+ curationNext,
170
204
  quietPassRules: quietPassRuleSummaries,
171
205
  rules: ruleSummaries,
172
206
  gaps
@@ -186,9 +220,17 @@ function renderFixtureReport(report) {
186
220
  `By kind: ${formatCounts(report.byKind)}`,
187
221
  `By preset: ${formatCounts(report.byPreset)}`,
188
222
  "",
189
- "Rules without pass-case coverage:"
223
+ "By rule family:"
190
224
  ];
191
225
 
226
+ lines.push(...formatRuleFamilySlices(report.ruleFamilySlices));
227
+ lines.push("", "Preset slices:");
228
+ lines.push(...formatPresetSlices(report.presetSlices));
229
+ lines.push(
230
+ "",
231
+ "Rules without pass-case coverage:"
232
+ );
233
+
192
234
  lines.push(...formatRuleGaps(report.gaps.rulesWithoutPassCases));
193
235
  lines.push("", "Rules without quiet-pass coverage:");
194
236
  lines.push(...formatRuleGaps(report.gaps.rulesWithoutQuietPassCoverage));
@@ -200,6 +242,8 @@ function renderFixtureReport(report) {
200
242
  lines.push(...formatPresetKindGaps(report.gaps.presetKindGaps));
201
243
  lines.push("", "Quiet pass fixtures:");
202
244
  lines.push(...formatFixtureSamples(report.gaps.quietPassFixtures));
245
+ lines.push("", "Curation next:");
246
+ lines.push(...formatCurationNext(report.curationNext));
203
247
  lines.push(
204
248
  "",
205
249
  "Next:",
@@ -236,6 +280,39 @@ function createQuietPassEntry(ruleId) {
236
280
  };
237
281
  }
238
282
 
283
+ function createPresetSlice(preset) {
284
+ return {
285
+ preset,
286
+ total: 0,
287
+ weight: 0,
288
+ edgeCases: 0,
289
+ quietPassFixtures: 0,
290
+ expectedRuleReferences: 0,
291
+ quietPassRuleReferences: 0,
292
+ byKind: zeroCounts(allowedKinds),
293
+ byVerdict: zeroCounts(allowedVerdicts)
294
+ };
295
+ }
296
+
297
+ function createRuleFamilySlice(family) {
298
+ return {
299
+ family,
300
+ ruleCount: 0,
301
+ ruleIds: [],
302
+ fixtureReferences: 0,
303
+ weight: 0,
304
+ passCases: 0,
305
+ cautionCases: 0,
306
+ blockCases: 0,
307
+ quietPassCases: 0,
308
+ quietPassWeight: 0,
309
+ rulesWithoutPassCases: [],
310
+ rulesWithoutQuietPassCoverage: [],
311
+ thinRules: [],
312
+ sampleRules: []
313
+ };
314
+ }
315
+
239
316
  function fixtureWeight(rawWeight) {
240
317
  if (typeof rawWeight === "number" && rawWeight > 0 && Number.isFinite(rawWeight)) {
241
318
  return Math.max(1, Math.min(3, Math.round(rawWeight)));
@@ -256,6 +333,204 @@ function orderedCounts(counts, keys) {
256
333
  return ordered;
257
334
  }
258
335
 
336
+ function ruleFamily(ruleId) {
337
+ if (ruleId.startsWith("blocked-command-")) {
338
+ return "blocked-command";
339
+ }
340
+
341
+ if (ruleId.startsWith("configured-sensitive-domain-")) {
342
+ return "configured-sensitive-domain";
343
+ }
344
+
345
+ if (ruleId.startsWith("custom-")) {
346
+ return "custom";
347
+ }
348
+
349
+ if (structuralRuleIds.has(ruleId)) {
350
+ return "structural";
351
+ }
352
+
353
+ return "built-in";
354
+ }
355
+
356
+ function buildRuleFamilySlices(ruleSummaries, quietPassRuleSummaries) {
357
+ const slices = new Map(ruleFamilyOrder.map((family) => [family, createRuleFamilySlice(family)]));
358
+
359
+ for (const entry of ruleSummaries) {
360
+ const family = ruleFamily(entry.ruleId);
361
+ const slice = slices.get(family) ?? createRuleFamilySlice(family);
362
+
363
+ slice.ruleCount += 1;
364
+ slice.ruleIds.push(entry.ruleId);
365
+ slice.fixtureReferences += entry.total;
366
+ slice.weight += entry.weight;
367
+ slice.passCases += entry.passCases;
368
+ slice.cautionCases += entry.cautionCases;
369
+ slice.blockCases += entry.blockCases;
370
+ if (entry.passCases === 0) {
371
+ slice.rulesWithoutPassCases.push(ruleGapSummary(entry));
372
+ }
373
+ if (entry.quietPassCases === 0) {
374
+ slice.rulesWithoutQuietPassCoverage.push(ruleGapSummary(entry));
375
+ }
376
+ if (entry.total < 2) {
377
+ slice.thinRules.push(ruleGapSummary(entry));
378
+ }
379
+ if (slice.sampleRules.length < sampleLimit) {
380
+ slice.sampleRules.push({
381
+ ruleId: entry.ruleId,
382
+ total: entry.total,
383
+ quietPassCases: entry.quietPassCases
384
+ });
385
+ }
386
+
387
+ slices.set(family, slice);
388
+ }
389
+
390
+ for (const entry of quietPassRuleSummaries) {
391
+ const family = ruleFamily(entry.ruleId);
392
+ const slice = slices.get(family) ?? createRuleFamilySlice(family);
393
+
394
+ slice.quietPassCases += entry.total;
395
+ slice.quietPassWeight += entry.weight;
396
+ slices.set(family, slice);
397
+ }
398
+
399
+ return sortRuleFamilies([...slices.values()])
400
+ .filter((entry) => entry.ruleCount > 0 || entry.quietPassCases > 0)
401
+ .map((entry) => ({
402
+ ...entry,
403
+ ruleIds: entry.ruleIds.slice().sort((a, b) => a.localeCompare(b)),
404
+ rulesWithoutPassCases: entry.rulesWithoutPassCases
405
+ .slice()
406
+ .sort((a, b) => b.total - a.total || a.ruleId.localeCompare(b.ruleId)),
407
+ rulesWithoutQuietPassCoverage: entry.rulesWithoutQuietPassCoverage
408
+ .slice()
409
+ .sort((a, b) => b.total - a.total || a.ruleId.localeCompare(b.ruleId)),
410
+ thinRules: entry.thinRules
411
+ .slice()
412
+ .sort((a, b) => a.total - b.total || a.ruleId.localeCompare(b.ruleId)),
413
+ sampleRules: entry.sampleRules
414
+ .slice()
415
+ .sort((a, b) => b.total - a.total || a.ruleId.localeCompare(b.ruleId))
416
+ }));
417
+ }
418
+
419
+ function buildPresetSlices(presetSlices) {
420
+ return [...presetSlices.values()]
421
+ .map((entry) => ({
422
+ preset: entry.preset,
423
+ total: entry.total,
424
+ weight: entry.weight,
425
+ edgeCases: entry.edgeCases,
426
+ quietPassFixtures: entry.quietPassFixtures,
427
+ expectedRuleReferences: entry.expectedRuleReferences,
428
+ quietPassRuleReferences: entry.quietPassRuleReferences,
429
+ byKind: orderedCounts(entry.byKind, allowedKinds),
430
+ byVerdict: orderedCounts(entry.byVerdict, allowedVerdicts)
431
+ }))
432
+ .sort((a, b) => presetOrder(a.preset) - presetOrder(b.preset) || a.preset.localeCompare(b.preset));
433
+ }
434
+
435
+ function buildCurationNext(gaps, ruleFamilySlices, presetSlices) {
436
+ const items = [];
437
+
438
+ if (gaps.presetKindGaps.length > 0) {
439
+ items.push({
440
+ priority: "high",
441
+ area: "preset-kind-gaps",
442
+ title: "Fill missing preset/review-kind combinations",
443
+ count: gaps.presetKindGaps.length,
444
+ details: gaps.presetKindGaps
445
+ .slice(0, 6)
446
+ .map((entry) => `${entry.preset}: ${entry.missingKinds.join(", ")}`)
447
+ });
448
+ }
449
+
450
+ if (gaps.rulesWithoutQuietPassCoverage.length > 0) {
451
+ items.push({
452
+ priority: "high",
453
+ area: "quiet-pass-gaps",
454
+ title: "Add safe near-miss fixtures for rules without quiet-pass coverage",
455
+ count: gaps.rulesWithoutQuietPassCoverage.length,
456
+ ruleIds: gaps.rulesWithoutQuietPassCoverage.slice(0, 8).map((entry) => entry.ruleId)
457
+ });
458
+ }
459
+
460
+ if (gaps.thinRuleCoverage.length > 0) {
461
+ items.push({
462
+ priority: "medium",
463
+ area: "thin-rule-coverage",
464
+ title: "Add a second example for rules with only one firing fixture",
465
+ count: gaps.thinRuleCoverage.length,
466
+ ruleIds: gaps.thinRuleCoverage.slice(0, 8).map((entry) => entry.ruleId)
467
+ });
468
+ }
469
+
470
+ if (gaps.rulesWithoutPassCases.length > 0) {
471
+ items.push({
472
+ priority: "medium",
473
+ area: "pass-case-coverage",
474
+ title: "Add benign or docs-only examples for rules with no pass-case evidence",
475
+ count: gaps.rulesWithoutPassCases.length,
476
+ ruleIds: gaps.rulesWithoutPassCases.slice(0, 8).map((entry) => entry.ruleId)
477
+ });
478
+ }
479
+
480
+ const thinFamilies = ruleFamilySlices
481
+ .filter((entry) => entry.thinRules.length > 0)
482
+ .map((entry) => ({
483
+ family: entry.family,
484
+ thinRules: entry.thinRules.length,
485
+ ruleIds: entry.thinRules.slice(0, 4).map((rule) => rule.ruleId)
486
+ }));
487
+
488
+ if (thinFamilies.length > 0) {
489
+ items.push({
490
+ priority: "low",
491
+ area: "rule-family-curation",
492
+ title: "Use rule-family slices to batch similar thin rules",
493
+ count: thinFamilies.reduce((total, entry) => total + entry.thinRules, 0),
494
+ families: thinFamilies
495
+ });
496
+ }
497
+
498
+ const lowerPresetSlices = presetSlices
499
+ .filter((entry) => entry.total > 0)
500
+ .slice()
501
+ .sort((a, b) => a.total - b.total || presetOrder(a.preset) - presetOrder(b.preset) || a.preset.localeCompare(b.preset))
502
+ .slice(0, 4)
503
+ .map((entry) => ({
504
+ preset: entry.preset,
505
+ total: entry.total,
506
+ quietPassFixtures: entry.quietPassFixtures
507
+ }));
508
+
509
+ items.push({
510
+ priority: "low",
511
+ area: "preset-real-world-curation",
512
+ title: "Collect real-world reports for the lowest-count preset slices",
513
+ count: lowerPresetSlices.length,
514
+ presets: lowerPresetSlices
515
+ });
516
+
517
+ return items;
518
+ }
519
+
520
+ function sortRuleFamilies(entries) {
521
+ return entries.sort((a, b) => ruleFamilyIndex(a.family) - ruleFamilyIndex(b.family) || a.family.localeCompare(b.family));
522
+ }
523
+
524
+ function ruleFamilyIndex(family) {
525
+ const index = ruleFamilyOrder.indexOf(family);
526
+ return index === -1 ? ruleFamilyOrder.length : index;
527
+ }
528
+
529
+ function presetOrder(preset) {
530
+ const index = allowedPresets.indexOf(preset);
531
+ return index === -1 ? allowedPresets.length : index;
532
+ }
533
+
259
534
  function presetKindGaps(fixtures) {
260
535
  const seen = new Set(fixtures.map((fixture) => `${fixture.preset}:${fixture.kind}`));
261
536
  return allowedPresets
@@ -294,6 +569,43 @@ function formatRuleGaps(entries) {
294
569
  .map((entry) => `- ${entry.ruleId}: ${entry.total} fixture(s), pass ${entry.passCases}, caution ${entry.cautionCases}, block ${entry.blockCases}, quiet-pass ${entry.quietPassCases}`);
295
570
  }
296
571
 
572
+ function formatRuleFamilySlices(entries) {
573
+ if (entries.length === 0) {
574
+ return ["- none"];
575
+ }
576
+
577
+ return entries.map((entry) =>
578
+ [
579
+ `- ${entry.family}: ${entry.ruleCount} rule(s)`,
580
+ `${entry.fixtureReferences} fixture ref(s)`,
581
+ `pass ${entry.passCases}`,
582
+ `caution ${entry.cautionCases}`,
583
+ `block ${entry.blockCases}`,
584
+ `quiet-pass ${entry.quietPassCases}`,
585
+ `thin ${entry.thinRules.length}`
586
+ ].join(", ")
587
+ );
588
+ }
589
+
590
+ function formatPresetSlices(entries) {
591
+ if (entries.length === 0) {
592
+ return ["- none"];
593
+ }
594
+
595
+ return entries.map((entry) =>
596
+ [
597
+ `- ${entry.preset}: ${entry.total} fixture(s)`,
598
+ `weight ${entry.weight}`,
599
+ `pass ${entry.byVerdict.pass ?? 0}`,
600
+ `caution ${entry.byVerdict.caution ?? 0}`,
601
+ `block ${entry.byVerdict.block ?? 0}`,
602
+ `quiet-pass ${entry.quietPassFixtures}`,
603
+ `rule refs ${entry.expectedRuleReferences}`,
604
+ `absent refs ${entry.quietPassRuleReferences}`
605
+ ].join(", ")
606
+ );
607
+ }
608
+
297
609
  function formatQuietPassRuleCoverage(entries) {
298
610
  if (entries.length === 0) {
299
611
  return ["- none"];
@@ -321,3 +633,34 @@ function formatFixtureSamples(entries) {
321
633
  .slice(0, 8)
322
634
  .map((entry) => `- ${entry.id}: ${entry.description}`);
323
635
  }
636
+
637
+ function formatCurationNext(entries) {
638
+ if (entries.length === 0) {
639
+ return ["- none"];
640
+ }
641
+
642
+ return entries.map((entry) => {
643
+ const details = formatCurationDetails(entry);
644
+ return `- ${entry.priority} ${entry.area}: ${entry.title} (${entry.count})${details}`;
645
+ });
646
+ }
647
+
648
+ function formatCurationDetails(entry) {
649
+ if (Array.isArray(entry.ruleIds) && entry.ruleIds.length > 0) {
650
+ return `: ${entry.ruleIds.join(", ")}`;
651
+ }
652
+
653
+ if (Array.isArray(entry.details) && entry.details.length > 0) {
654
+ return `: ${entry.details.join("; ")}`;
655
+ }
656
+
657
+ if (Array.isArray(entry.families) && entry.families.length > 0) {
658
+ return `: ${entry.families.map((family) => `${family.family} ${family.thinRules}`).join(", ")}`;
659
+ }
660
+
661
+ if (Array.isArray(entry.presets) && entry.presets.length > 0) {
662
+ return `: ${entry.presets.map((preset) => `${preset.preset} ${preset.total}`).join(", ")}`;
663
+ }
664
+
665
+ return "";
666
+ }