rulereceipt 0.1.48 → 0.1.49

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
@@ -393,6 +393,28 @@ report to the path you name, and `rules --include/--exclude` records a
393
393
  correction in `.rulereceipt/overrides.json`. Plain `rulereceipt check` writes
394
394
  nothing and makes no network calls.
395
395
 
396
+ **Severity, per rule.** A committed, team-shared `.rulereceipt/config.json`
397
+ sets how hard each rule bites in CI, by its stable handle (from `rulereceipt
398
+ rules --list`):
399
+
400
+ ```json
401
+ {
402
+ "rules": {
403
+ "a1b2c3": "off", // hidden from the report, never gates
404
+ "d4e5f6": "warn", // shown, but does not fail the build
405
+ "97h8i9": "error" // shown, FAILS the build — the default for a checkable rule
406
+ }
407
+ }
408
+ ```
409
+
410
+ No config means today's behaviour: every checkable FAIL is an `error`. This is
411
+ the one place severity lives — a team marks the must-not-break rules `error`
412
+ and the nice-to-haves `warn`, so CI gates on what matters instead of going red
413
+ on day one. (Refusing a command *before* it runs is separate, and stays with
414
+ the guard's `rules --forbid` clause-mark — a config that could block on any
415
+ rule would refuse far too much.) The older `{"warn": ["a1b2c3"]}` list still
416
+ works and means the same as `"warn"` above.
417
+
396
418
  **You can verify the package came from this source.** Every release from
397
419
  0.1.19 on is built and published by GitHub Actions and signed with
398
420
  [npm provenance](https://docs.npmjs.com/generating-provenance-statements),
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ import { maybeShowWhatsNew } from "./whatsNew.js";
33
33
  import { verifyReceipt, parseReceipt } from "./receipt.js";
34
34
  import { buildBadge } from "./badge.js";
35
35
  import { buildInitGuidance } from "./init.js";
36
- import { loadProjectConfig, handleMap, blockingFailures, warningFailures, PROJECT_CONFIG_PATH } from "./projectConfig.js";
36
+ import { loadProjectConfig, handleMap, blockingFailures, warningFailures, visibleResults, PROJECT_CONFIG_PATH } from "./projectConfig.js";
37
37
  import { maybeCheckUpdates, isUpdateCheckEnabled } from "./updateCheck.js";
38
38
  import { generateDigest } from "./digest.js";
39
39
  import { enableSchedule, disableSchedule, scheduleStatus } from "./schedule.js";
@@ -264,12 +264,14 @@ async function runCheck(opts) {
264
264
  // sends only a random install ID, never rule text or transcript content,
265
265
  // regardless of --llm.
266
266
  const judgmentResults = llm ? await runJudgmentChecks(judgment, events) : judgment.map(({ rule }) => needsLlmResult(rule));
267
- const results = [...deterministicResults, ...judgmentResults];
268
- // Severity: rules a team marked as warnings in .rulereceipt/config.json are
269
- // still reported but do not fail the build. handleFor maps a result back to
270
- // its stable handle so the mark survives edits that renumber rule ids.
267
+ const rawResults = [...deterministicResults, ...judgmentResults];
268
+ // Severity ladder from .rulereceipt/config.json (per rule handle): `off`
269
+ // rules are hidden entirely, `warn` rules are shown but do not fail the
270
+ // build, everything else is `error` (the default). handleFor maps a result
271
+ // back to its stable handle so the mark survives edits that renumber ids.
271
272
  const projectConfig = loadProjectConfig(cwd);
272
273
  const handleFor = handleMap(rules);
274
+ const results = visibleResults(rawResults, projectConfig, handleFor);
273
275
  const blockingFails = blockingFailures(results, projectConfig, handleFor);
274
276
  const warnedFails = warningFailures(results, projectConfig, handleFor);
275
277
  const meta = { sessionFilePath, ruleCount: results.length };
@@ -2,24 +2,49 @@ import type { CheckResult, Rule } from "./types.js";
2
2
  /**
3
3
  * A committed, team-shared config at .rulereceipt/config.json.
4
4
  *
5
- * Today it holds one thing: rule handles to treat as WARNINGS — a broken
6
- * "warning" rule is still reported, but it does not fail the build. This is
7
- * the honest version of "severity": a team marks the must-not-break rules as
8
- * errors (the default) and the nice-to-have ones as warnings, so CI gates on
9
- * what actually matters instead of going red on day one.
5
+ * ONE place for "how hard should this rule bite", a ladder per rule handle:
6
+ *
7
+ * off hidden from the report, never gates CI
8
+ * warn shown, does not fail the build
9
+ * error shown, FAILS the build (the default for a checkable rule)
10
+ *
11
+ * This is the honest version of "severity": a team marks must-not-break rules
12
+ * as errors (the default) and nice-to-haves as warnings, and silences the
13
+ * irrelevant ones, so CI gates on what actually matters instead of going red
14
+ * on day one. It replaces three scattered mechanisms — the old `warn` list,
15
+ * `rules --exclude` (now `off`), and the plain default — with one field.
16
+ *
17
+ * Pre-run BLOCKING is deliberately NOT a mode here: refusing a command before
18
+ * it runs still goes through the guard's clause-mark (`rules --forbid`),
19
+ * because a config that could block on any rule would refuse the 62.8% of
20
+ * commands the measured guard already showed it must not. This file governs
21
+ * the report and the CI gate; the guard governs refusal.
10
22
  *
11
23
  * Handles, not rule ids: an id is positional and renumbers when the file is
12
24
  * edited above it; a handle is a content hash, so it survives edits. Get one
13
25
  * from `rulereceipt rules --list`.
26
+ *
27
+ * Backward compatible: the old top-level `warn: [handle, ...]` list still
28
+ * works and means the same as `rules: { <handle>: "warn" }`.
14
29
  */
30
+ export type RuleMode = "off" | "warn" | "error";
15
31
  export interface ProjectConfig {
16
32
  warn: string[];
33
+ rules: Record<string, RuleMode>;
17
34
  }
18
35
  export declare const PROJECT_CONFIG_PATH: string;
19
36
  export declare function loadProjectConfig(cwd: string): ProjectConfig;
37
+ /**
38
+ * The mode for one result. `rules` wins over the legacy `warn` list; anything
39
+ * unlisted is `error`, so the default is unchanged and no config means today's
40
+ * behaviour exactly.
41
+ */
42
+ export declare function modeForResult(result: CheckResult, config: ProjectConfig, handleFor: (r: CheckResult) => string): RuleMode;
43
+ /** Results the report should show — everything except rules set to `off`. */
44
+ export declare function visibleResults(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
20
45
  /** A lookup from a result back to its stable rule handle, built from the loaded rules. */
21
46
  export declare function handleMap(rules: Rule[]): (r: CheckResult) => string;
22
- /** FAILs that are NOT configured as warnings — these fail the build. */
47
+ /** FAILs at `error` mode — these fail the build. (`off` never reaches here.) */
23
48
  export declare function blockingFailures(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
24
- /** FAILs that ARE configured as warnings — shown, but they do not fail the build. */
49
+ /** FAILs at `warn` mode — shown, but they do not fail the build. */
25
50
  export declare function warningFailures(results: CheckResult[], config: ProjectConfig, handleFor: (r: CheckResult) => string): CheckResult[];
@@ -1,19 +1,44 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { ruleFingerprint } from "./overrides.js";
4
+ const VALID_MODES = ["off", "warn", "error"];
4
5
  export const PROJECT_CONFIG_PATH = join(".rulereceipt", "config.json");
5
6
  export function loadProjectConfig(cwd) {
6
7
  try {
7
8
  const parsed = JSON.parse(readFileSync(join(cwd, PROJECT_CONFIG_PATH), "utf-8"));
8
- const warn = parsed?.warn;
9
- return { warn: Array.isArray(warn) ? warn.filter((x) => typeof x === "string") : [] };
9
+ const warn = Array.isArray(parsed?.warn) ? parsed.warn.filter((x) => typeof x === "string") : [];
10
+ const rules = {};
11
+ if (parsed?.rules && typeof parsed.rules === "object" && !Array.isArray(parsed.rules)) {
12
+ for (const [handle, mode] of Object.entries(parsed.rules)) {
13
+ if (typeof mode === "string" && VALID_MODES.includes(mode))
14
+ rules[handle] = mode;
15
+ }
16
+ }
17
+ return { warn, rules };
10
18
  }
11
19
  catch {
12
20
  // Missing or malformed config means no severities configured, never an
13
21
  // error — same fail-open discipline as the rest of the tool.
14
- return { warn: [] };
22
+ return { warn: [], rules: {} };
15
23
  }
16
24
  }
25
+ /**
26
+ * The mode for one result. `rules` wins over the legacy `warn` list; anything
27
+ * unlisted is `error`, so the default is unchanged and no config means today's
28
+ * behaviour exactly.
29
+ */
30
+ export function modeForResult(result, config, handleFor) {
31
+ const handle = handleFor(result);
32
+ if (config.rules[handle])
33
+ return config.rules[handle];
34
+ if (config.warn.includes(handle))
35
+ return "warn";
36
+ return "error";
37
+ }
38
+ /** Results the report should show — everything except rules set to `off`. */
39
+ export function visibleResults(results, config, handleFor) {
40
+ return results.filter((r) => modeForResult(r, config, handleFor) !== "off");
41
+ }
17
42
  /** A lookup from a result back to its stable rule handle, built from the loaded rules. */
18
43
  export function handleMap(rules) {
19
44
  const m = new Map();
@@ -21,11 +46,11 @@ export function handleMap(rules) {
21
46
  m.set(`${rule.source}:${rule.id}`, ruleFingerprint(rule));
22
47
  return (r) => m.get(`${r.ruleSource}:${r.ruleId}`) ?? "";
23
48
  }
24
- /** FAILs that are NOT configured as warnings — these fail the build. */
49
+ /** FAILs at `error` mode — these fail the build. (`off` never reaches here.) */
25
50
  export function blockingFailures(results, config, handleFor) {
26
- return results.filter((r) => r.status === "FAIL" && !config.warn.includes(handleFor(r)));
51
+ return results.filter((r) => r.status === "FAIL" && modeForResult(r, config, handleFor) === "error");
27
52
  }
28
- /** FAILs that ARE configured as warnings — shown, but they do not fail the build. */
53
+ /** FAILs at `warn` mode — shown, but they do not fail the build. */
29
54
  export function warningFailures(results, config, handleFor) {
30
- return results.filter((r) => r.status === "FAIL" && config.warn.includes(handleFor(r)));
55
+ return results.filter((r) => r.status === "FAIL" && modeForResult(r, config, handleFor) === "warn");
31
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "Checks whether a Claude Code session actually followed your CLAUDE.md / AGENTS.md rules, with evidence.",
5
5
  "repository": {
6
6
  "type": "git",