rulereceipt 0.1.25 → 0.1.26

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Entry point for the in-browser checker on the site.
3
+ *
4
+ * The privacy claim on that page — "your file never leaves this page" — is
5
+ * only true because everything reachable from here is pure string work.
6
+ * parseClaudeMdText and classifyRules touch no Node API, no network, and
7
+ * no storage. If anything in this dependency chain ever gains an import
8
+ * that does, the page's claim becomes false and must change with it.
9
+ *
10
+ * Deliberately returns counts and a small sample, never the file: nothing
11
+ * here should make it easy to accidentally send the text somewhere.
12
+ */
13
+ export interface Breakdown {
14
+ kind: string;
15
+ count: number;
16
+ }
17
+ export interface AnalysisResult {
18
+ /** Everything the parser pulled out of the file, rules and non-rules alike. */
19
+ parsed: number;
20
+ /** Items carrying no instruction — documentation, tables, listings. */
21
+ notRules: number;
22
+ /** Items that are genuinely directives. */
23
+ realRules: number;
24
+ /** Real rules a check can answer by looking at what ran. */
25
+ checkable: number;
26
+ /** Real rules that need a person. */
27
+ judgment: number;
28
+ /** Share of the whole file that isn't an instruction, 0-100. */
29
+ notRulesPct: number;
30
+ /** Share of REAL rules that are mechanically answerable, 0-100. */
31
+ checkablePct: number;
32
+ byKind: Breakdown[];
33
+ /** A few non-rule titles, so the number is inspectable rather than asserted. */
34
+ notRuleSamples: string[];
35
+ /** A few judgment-call titles, same reason. */
36
+ judgmentSamples: string[];
37
+ }
38
+ /** Measured across 559 real public rules files. Shown for comparison. */
39
+ export declare const CORPUS: {
40
+ files: number;
41
+ parsed: number;
42
+ notRulesPct: number;
43
+ checkablePct: number;
44
+ };
45
+ export declare function analyze(text: string): AnalysisResult;
@@ -0,0 +1,49 @@
1
+ import { parseClaudeMdText } from "../parsers/claudeMdParser.js";
2
+ import { classifyRules } from "../checks/classify.js";
3
+ /** Measured across 559 real public rules files. Shown for comparison. */
4
+ export const CORPUS = {
5
+ files: 559,
6
+ parsed: 23704,
7
+ notRulesPct: 62.9,
8
+ checkablePct: 43.5,
9
+ };
10
+ function pct(part, whole) {
11
+ return whole === 0 ? 0 : Math.round((part / whole) * 1000) / 10;
12
+ }
13
+ export function analyze(text) {
14
+ const rules = parseClaudeMdText(text, "project");
15
+ const classified = classifyRules(rules);
16
+ const counts = new Map();
17
+ const notRuleSamples = [];
18
+ const judgmentSamples = [];
19
+ for (const c of classified) {
20
+ counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
21
+ const title = c.rule.title.trim();
22
+ if (!title)
23
+ continue;
24
+ if (c.kind === "notARule" && notRuleSamples.length < 4)
25
+ notRuleSamples.push(title);
26
+ if (c.kind === "judgment" && judgmentSamples.length < 4)
27
+ judgmentSamples.push(title);
28
+ }
29
+ const parsed = classified.length;
30
+ const notRules = counts.get("notARule") ?? 0;
31
+ const judgment = counts.get("judgment") ?? 0;
32
+ const realRules = parsed - notRules;
33
+ const checkable = realRules - judgment;
34
+ const byKind = [...counts.entries()]
35
+ .map(([kind, count]) => ({ kind, count }))
36
+ .sort((a, b) => b.count - a.count);
37
+ return {
38
+ parsed,
39
+ notRules,
40
+ realRules,
41
+ checkable,
42
+ judgment,
43
+ notRulesPct: pct(notRules, parsed),
44
+ checkablePct: pct(checkable, realRules),
45
+ byKind,
46
+ notRuleSamples,
47
+ judgmentSamples,
48
+ };
49
+ }
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import { join, dirname, resolve, isAbsolute } from "node:path";
6
6
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
- import { parseClaudeMd } from "./parsers/claudeMdParser.js";
8
+ import { parseClaudeMd } from "./parsers/readClaudeMd.js";
9
9
  import { readLatestTranscript, readTranscriptFromFile, findLatestSessionFile } from "./parsers/transcriptParser.js";
10
10
  import { loadRules } from "./rules.js";
11
11
  import { classifyRules } from "./checks/classify.js";
@@ -1,2 +1,11 @@
1
1
  import type { Rule } from "../types.js";
2
- export declare function parseClaudeMd(filePath: string, source: "global" | "project"): Rule[];
2
+ /**
3
+ * The actual parser, taking text rather than a path.
4
+ *
5
+ * Split out from parseClaudeMd so the parsing logic carries no filesystem
6
+ * dependency and can run anywhere a string can — including a browser, for
7
+ * the client-side checker on the site. That checker's privacy claim
8
+ * ("your file never leaves the page") is only true because nothing in
9
+ * this function or in classify.ts touches Node APIs; keep it that way.
10
+ */
11
+ export declare function parseClaudeMdText(raw: string, source: "global" | "project"): Rule[];
@@ -1,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  /**
3
2
  * Real CLAUDE.md/AGENTS.md files use several different conventions for
4
3
  * organizing rules. Verified against real files and templates: Anthropic's
@@ -67,14 +66,16 @@ function normalizeSetextHeaders(lines) {
67
66
  }
68
67
  return out;
69
68
  }
70
- export function parseClaudeMd(filePath, source) {
71
- let raw;
72
- try {
73
- raw = readFileSync(filePath, "utf-8");
74
- }
75
- catch {
76
- return [];
77
- }
69
+ /**
70
+ * The actual parser, taking text rather than a path.
71
+ *
72
+ * Split out from parseClaudeMd so the parsing logic carries no filesystem
73
+ * dependency and can run anywhere a string can — including a browser, for
74
+ * the client-side checker on the site. That checker's privacy claim
75
+ * ("your file never leaves the page") is only true because nothing in
76
+ * this function or in classify.ts touches Node APIs; keep it that way.
77
+ */
78
+ export function parseClaudeMdText(raw, source) {
78
79
  const lines = normalizeSetextHeaders(raw.split("\n"));
79
80
  const rules = [];
80
81
  // `current` accumulates a numbered-header rule, a bold-rule-header rule,
@@ -0,0 +1,19 @@
1
+ import type { Rule } from "../types.js";
2
+ /**
3
+ * The filesystem half of rules-file parsing, deliberately kept in its own
4
+ * module.
5
+ *
6
+ * claudeMdParser.ts must stay free of Node imports so it can be bundled
7
+ * for the browser — the in-page checker on the site claims the pasted file
8
+ * never leaves the page, and that is only true while nothing reachable
9
+ * from the parser can perform I/O. Keeping the one readFileSync here
10
+ * means a bundler cannot pull `node:fs` in behind it, and a future import
11
+ * that breaks the guarantee has to be added here, visibly, rather than
12
+ * appearing by accident in the parser.
13
+ */
14
+ /**
15
+ * Reads a rules file from disk. Thin wrapper: an unreadable file is an
16
+ * empty rule list, never a throw, because a missing global CLAUDE.md is a
17
+ * normal state rather than an error.
18
+ */
19
+ export declare function parseClaudeMd(filePath: string, source: "global" | "project"): Rule[];
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseClaudeMdText } from "./claudeMdParser.js";
3
+ /**
4
+ * The filesystem half of rules-file parsing, deliberately kept in its own
5
+ * module.
6
+ *
7
+ * claudeMdParser.ts must stay free of Node imports so it can be bundled
8
+ * for the browser — the in-page checker on the site claims the pasted file
9
+ * never leaves the page, and that is only true while nothing reachable
10
+ * from the parser can perform I/O. Keeping the one readFileSync here
11
+ * means a bundler cannot pull `node:fs` in behind it, and a future import
12
+ * that breaks the guarantee has to be added here, visibly, rather than
13
+ * appearing by accident in the parser.
14
+ */
15
+ /**
16
+ * Reads a rules file from disk. Thin wrapper: an unreadable file is an
17
+ * empty rule list, never a throw, because a missing global CLAUDE.md is a
18
+ * normal state rather than an error.
19
+ */
20
+ export function parseClaudeMd(filePath, source) {
21
+ let raw;
22
+ try {
23
+ raw = readFileSync(filePath, "utf-8");
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ return parseClaudeMdText(raw, source);
29
+ }
package/dist/rules.d.ts CHANGED
@@ -5,5 +5,15 @@ import type { Rule } from "./types.js";
5
5
  * global CLAUDE.md under its own home dir (e.g. ~/.claude-office/CLAUDE.md).
6
6
  * Real gap found 2026-08-30, same root cause as the transcript-lookup fix
7
7
  * in transcriptParser.ts: hardcoding one home-dir name misses any variant.
8
+ *
9
+ * Also reads ~/.claude/rules/*.md, the documented location for personal
10
+ * rules that apply across every project.
11
+ *
12
+ * NOT covered, and stated rather than left silent: machine-wide managed
13
+ * enterprise policy files (/Library/Application Support/ClaudeCode,
14
+ * /etc/claude-code, C:\Program Files\ClaudeCode). Those are deployed by
15
+ * IT, exist on no development machine this can be tested against, and
16
+ * guessing at their location would be the kind of unverified assumption
17
+ * this project has already been bitten by twice.
8
18
  */
9
19
  export declare function loadRules(cwd: string): Rule[];
package/dist/rules.js CHANGED
@@ -1,9 +1,62 @@
1
1
  import { homedir } from "node:os";
2
2
  import { dirname, join, parse } from "node:path";
3
- import { existsSync } from "node:fs";
4
- import { parseClaudeMd } from "./parsers/claudeMdParser.js";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { parseClaudeMd } from "./parsers/readClaudeMd.js";
5
5
  import { findClaudeHomeDirNames } from "./parsers/transcriptParser.js";
6
- const RULE_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
6
+ /**
7
+ * Every place Claude Code actually reads a rule from, at one directory
8
+ * level. Order mirrors the documented load order, broadest first.
9
+ *
10
+ * Real gap found 2026-09-02: only the two bare filenames were read. A
11
+ * project with four rules files reported "1 rules checked · all passed" —
12
+ * three files invisible, with nothing saying so. A clean report on rules
13
+ * the tool never opened is the most misleading result this can produce,
14
+ * worse than no report, because it looks like evidence.
15
+ */
16
+ const RULE_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "CLAUDE.local.md", "AGENTS.local.md"];
17
+ const RULE_SUBDIR_FILES = [join(".claude", "CLAUDE.md"), join(".claude", "AGENTS.md")];
18
+ const RULE_DIRS = [join(".claude", "rules")];
19
+ /**
20
+ * Lists the markdown files in a rules directory, if it exists.
21
+ *
22
+ * Sorted so the same project always produces the same rule order — rule
23
+ * ids are positional, and an unstable order would renumber rules between
24
+ * runs on different machines, making two reports of the same session
25
+ * impossible to compare. Non-markdown files are skipped: a rules
26
+ * directory legitimately holds README fragments and notes.
27
+ */
28
+ function markdownFilesIn(dir) {
29
+ if (!existsSync(dir))
30
+ return [];
31
+ try {
32
+ if (!statSync(dir).isDirectory())
33
+ return [];
34
+ return readdirSync(dir)
35
+ .filter((f) => f.toLowerCase().endsWith(".md"))
36
+ .sort()
37
+ .map((f) => join(dir, f));
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ /** Every rules file at one directory level, in documented load order. */
44
+ function ruleFilesAtLevel(dir) {
45
+ const found = [];
46
+ for (const rel of RULE_SUBDIR_FILES) {
47
+ const p = join(dir, rel);
48
+ if (existsSync(p))
49
+ found.push(p);
50
+ }
51
+ for (const rel of RULE_DIRS)
52
+ found.push(...markdownFilesIn(join(dir, rel)));
53
+ for (const name of RULE_FILE_NAMES) {
54
+ const p = join(dir, name);
55
+ if (existsSync(p))
56
+ found.push(p);
57
+ }
58
+ return found;
59
+ }
7
60
  /**
8
61
  * Walks from the working directory up toward the repository root,
9
62
  * collecting rules files at every level.
@@ -25,11 +78,7 @@ function findProjectRuleFiles(cwd) {
25
78
  const home = homedir();
26
79
  let dir = cwd;
27
80
  for (;;) {
28
- for (const name of RULE_FILE_NAMES) {
29
- const p = join(dir, name);
30
- if (existsSync(p))
31
- found.push(p);
32
- }
81
+ found.push(...ruleFilesAtLevel(dir));
33
82
  // stop AT the repo root (inclusive) — its rules do apply
34
83
  if (existsSync(join(dir, ".git")))
35
84
  break;
@@ -48,9 +97,26 @@ function findProjectRuleFiles(cwd) {
48
97
  * global CLAUDE.md under its own home dir (e.g. ~/.claude-office/CLAUDE.md).
49
98
  * Real gap found 2026-08-30, same root cause as the transcript-lookup fix
50
99
  * in transcriptParser.ts: hardcoding one home-dir name misses any variant.
100
+ *
101
+ * Also reads ~/.claude/rules/*.md, the documented location for personal
102
+ * rules that apply across every project.
103
+ *
104
+ * NOT covered, and stated rather than left silent: machine-wide managed
105
+ * enterprise policy files (/Library/Application Support/ClaudeCode,
106
+ * /etc/claude-code, C:\Program Files\ClaudeCode). Those are deployed by
107
+ * IT, exist on no development machine this can be tested against, and
108
+ * guessing at their location would be the kind of unverified assumption
109
+ * this project has already been bitten by twice.
51
110
  */
52
111
  export function loadRules(cwd) {
53
- const rules = findClaudeHomeDirNames().flatMap((dirName) => parseClaudeMd(join(homedir(), dirName, "CLAUDE.md"), "global"));
112
+ const rules = [];
113
+ for (const dirName of findClaudeHomeDirNames()) {
114
+ const base = join(homedir(), dirName);
115
+ rules.push(...parseClaudeMd(join(base, "CLAUDE.md"), "global"));
116
+ for (const file of markdownFilesIn(join(base, "rules"))) {
117
+ rules.push(...parseClaudeMd(file, "global"));
118
+ }
119
+ }
54
120
  for (const path of findProjectRuleFiles(cwd)) {
55
121
  rules.push(...parseClaudeMd(path, "project"));
56
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulereceipt",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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",