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 +6 -0
- package/README.md +1 -1
- package/ROADMAP.md +2 -1
- package/docs/DEMO.md +2 -0
- package/docs/MAINTAINER_TRIAGE.md +1 -1
- package/docs/PRODUCTION_READINESS.md +1 -1
- package/docs/RELEASE_NOTES_v0.1.60.md +30 -0
- package/examples/fixtures/README.md +3 -1
- package/package.json +1 -1
- package/scripts/report-fixtures.mjs +348 -5
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,
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
"
|
|
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
|
+
}
|