rulereceipt 0.1.23 → 0.1.24
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 +33 -0
- package/dist/cli.js +20 -1
- package/dist/overrides.d.ts +82 -0
- package/dist/overrides.js +134 -0
- package/dist/report/generateHtmlReport.js +11 -1
- package/dist/report/generateReport.js +19 -0
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,38 @@ on a session it never found.
|
|
|
93
93
|
For most people the honest answer is simpler: run `rulereceipt check --html`
|
|
94
94
|
locally and attach the report to the PR.
|
|
95
95
|
|
|
96
|
+
## Correcting a misclassification
|
|
97
|
+
|
|
98
|
+
If the tool treats something in your rules file as a rule when it isn't,
|
|
99
|
+
create `.rulereceipt.json` in your project:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"overrides": [
|
|
104
|
+
{ "rule": "12", "reason": "changelog entry, not a rule", "date": "2026-08-31" }
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Commit it. An override should be visible in code review, not just in the
|
|
110
|
+
report.
|
|
111
|
+
|
|
112
|
+
**An override cannot hide a violation, by design.** The check still runs,
|
|
113
|
+
keeps its real result, and the override only adds a label. So the report
|
|
114
|
+
shows the rule, its true result, your reason, and a line saying what the
|
|
115
|
+
result would have been without the override. The headline count includes
|
|
116
|
+
overridden failures, and the exit code still fails on them — otherwise CI
|
|
117
|
+
would be the loophole.
|
|
118
|
+
|
|
119
|
+
The worst an override can do is draw a labelled box around a real
|
|
120
|
+
violation and sign it with your reason. That leaves a reader better
|
|
121
|
+
informed than a plain failure would, not worse.
|
|
122
|
+
|
|
123
|
+
A reason is required; an override without one is refused rather than
|
|
124
|
+
applied quietly. If a rule id exists in both your global and project
|
|
125
|
+
rules files, write `"project:12"` or `"global:12"` — a bare id that
|
|
126
|
+
matches both is refused rather than silently disabling both.
|
|
127
|
+
|
|
96
128
|
## Sharing a report
|
|
97
129
|
|
|
98
130
|
`rulereceipt check --html` writes one self-contained HTML file. No
|
|
@@ -128,6 +160,7 @@ rulereceipt check --html # write a shareable single-file HTML report you c
|
|
|
128
160
|
rulereceipt check --html report.html # ...to a specific path
|
|
129
161
|
rulereceipt check --require-session # fail if there's no session, instead of passing silently
|
|
130
162
|
rulereceipt check --exit-zero # report failures without failing the build
|
|
163
|
+
# .rulereceipt.json # mark a misclassified item as not-a-rule (see below)
|
|
131
164
|
rulereceipt check --llm # opt-in: grade judgment rules with your own Claude key
|
|
132
165
|
rulereceipt check --share # opt-in: send anonymous pass/fail/unclear counts
|
|
133
166
|
rulereceipt check --telemetry # opt-in: send one random per-machine ID
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ import { enableSchedule, disableSchedule, scheduleStatus } from "./schedule.js";
|
|
|
26
26
|
import { findSplitBrainConflicts } from "./checks/splitBrain.js";
|
|
27
27
|
import { runDoctor } from "./checks/doctor.js";
|
|
28
28
|
import { sendTelemetryPing, isTelemetryEnabled } from "./telemetry.js";
|
|
29
|
+
import { loadOverrides, resolveOverrides } from "./overrides.js";
|
|
29
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
31
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
31
32
|
const SHARE_ENDPOINT = "https://rulereceipt.dev/api/share";
|
|
@@ -220,7 +221,22 @@ async function runCheck(opts) {
|
|
|
220
221
|
// sends only a random install ID, never rule text or transcript content,
|
|
221
222
|
// regardless of --llm.
|
|
222
223
|
const judgmentResults = llm ? await runJudgmentChecks(judgment, events) : judgment.map(({ rule }) => needsLlmResult(rule));
|
|
223
|
-
const
|
|
224
|
+
const computed = [...deterministicResults, ...judgmentResults];
|
|
225
|
+
// Applied AFTER every check has run, and it only attaches a label.
|
|
226
|
+
// Nothing is skipped and no status is changed: an override that could
|
|
227
|
+
// suppress a check would let anyone delete their own violations, which
|
|
228
|
+
// is precisely what this design refuses to allow. See src/overrides.ts.
|
|
229
|
+
// Resolved against the rules actually present, and scoped by source: a
|
|
230
|
+
// global and a project rules file can both define "Rule 1", and a bare
|
|
231
|
+
// id would otherwise silently disable both.
|
|
232
|
+
const { bySourceAndId: overrides, problems: overrideProblems } = resolveOverrides(loadOverrides(cwd), computed.map((r) => ({ ruleId: r.ruleId, ruleSource: r.ruleSource })));
|
|
233
|
+
const results = computed.map((r) => {
|
|
234
|
+
const o = overrides.get(`${r.ruleSource}:${r.ruleId}`);
|
|
235
|
+
return o ? { ...r, overriddenReason: o.reason, overriddenDate: o.date } : r;
|
|
236
|
+
});
|
|
237
|
+
for (const problem of overrideProblems) {
|
|
238
|
+
console.log(`\n(${problem})`);
|
|
239
|
+
}
|
|
224
240
|
const meta = { sessionFilePath, ruleCount: results.length };
|
|
225
241
|
const reportText = markdown ? generateMarkdownReport(results, meta) : generateReport(results, meta);
|
|
226
242
|
console.log(reportText);
|
|
@@ -262,6 +278,9 @@ async function runCheck(opts) {
|
|
|
262
278
|
// rules in a real CLAUDE.md need judgment, so without --llm they
|
|
263
279
|
// legitimately report UNCLEAR. Gating on those would make every build
|
|
264
280
|
// red on day one and the check would be deleted within a week.
|
|
281
|
+
// Deliberately reads .status, which an override never changes. If an
|
|
282
|
+
// overridden failure exited 0, CI would become the loophole this whole
|
|
283
|
+
// design exists to close: mark the rule, get a green build, done.
|
|
265
284
|
if (!exitZero && results.some((r) => r.status === "FAIL")) {
|
|
266
285
|
process.exitCode = 1;
|
|
267
286
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project overrides: a way to say "this item in my rules file isn't
|
|
3
|
+
* actually a rule for my project" without waiting for an upstream release.
|
|
4
|
+
*
|
|
5
|
+
* THE SECURITY PROPERTY THAT MAKES THIS SAFE
|
|
6
|
+
*
|
|
7
|
+
* An override never changes whether a check runs, and never changes its
|
|
8
|
+
* result. The check executes exactly as it would have, keeps its real
|
|
9
|
+
* status, and the override only changes how the result is PRESENTED.
|
|
10
|
+
*
|
|
11
|
+
* That distinction is the whole design. The obvious version of this
|
|
12
|
+
* feature — "let the user mark a rule as not-a-rule, and skip it" — is
|
|
13
|
+
* not safe, and restricting the direction of the override does not make
|
|
14
|
+
* it safe:
|
|
15
|
+
*
|
|
16
|
+
* Rule: "Never commit directly to main"
|
|
17
|
+
* Agent: commits directly to main -> FAIL
|
|
18
|
+
* User: marks it "not a rule" -> not checked
|
|
19
|
+
* Report: the violation is gone
|
|
20
|
+
*
|
|
21
|
+
* Nobody had to claim a pass. They deleted the question instead. So this
|
|
22
|
+
* implementation refuses to delete questions. The worst an override can
|
|
23
|
+
* do is draw a labelled box around a real violation and sign it with a
|
|
24
|
+
* reason and a date — which leaves a reader BETTER informed than a plain
|
|
25
|
+
* failure would, not worse.
|
|
26
|
+
*
|
|
27
|
+
* Consequences enforced elsewhere, and deliberately not weakened:
|
|
28
|
+
* - the exit code still fails on an overridden violation (cli.ts), or
|
|
29
|
+
* CI becomes the loophole this whole design exists to close;
|
|
30
|
+
* - the report's headline verdict counts overridden failures, or the
|
|
31
|
+
* one line everyone reads would be the one line that lies.
|
|
32
|
+
*
|
|
33
|
+
* A reason is mandatory. An override without one is refused, not applied
|
|
34
|
+
* silently: it costs a sentence to write, and it is the part a reviewer
|
|
35
|
+
* actually reads.
|
|
36
|
+
*
|
|
37
|
+
* AMBIGUOUS IDS FAIL CLOSED. A global CLAUDE.md and a project one can
|
|
38
|
+
* legitimately both contain a "Rule 1" — the report already disambiguates
|
|
39
|
+
* those on collision. An override written as `"rule": "1"` when two rules
|
|
40
|
+
* share that id would silently disable BOTH, including one the user never
|
|
41
|
+
* meant to touch. Found while testing this feature against a real machine
|
|
42
|
+
* that has a global rules file. So a bare id is applied only when it is
|
|
43
|
+
* unambiguous; when it is not, the override is refused and the user is
|
|
44
|
+
* told to write `"project:1"` or `"global:1"` instead.
|
|
45
|
+
*/
|
|
46
|
+
export declare const OVERRIDES_FILE = ".rulereceipt.json";
|
|
47
|
+
export interface RuleOverride {
|
|
48
|
+
/** Rule id as it appears in the report, e.g. "12" or "S7.0". */
|
|
49
|
+
rule: string;
|
|
50
|
+
/** Why this isn't a rule for this project. Required — never optional. */
|
|
51
|
+
reason: string;
|
|
52
|
+
/** Optional ISO date, shown in the report so a reader can judge staleness. */
|
|
53
|
+
date?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface LoadedOverrides {
|
|
56
|
+
/** Keyed by the raw id as written by the user ("1" or "project:1"). */
|
|
57
|
+
byRuleId: Map<string, RuleOverride>;
|
|
58
|
+
/** Problems worth telling the user about — malformed entries, missing reasons. */
|
|
59
|
+
problems: string[];
|
|
60
|
+
}
|
|
61
|
+
/** A rule as the report identifies it, used to resolve an override target. */
|
|
62
|
+
export interface OverrideTarget {
|
|
63
|
+
ruleId: string;
|
|
64
|
+
ruleSource: "global" | "project";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolves override entries against the rules actually present, and
|
|
68
|
+
* refuses anything ambiguous rather than guessing which rule was meant.
|
|
69
|
+
* Returns a lookup keyed by `${source}:${id}`, plus any new problems.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveOverrides(loaded: LoadedOverrides, targets: OverrideTarget[]): {
|
|
72
|
+
bySourceAndId: Map<string, RuleOverride>;
|
|
73
|
+
problems: string[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Reads and validates the override file. Never throws: a broken override
|
|
77
|
+
* file must not take down a check that would otherwise have worked, and
|
|
78
|
+
* an unreadable file means "no overrides", never "override everything".
|
|
79
|
+
* Every rejection is reported rather than swallowed, so a user whose
|
|
80
|
+
* override isn't working finds out why.
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadOverrides(cwd: string): LoadedOverrides;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Per-project overrides: a way to say "this item in my rules file isn't
|
|
5
|
+
* actually a rule for my project" without waiting for an upstream release.
|
|
6
|
+
*
|
|
7
|
+
* THE SECURITY PROPERTY THAT MAKES THIS SAFE
|
|
8
|
+
*
|
|
9
|
+
* An override never changes whether a check runs, and never changes its
|
|
10
|
+
* result. The check executes exactly as it would have, keeps its real
|
|
11
|
+
* status, and the override only changes how the result is PRESENTED.
|
|
12
|
+
*
|
|
13
|
+
* That distinction is the whole design. The obvious version of this
|
|
14
|
+
* feature — "let the user mark a rule as not-a-rule, and skip it" — is
|
|
15
|
+
* not safe, and restricting the direction of the override does not make
|
|
16
|
+
* it safe:
|
|
17
|
+
*
|
|
18
|
+
* Rule: "Never commit directly to main"
|
|
19
|
+
* Agent: commits directly to main -> FAIL
|
|
20
|
+
* User: marks it "not a rule" -> not checked
|
|
21
|
+
* Report: the violation is gone
|
|
22
|
+
*
|
|
23
|
+
* Nobody had to claim a pass. They deleted the question instead. So this
|
|
24
|
+
* implementation refuses to delete questions. The worst an override can
|
|
25
|
+
* do is draw a labelled box around a real violation and sign it with a
|
|
26
|
+
* reason and a date — which leaves a reader BETTER informed than a plain
|
|
27
|
+
* failure would, not worse.
|
|
28
|
+
*
|
|
29
|
+
* Consequences enforced elsewhere, and deliberately not weakened:
|
|
30
|
+
* - the exit code still fails on an overridden violation (cli.ts), or
|
|
31
|
+
* CI becomes the loophole this whole design exists to close;
|
|
32
|
+
* - the report's headline verdict counts overridden failures, or the
|
|
33
|
+
* one line everyone reads would be the one line that lies.
|
|
34
|
+
*
|
|
35
|
+
* A reason is mandatory. An override without one is refused, not applied
|
|
36
|
+
* silently: it costs a sentence to write, and it is the part a reviewer
|
|
37
|
+
* actually reads.
|
|
38
|
+
*
|
|
39
|
+
* AMBIGUOUS IDS FAIL CLOSED. A global CLAUDE.md and a project one can
|
|
40
|
+
* legitimately both contain a "Rule 1" — the report already disambiguates
|
|
41
|
+
* those on collision. An override written as `"rule": "1"` when two rules
|
|
42
|
+
* share that id would silently disable BOTH, including one the user never
|
|
43
|
+
* meant to touch. Found while testing this feature against a real machine
|
|
44
|
+
* that has a global rules file. So a bare id is applied only when it is
|
|
45
|
+
* unambiguous; when it is not, the override is refused and the user is
|
|
46
|
+
* told to write `"project:1"` or `"global:1"` instead.
|
|
47
|
+
*/
|
|
48
|
+
export const OVERRIDES_FILE = ".rulereceipt.json";
|
|
49
|
+
/**
|
|
50
|
+
* Resolves override entries against the rules actually present, and
|
|
51
|
+
* refuses anything ambiguous rather than guessing which rule was meant.
|
|
52
|
+
* Returns a lookup keyed by `${source}:${id}`, plus any new problems.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveOverrides(loaded, targets) {
|
|
55
|
+
const bySourceAndId = new Map();
|
|
56
|
+
const problems = [...loaded.problems];
|
|
57
|
+
for (const [written, entry] of loaded.byRuleId) {
|
|
58
|
+
const scoped = /^(global|project):(.+)$/i.exec(written);
|
|
59
|
+
if (scoped) {
|
|
60
|
+
const source = scoped[1].toLowerCase();
|
|
61
|
+
const id = scoped[2].trim();
|
|
62
|
+
const hit = targets.find((t) => t.ruleId === id && t.ruleSource === source);
|
|
63
|
+
if (!hit) {
|
|
64
|
+
problems.push(`${OVERRIDES_FILE}: no ${source} rule with id "${id}" was found, so that override did nothing.`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
bySourceAndId.set(`${source}:${id}`, entry);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const matches = targets.filter((t) => t.ruleId === written);
|
|
71
|
+
if (matches.length === 0) {
|
|
72
|
+
problems.push(`${OVERRIDES_FILE}: no rule with id "${written}" was found, so that override did nothing.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const sources = new Set(matches.map((m) => m.ruleSource));
|
|
76
|
+
if (sources.size > 1) {
|
|
77
|
+
problems.push(`${OVERRIDES_FILE}: rule id "${written}" exists in BOTH your global and project rules, so the override was NOT applied — it would have silently disabled both. Write "project:${written}" or "global:${written}" instead.`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
bySourceAndId.set(`${[...sources][0]}:${written}`, entry);
|
|
81
|
+
}
|
|
82
|
+
return { bySourceAndId, problems };
|
|
83
|
+
}
|
|
84
|
+
function isNonEmptyString(v) {
|
|
85
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Reads and validates the override file. Never throws: a broken override
|
|
89
|
+
* file must not take down a check that would otherwise have worked, and
|
|
90
|
+
* an unreadable file means "no overrides", never "override everything".
|
|
91
|
+
* Every rejection is reported rather than swallowed, so a user whose
|
|
92
|
+
* override isn't working finds out why.
|
|
93
|
+
*/
|
|
94
|
+
export function loadOverrides(cwd) {
|
|
95
|
+
const byRuleId = new Map();
|
|
96
|
+
const problems = [];
|
|
97
|
+
const path = join(cwd, OVERRIDES_FILE);
|
|
98
|
+
if (!existsSync(path))
|
|
99
|
+
return { byRuleId, problems };
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
problems.push(`${OVERRIDES_FILE} isn't valid JSON, so no overrides were applied: ${err instanceof Error ? err.message : String(err)}`);
|
|
106
|
+
return { byRuleId, problems };
|
|
107
|
+
}
|
|
108
|
+
const raw = parsed?.overrides;
|
|
109
|
+
if (raw === undefined)
|
|
110
|
+
return { byRuleId, problems };
|
|
111
|
+
if (!Array.isArray(raw)) {
|
|
112
|
+
problems.push(`${OVERRIDES_FILE}: "overrides" must be an array, so no overrides were applied.`);
|
|
113
|
+
return { byRuleId, problems };
|
|
114
|
+
}
|
|
115
|
+
for (const [i, entry] of raw.entries()) {
|
|
116
|
+
const e = entry;
|
|
117
|
+
if (!isNonEmptyString(e?.rule)) {
|
|
118
|
+
problems.push(`${OVERRIDES_FILE} entry ${i + 1}: missing a "rule" id, so it was ignored.`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Fails closed, on purpose. An override with no stated reason is the
|
|
122
|
+
// exact shape of one added to make a number go away.
|
|
123
|
+
if (!isNonEmptyString(e?.reason)) {
|
|
124
|
+
problems.push(`${OVERRIDES_FILE} entry for rule ${e.rule}: no "reason" given, so it was NOT applied. Every override needs a reason a reviewer can read.`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
byRuleId.set(e.rule.trim(), {
|
|
128
|
+
rule: e.rule.trim(),
|
|
129
|
+
reason: e.reason.trim(),
|
|
130
|
+
date: isNonEmptyString(e.date) ? e.date.trim() : undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return { byRuleId, problems };
|
|
134
|
+
}
|
|
@@ -103,6 +103,13 @@ function renderResultRow(result, all) {
|
|
|
103
103
|
</div>
|
|
104
104
|
<h3 class="result__title">${clean(result.ruleTitle)}</h3>
|
|
105
105
|
${result.evidence ? `<p class="result__evidence">${clean(result.evidence)}</p>` : ""}
|
|
106
|
+
${result.overriddenReason
|
|
107
|
+
? `<div class="override">
|
|
108
|
+
<strong>Marked by the developer as “not a rule for this project”${result.overriddenDate ? ` on ${clean(result.overriddenDate)}` : ""}.</strong>
|
|
109
|
+
Reason given: ${clean(result.overriddenReason)}
|
|
110
|
+
<span class="override__truth">Without this override, the result is: <strong>${clean(BUCKET_LABEL[bucketOf({ ...result, overriddenReason: undefined })])}</strong></span>
|
|
111
|
+
</div>`
|
|
112
|
+
: ""}
|
|
106
113
|
</article>`;
|
|
107
114
|
}
|
|
108
115
|
function renderSection(bucket, results, all) {
|
|
@@ -202,6 +209,9 @@ export function generateHtmlReport(results, meta) {
|
|
|
202
209
|
.result--judgment { border-left-color: #6b6f76; }
|
|
203
210
|
.badge--judgment { background: #f2f3f5; color: #4a4e55; }
|
|
204
211
|
.section__note { font-size: 13px; color: var(--muted); margin: -4px 0 12px; }
|
|
212
|
+
.override { margin-top: 10px; padding: 10px 12px; border-radius: 6px; background: var(--unclear-bg); border: 1px solid var(--unclear-line); font-size: 13.5px; color: var(--ink-soft); }
|
|
213
|
+
.override strong { color: var(--ink); }
|
|
214
|
+
.override__truth { display: block; margin-top: 6px; }
|
|
205
215
|
.result__id { font-size: 12px; color: var(--muted); }
|
|
206
216
|
.result__title { font-size: 15px; margin: 0 0 6px; font-weight: 600; }
|
|
207
217
|
.result__evidence { margin: 0; font-size: 14px; color: var(--muted); white-space: pre-wrap; }
|
|
@@ -232,7 +242,7 @@ export function generateHtmlReport(results, meta) {
|
|
|
232
242
|
|
|
233
243
|
<div class="verdict verdict--${v.cls}">
|
|
234
244
|
<strong>${clean(v.text)}</strong>
|
|
235
|
-
<span>${countBy(results, "PASS")} followed · ${countBy(results, "FAIL")} not followed · ${results.filter((r) => bucketOf(r) === "UNCLEAR_EVIDENCE").length} couldn't tell · ${results.filter((r) => bucketOf(r) === "UNCLEAR_JUDGMENT").length} need your judgment</span>
|
|
245
|
+
<span>${countBy(results, "PASS")} followed · ${countBy(results, "FAIL")} not followed · ${results.filter((r) => bucketOf(r) === "UNCLEAR_EVIDENCE").length} couldn't tell · ${results.filter((r) => bucketOf(r) === "UNCLEAR_JUDGMENT").length} need your judgment${results.filter((r) => r.overriddenReason).length > 0 ? ` · ${results.filter((r) => r.overriddenReason).length} developer-overridden` : ""}</span>
|
|
236
246
|
</div>
|
|
237
247
|
|
|
238
248
|
<table class="facts">
|
|
@@ -64,11 +64,21 @@ function summaryLine(results) {
|
|
|
64
64
|
const fail = results.filter((r) => r.status === "FAIL").length;
|
|
65
65
|
const needsHuman = results.filter((r) => r.status === "UNCLEAR" && r.needsHuman).length;
|
|
66
66
|
const couldntTell = results.filter((r) => r.status === "UNCLEAR" && !r.needsHuman).length;
|
|
67
|
+
const overriddenFails = results.filter((r) => r.status === "FAIL" && r.overriddenReason).length;
|
|
68
|
+
const overridden = results.filter((r) => r.overriddenReason).length;
|
|
67
69
|
const parts = [`${pass} followed`, `${fail} not followed`];
|
|
68
70
|
if (couldntTell > 0)
|
|
69
71
|
parts.push(`${couldntTell} couldn't tell`);
|
|
70
72
|
if (needsHuman > 0)
|
|
71
73
|
parts.push(`${needsHuman} need your judgment`);
|
|
74
|
+
// Named in the one line everyone reads. A headline that quietly folded
|
|
75
|
+
// overridden failures into a clean total would be the single most
|
|
76
|
+
// misleading thing this tool could print.
|
|
77
|
+
if (overridden > 0) {
|
|
78
|
+
parts.push(overriddenFails > 0
|
|
79
|
+
? `${overridden} user-overridden (${overriddenFails} still not followed)`
|
|
80
|
+
: `${overridden} user-overridden`);
|
|
81
|
+
}
|
|
72
82
|
return parts.join(" · ");
|
|
73
83
|
}
|
|
74
84
|
export function generateReport(results, meta) {
|
|
@@ -80,6 +90,15 @@ export function generateReport(results, meta) {
|
|
|
80
90
|
lines.push(`${MARK[r.status]} ${r.status.padEnd(7)} ${ruleLabel(r, clean)}`);
|
|
81
91
|
if (r.evidence)
|
|
82
92
|
lines.push(` evidence: ${r.evidence}`);
|
|
93
|
+
// The true status is printed above, unchanged. This line adds the
|
|
94
|
+
// override on top of it rather than replacing it — a reader must be
|
|
95
|
+
// able to see what the result would have been without the override.
|
|
96
|
+
if (r.overriddenReason) {
|
|
97
|
+
lines.push(` USER-OVERRIDDEN as "not a rule for this project"${r.overriddenDate ? ` on ${r.overriddenDate}` : ""} — reason: ${r.overriddenReason}`);
|
|
98
|
+
if (r.status === "FAIL") {
|
|
99
|
+
lines.push(" NOTE: this rule was NOT FOLLOWED. The override does not change that.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
83
102
|
}
|
|
84
103
|
lines.push("─".repeat(40));
|
|
85
104
|
lines.push(summaryLine(clean));
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,15 @@ export interface CheckResult {
|
|
|
32
32
|
ruleSource: "global" | "project";
|
|
33
33
|
status: CheckStatus;
|
|
34
34
|
evidence: string;
|
|
35
|
+
/**
|
|
36
|
+
* Set when the user marked this rule as "not a rule for my project" in
|
|
37
|
+
* .rulereceipt.json. The status above is STILL the real, computed
|
|
38
|
+
* result — an override changes presentation only, never the answer.
|
|
39
|
+
* See src/overrides.ts for why that distinction is the whole design.
|
|
40
|
+
*/
|
|
41
|
+
overriddenReason?: string;
|
|
42
|
+
/** Optional date from the override entry, so a reader can judge staleness. */
|
|
43
|
+
overriddenDate?: string;
|
|
35
44
|
/**
|
|
36
45
|
* True when this rule was never mechanically answerable — a judgment
|
|
37
46
|
* call like "surface bad news first", which has no command to inspect.
|