rulereceipt 0.1.21 → 0.1.23
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/checks/classify.js +42 -0
- package/dist/cli.js +10 -0
- package/dist/report/generateHtmlReport.js +33 -1
- package/package.json +1 -1
package/dist/checks/classify.js
CHANGED
|
@@ -31,7 +31,49 @@ const IMPERATIVE_INSTRUCTION = /(?:^|[.;:!?]\s+|^\s*[-*+]\s*|\n\s*[-*+]\s*)(use|
|
|
|
31
31
|
* with no instruction in it has nothing to check compliance against,
|
|
32
32
|
* whatever its punctuation.
|
|
33
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* A section whose TITLE announces a record of something that happened —
|
|
36
|
+
* an incident, a postmortem, a retrospective. These are written to
|
|
37
|
+
* explain history, not to instruct the agent.
|
|
38
|
+
*
|
|
39
|
+
* Found by running this tool on a real session (2026-08-31): a section
|
|
40
|
+
* titled "Real incident (2026-08-28): Vercel had the same office/personal
|
|
41
|
+
* mixup" was enforced as a REQUIRE rule whose pattern was an employer
|
|
42
|
+
* name pulled out of the narrative, so the report announced FOLLOWED
|
|
43
|
+
* because that name appeared somewhere in the session. The rule being
|
|
44
|
+
* "satisfied" was a sentence describing a past mistake.
|
|
45
|
+
*
|
|
46
|
+
* The existing directive test cannot catch these, and correctly so: a
|
|
47
|
+
* good incident note ends with the lesson ("Verify with `vercel whoami`
|
|
48
|
+
* before every deploy"), so it genuinely does contain a directive. What
|
|
49
|
+
* the section IS gets announced by its title, which is where this looks.
|
|
50
|
+
*
|
|
51
|
+
* Kept to a small closed class of words that name a record of an event,
|
|
52
|
+
* in the same spirit as DIRECTIVE_LANGUAGE above — a bounded property of
|
|
53
|
+
* language, not an enumeration of document formats. Enumerating formats
|
|
54
|
+
* is the mistake this project already made once and wrote up publicly.
|
|
55
|
+
*/
|
|
56
|
+
const EVENT_RECORD_TITLE = /\b(incident|post-?mortem|retro(spective)?|outage|what went wrong)\b/i;
|
|
57
|
+
/**
|
|
58
|
+
* A title that OPENS with an instruction is a rule, whatever it goes on
|
|
59
|
+
* to mention. "Never repeat the 2026-08-28 incident" is a directive that
|
|
60
|
+
* happens to name an incident; "Real incident (2026-08-28): ..." is a
|
|
61
|
+
* report that happens to contain the word never further along.
|
|
62
|
+
*/
|
|
63
|
+
const TITLE_OPENS_WITH_DIRECTIVE = /^\s*[-*+\d.\s]*(never|always|must|do not|don't|dont|avoid|ensure|prefer|only|make sure|be sure)\b/i;
|
|
64
|
+
function isEventRecord(rule) {
|
|
65
|
+
if (TITLE_OPENS_WITH_DIRECTIVE.test(rule.title))
|
|
66
|
+
return false;
|
|
67
|
+
if (IMPERATIVE_INSTRUCTION.test(rule.title))
|
|
68
|
+
return false;
|
|
69
|
+
return EVENT_RECORD_TITLE.test(rule.title);
|
|
70
|
+
}
|
|
34
71
|
function isNotARule(rule) {
|
|
72
|
+
// Checked before the directive test on purpose: an incident note that
|
|
73
|
+
// ends with its lesson contains a real directive, and would otherwise
|
|
74
|
+
// be enforced as though the history itself were the rule.
|
|
75
|
+
if (isEventRecord(rule))
|
|
76
|
+
return true;
|
|
35
77
|
const combined = `${rule.title} ${rule.text}`;
|
|
36
78
|
if (DIRECTIVE_LANGUAGE.test(combined))
|
|
37
79
|
return false;
|
package/dist/cli.js
CHANGED
|
@@ -116,6 +116,16 @@ function writeHtmlReport(results, meta, cwd, target) {
|
|
|
116
116
|
writeFileSync(outPath, html, "utf-8");
|
|
117
117
|
console.log(`\nShareable report written to ${outPath}`);
|
|
118
118
|
console.log("Open it in a browser, attach it to an email, or print it to PDF. It's a single self-contained file.");
|
|
119
|
+
// Found by dogfooding on a real session (2026-08-31): this file
|
|
120
|
+
// reproduces rule text and quoted evidence verbatim, which is exactly
|
|
121
|
+
// what makes it useful — and means it inherits whatever is in the
|
|
122
|
+
// rules file. A real CLAUDE.md turned out to contain an employer
|
|
123
|
+
// name, an office email, and absolute paths. Home paths are redacted
|
|
124
|
+
// automatically; nothing else can be, so say so plainly at the moment
|
|
125
|
+
// the file is created rather than burying it in a policy page.
|
|
126
|
+
console.log("\n⚠ This report includes your rule text and session evidence VERBATIM.\n" +
|
|
127
|
+
" Review it before sharing outside your team — only you know what's in your rules file.\n" +
|
|
128
|
+
" Nothing is auto-redacted: this tool cannot tell which of your own rules are sensitive.");
|
|
119
129
|
}
|
|
120
130
|
catch (err) {
|
|
121
131
|
console.log(`\n(--html: couldn't write ${outPath} — ${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { computeTranscriptHash } from "./generateReport.js";
|
|
3
4
|
/**
|
|
4
5
|
* Escapes the five characters that can break out of either an HTML text
|
|
@@ -21,9 +22,40 @@ function escapeHtml(value) {
|
|
|
21
22
|
function stripControlChars(value) {
|
|
22
23
|
return value.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, "");
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Replaces the user's home directory with `~`.
|
|
27
|
+
*
|
|
28
|
+
* Found by dogfooding on a real session (2026-08-31): the shareable
|
|
29
|
+
* report is the one output explicitly designed to be emailed to someone
|
|
30
|
+
* else, and it was printing absolute paths like
|
|
31
|
+
* /Users/<realname>/Desktop/... in both the project header and inside
|
|
32
|
+
* quoted evidence. For anyone publishing under a pseudonym — or simply
|
|
33
|
+
* anyone who would rather not hand a stranger their username and
|
|
34
|
+
* directory layout — that is a leak in the one artifact most likely to
|
|
35
|
+
* leave the machine.
|
|
36
|
+
*
|
|
37
|
+
* This is DISPLAY FORMATTING, not a safety mechanism, and the difference
|
|
38
|
+
* matters. It is a fixed, deterministic substitution of one known string
|
|
39
|
+
* — always correct, never guessing — and it makes paths easier to read as
|
|
40
|
+
* a side benefit. It protects nothing.
|
|
41
|
+
*
|
|
42
|
+
* Auto-detecting "sensitive" content and scrubbing it would be a losing
|
|
43
|
+
* game: it can never catch everything, and a partial scrub is worse than
|
|
44
|
+
* none because it invites the user to trust the output. Rule text and
|
|
45
|
+
* evidence are reproduced verbatim on purpose, because that is what makes
|
|
46
|
+
* the report worth sending. The real safeguard is the warning the CLI
|
|
47
|
+
* prints at write time, which puts the responsibility where it belongs —
|
|
48
|
+
* with the person who knows what is in their own rules file.
|
|
49
|
+
*/
|
|
50
|
+
function redactHome(value) {
|
|
51
|
+
const home = homedir();
|
|
52
|
+
if (!home || home === "/" || home.length < 4)
|
|
53
|
+
return value;
|
|
54
|
+
return value.split(home).join("~");
|
|
55
|
+
}
|
|
24
56
|
/** Single choke point: nothing untrusted reaches the document except through this. */
|
|
25
57
|
function clean(value) {
|
|
26
|
-
return escapeHtml(stripControlChars(value));
|
|
58
|
+
return escapeHtml(stripControlChars(redactHome(value)));
|
|
27
59
|
}
|
|
28
60
|
function bucketOf(result) {
|
|
29
61
|
if (result.status === "FAIL")
|