rulereceipt 0.1.55 → 0.1.57
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/dist/audit.d.ts +28 -0
- package/dist/audit.js +44 -0
- package/dist/checks/classify.js +48 -1
- package/dist/cli.js +14 -0
- package/dist/rules.js +5 -1
- package/package.json +1 -1
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Rule } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A rules-only health score — how much of a rules file can actually be checked,
|
|
4
|
+
* with NO session needed.
|
|
5
|
+
*
|
|
6
|
+
* The recurring day-one gap: a first `check` with no session is empty, and a
|
|
7
|
+
* real CLAUDE.md is mostly a handbook — measured across the public corpus, ~38%
|
|
8
|
+
* of items are rules and ~56% of those need judgment. `audit` answers "is your
|
|
9
|
+
* rules file enforceable?" on any format (CLAUDE.md, AGENTS.md, Cursor, Copilot,
|
|
10
|
+
* Windsurf, Gemini) instantly, and points at `rules --advise` for the fixes.
|
|
11
|
+
*
|
|
12
|
+
* Buckets, by how classifyRule routes each item:
|
|
13
|
+
* - checkable : any structured/deterministic kind — a session can be checked
|
|
14
|
+
* against it without a human or an LLM
|
|
15
|
+
* - judgment : needs a person (or `--llm`)
|
|
16
|
+
* - skipped : not a rule (docs, directory maps, glossary rows)
|
|
17
|
+
*/
|
|
18
|
+
export interface RulesAudit {
|
|
19
|
+
total: number;
|
|
20
|
+
checkable: number;
|
|
21
|
+
judgment: number;
|
|
22
|
+
skipped: number;
|
|
23
|
+
/** checkable / (checkable + judgment), whole %, 0 when there are no rules. */
|
|
24
|
+
percentCheckable: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function auditRules(rules: Rule[]): RulesAudit;
|
|
27
|
+
/** A short, readable audit. Never says "compliant" — it measures the file, not a session. */
|
|
28
|
+
export declare function renderAudit(a: RulesAudit, md?: boolean): string;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { classifyRules } from "./checks/classify.js";
|
|
2
|
+
export function auditRules(rules) {
|
|
3
|
+
let checkable = 0;
|
|
4
|
+
let judgment = 0;
|
|
5
|
+
let skipped = 0;
|
|
6
|
+
for (const c of classifyRules(rules)) {
|
|
7
|
+
if (c.kind === "notARule")
|
|
8
|
+
skipped++;
|
|
9
|
+
else if (c.kind === "judgment")
|
|
10
|
+
judgment++;
|
|
11
|
+
else
|
|
12
|
+
checkable++;
|
|
13
|
+
}
|
|
14
|
+
const decided = checkable + judgment;
|
|
15
|
+
return {
|
|
16
|
+
total: checkable + judgment + skipped,
|
|
17
|
+
checkable,
|
|
18
|
+
judgment,
|
|
19
|
+
skipped,
|
|
20
|
+
percentCheckable: decided > 0 ? Math.round((checkable / decided) * 100) : 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** A short, readable audit. Never says "compliant" — it measures the file, not a session. */
|
|
24
|
+
export function renderAudit(a, md = false) {
|
|
25
|
+
if (a.checkable + a.judgment === 0) {
|
|
26
|
+
return md
|
|
27
|
+
? "**No rules found** in this project's rules files. Is there a `CLAUDE.md`, `AGENTS.md` or similar here?"
|
|
28
|
+
: "No rules found in this project's rules files.\nIs there a CLAUDE.md / AGENTS.md (or Cursor/Copilot/Windsurf rules) here?";
|
|
29
|
+
}
|
|
30
|
+
const H = (s) => (md ? `## ${s}` : s);
|
|
31
|
+
const out = [];
|
|
32
|
+
out.push(md ? "# RuleReceipt — rules audit" : "RuleReceipt · rules audit (no session needed)");
|
|
33
|
+
out.push("");
|
|
34
|
+
out.push(`${a.checkable + a.judgment} rules read (plus ${a.skipped} documentation item${a.skipped === 1 ? "" : "s"} not scored).`);
|
|
35
|
+
out.push("");
|
|
36
|
+
out.push(H("Can this session be checked against them?"));
|
|
37
|
+
out.push(` ${String(a.checkable).padStart(4)} checkable — verifiable from a session, no human needed`);
|
|
38
|
+
out.push(` ${String(a.judgment).padStart(4)} need judgment — a person (or \`--llm\`) decides these`);
|
|
39
|
+
out.push(` ${String(a.skipped).padStart(4)} documentation — structure/notes, not scored as rules`);
|
|
40
|
+
out.push("");
|
|
41
|
+
out.push(`${a.percentCheckable}% of your rules can be checked mechanically.`);
|
|
42
|
+
out.push("See which can't, and the smallest edit that would fix each: rulereceipt rules --advise");
|
|
43
|
+
return out.join("\n");
|
|
44
|
+
}
|
package/dist/checks/classify.js
CHANGED
|
@@ -293,6 +293,25 @@ const FILE_PATH_PATTERN = /^[^\s]*(\.(json|ya?ml|toml|md|env|lock|ini|cfg|conf|x
|
|
|
293
293
|
// file, not merely mentioning one (e.g. "read `config.yaml` before
|
|
294
294
|
// starting" names a path but isn't a protection rule).
|
|
295
295
|
const FILE_MUTATION_INTENT = /\b(modif|chang|edit|delet|remov|overwrit|touch|writ|creat|rename|mov)\w*\b/i;
|
|
296
|
+
/**
|
|
297
|
+
* A backtick literal distinctive enough to check INSIDE written code without
|
|
298
|
+
* matching ordinary prose. A single token (no whitespace), not a shell flag,
|
|
299
|
+
* carrying code punctuation (`. - / @ # :`) and at least three alphanumerics —
|
|
300
|
+
* so `lucide-react`, `#0af`, `@deprecated`, `react-dom` qualify, while plain
|
|
301
|
+
* words (`TODO`, `name`), shell commands (`git push --force`) and bare flags
|
|
302
|
+
* do not. Used to route a FORBID rule's token to codeContent: found in
|
|
303
|
+
* Write/Edit content it is an ACTION (a real FAIL), not a mention. Added
|
|
304
|
+
* 2026-09-28 to move import/value-style rules out of the UNCLEAR bucket.
|
|
305
|
+
*/
|
|
306
|
+
function isContentToken(literal) {
|
|
307
|
+
if (/\s/.test(literal))
|
|
308
|
+
return false;
|
|
309
|
+
if (literal.startsWith("-"))
|
|
310
|
+
return false;
|
|
311
|
+
if (!/[.\-/@#:]/.test(literal))
|
|
312
|
+
return false;
|
|
313
|
+
return (literal.match(/[A-Za-z0-9]/g) ?? []).length >= 3;
|
|
314
|
+
}
|
|
296
315
|
// Catches rules like "add tests for every change" or "every new function
|
|
297
316
|
// needs a test" - no literal backtick token to pattern-match, so without
|
|
298
317
|
// this they'd fall all the way through to judgment (an LLM call) even
|
|
@@ -485,6 +504,14 @@ function polarityWasInferred(rule) {
|
|
|
485
504
|
*/
|
|
486
505
|
const EMOJI_SUBJECT = /\bemojis?\b|\bemoticons?\b/i;
|
|
487
506
|
const EMOJI_FORBID = /\b(no|never|avoid|don't|do not|without|free of|refrain from|must not|shall not|not use|zero)\b/i;
|
|
507
|
+
// A permissive threshold means the rule allows SOME emoji — not a zero-ban the
|
|
508
|
+
// deterministic checker can decide.
|
|
509
|
+
const EMOJI_THRESHOLD = /\b(?:liberal|sparing|minimal|excessive|overus|one or two|a couple|a few|maximum|at most|no more than|too many|limit)\w*/i;
|
|
510
|
+
// A rule scoped only to an artifact the checker can't see (it reads the
|
|
511
|
+
// assistant's chat text, not posts/commits/READMEs) can't be verified from
|
|
512
|
+
// that text — unless it also names the chat/output the checker DOES see.
|
|
513
|
+
const EMOJI_OFFTARGET = /\b(?:posts?|commits?|pull requests?|prs?|readme|docs?|documentation|blog|articles?|captions?|changelog)\b/i;
|
|
514
|
+
const EMOJI_ONTARGET = /\b(?:output|repl(?:y|ies)|response|chat|message|answer|conversation|everywhere|anywhere)\b/i;
|
|
488
515
|
function isEmojiRule(rule) {
|
|
489
516
|
const text = `${rule.title} ${rule.text}`;
|
|
490
517
|
if (!EMOJI_SUBJECT.test(text))
|
|
@@ -497,7 +524,17 @@ function isEmojiRule(rule) {
|
|
|
497
524
|
if (!m || m.index === undefined)
|
|
498
525
|
return false;
|
|
499
526
|
const window = text.slice(Math.max(0, m.index - 60), m.index + 40);
|
|
500
|
-
|
|
527
|
+
if (!EMOJI_FORBID.test(window))
|
|
528
|
+
return false;
|
|
529
|
+
// Real false positive 2026-09-28: "one or two emojis per post is the maximum"
|
|
530
|
+
// (a THRESHOLD, about POSTS) failed on a ✅ in a chat reply. A permissive
|
|
531
|
+
// threshold, or a rule scoped only to an artifact this checker can't see,
|
|
532
|
+
// is not a confident deterministic FAIL — it goes to judgment.
|
|
533
|
+
if (EMOJI_THRESHOLD.test(text))
|
|
534
|
+
return false;
|
|
535
|
+
if (EMOJI_OFFTARGET.test(text) && !EMOJI_ONTARGET.test(text))
|
|
536
|
+
return false;
|
|
537
|
+
return true;
|
|
501
538
|
}
|
|
502
539
|
/**
|
|
503
540
|
* A rule that forbids an AI-authorship mark in git commits, PRs or comments.
|
|
@@ -668,6 +705,16 @@ export function classifyRule(rule) {
|
|
|
668
705
|
if (filePath && FILE_MUTATION_INTENT.test(text)) {
|
|
669
706
|
return { kind: "fileLifecycle", rule, filePath, polarity, polarityInferred };
|
|
670
707
|
}
|
|
708
|
+
// A forbid rule naming a distinctive token (an import, a value) is a real
|
|
709
|
+
// violation when the agent WRITES it — codeContent checks Write/Edit content
|
|
710
|
+
// only, so this is an action not a mention. Plain words and shell commands
|
|
711
|
+
// are excluded by isContentToken; protected files already routed above.
|
|
712
|
+
if (polarity === "forbid") {
|
|
713
|
+
const contentTokens = [...patterns].filter(isContentToken);
|
|
714
|
+
if (contentTokens.length > 0) {
|
|
715
|
+
return { kind: "codeContent", rule, patterns: contentTokens, polarity, polarityInferred };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
671
718
|
return { kind: "deterministic", rule, patterns: [...patterns], polarity, polarityInferred };
|
|
672
719
|
}
|
|
673
720
|
export function classifyRules(rules) {
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { adviseRules } from "./checkability.js";
|
|
|
13
13
|
import { shadowedAgentsMd } from "./shadowedAgents.js";
|
|
14
14
|
import { partitionByAge, futureResult } from "./ruleAge.js";
|
|
15
15
|
import { auditSessions, renderComplianceReport } from "./report/complianceReport.js";
|
|
16
|
+
import { auditRules, renderAudit } from "./audit.js";
|
|
16
17
|
import { classifyRules } from "./checks/classify.js";
|
|
17
18
|
import { loadOverrides, saveOverride, clearOverride, staleOverrides, ruleFingerprint, OVERRIDES_PATH } from "./overrides.js";
|
|
18
19
|
import { runDeterministicChecks } from "./checks/deterministicChecks.js";
|
|
@@ -891,6 +892,19 @@ program
|
|
|
891
892
|
const r = await auditSessions(process.cwd(), Number.isFinite(n) ? n : 25);
|
|
892
893
|
console.log(renderComplianceReport(r, Boolean(opts.markdown)));
|
|
893
894
|
});
|
|
895
|
+
program
|
|
896
|
+
.command("audit")
|
|
897
|
+
.description("Score your rules files for checkability — NO session needed. How much can be checked mechanically vs needs a human vs is documentation. Works on CLAUDE.md, AGENTS.md, Cursor, Copilot, Windsurf and Gemini rules.")
|
|
898
|
+
.option("--markdown", "output as markdown, for a report you can send")
|
|
899
|
+
.option("--json", "output machine-readable JSON (counts and the checkable %)")
|
|
900
|
+
.action((opts) => {
|
|
901
|
+
const a = auditRules(loadRules(process.cwd()));
|
|
902
|
+
if (opts.json) {
|
|
903
|
+
console.log(JSON.stringify(a, null, 2));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
console.log(renderAudit(a, Boolean(opts.markdown)));
|
|
907
|
+
});
|
|
894
908
|
program
|
|
895
909
|
.command("digest")
|
|
896
910
|
.description("A non-technical summary of recent check runs (counts only, no rule text) — for a manager who doesn't have time to read 30 individual reports.")
|
package/dist/rules.js
CHANGED
|
@@ -62,8 +62,12 @@ function ruleFilesAtLevel(dir) {
|
|
|
62
62
|
for (const rel of RULE_DIRS)
|
|
63
63
|
found.push(...markdownFilesIn(join(dir, rel)));
|
|
64
64
|
push("CLAUDE.md");
|
|
65
|
-
|
|
65
|
+
// AGENTS.md (and the singular AGENT.md some tools use) are read only when
|
|
66
|
+
// there's no CLAUDE.md at this level, mirroring Claude Code's shadow rule.
|
|
67
|
+
if (!has("CLAUDE.md")) {
|
|
66
68
|
push("AGENTS.md");
|
|
69
|
+
push("AGENT.md");
|
|
70
|
+
}
|
|
67
71
|
// .local variants: their precedence relative to the base files is not
|
|
68
72
|
// documented, so both are kept rather than guessing at a shadow rule.
|
|
69
73
|
push("CLAUDE.local.md");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulereceipt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"description": "Checks whether your AI coding agent followed your rules, with evidence. Works with Claude Code (Codex in testing); reads CLAUDE.md, AGENTS.md, Cursor, Copilot and Windsurf rules.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|