rulereceipt 0.1.38 → 0.1.40
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 +29 -8
- package/dist/checks/classify.d.ts +30 -0
- package/dist/checks/classify.js +5 -5
- package/dist/cli.js +77 -0
- package/dist/guard.js +49 -3
- package/dist/overrides.d.ts +33 -0
- package/dist/overrides.js +33 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,16 +168,37 @@ Read the limit before wiring it in, because it is most of the story. It
|
|
|
168
168
|
enforces rules naming a **file** or a **branch** — "never modify `.env`",
|
|
169
169
|
"never commit to `main`" — and nothing else.
|
|
170
170
|
|
|
171
|
-
It does **not** block banned
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
`npm run build`, which is its
|
|
171
|
+
It does **not** block a banned command unless you have said which command is
|
|
172
|
+
banned. That was the point of building it, and the automatic version did not
|
|
173
|
+
survive measurement: replaying 16,336 real tool calls against every forbidding
|
|
174
|
+
rule in a 559-file corpus, blocking on command literals refused 62.8% of them.
|
|
175
|
+
Narrowing twice reached 2.5%, and the residue was still wrong in a way no
|
|
176
|
+
matcher fixes — one rule refused `npm run build` 112 times, because it forbids
|
|
177
|
+
running Playwright unprompted and *recommends* `npm run build`, which is its
|
|
178
|
+
only command-shaped literal.
|
|
178
179
|
|
|
179
180
|
Nothing in a rules file marks which backtick is the prohibition. A report
|
|
180
|
-
survives that by saying UNCLEAR. A gate cannot
|
|
181
|
+
survives that by saying UNCLEAR. A gate cannot — so you mark it:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
rulereceipt rules --forbid <handle> --literal "git push --force"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Handles come from `rulereceipt rules --handles`. The mark is stored
|
|
188
|
+
against the rule's content hash, and the guard blocks on that literal and no
|
|
189
|
+
other. Three things it deliberately will not do:
|
|
190
|
+
|
|
191
|
+
- An **unmarked** rule cannot block, at any confidence, ever. There is no
|
|
192
|
+
fallback to "probably the first literal" — that fallback is the bug.
|
|
193
|
+
- **Rewording the rule drops the mark.** It would otherwise carry your
|
|
194
|
+
judgment onto words you never read.
|
|
195
|
+
- A mark naming a literal the rule no longer contains is **ignored**. A gate
|
|
196
|
+
refusing a command for a reason written nowhere is the worst failure a gate
|
|
197
|
+
has.
|
|
198
|
+
|
|
199
|
+
Of 99 forbidding rules in the corpus that name a command-shaped literal, only
|
|
200
|
+
43 have a prohibition that actually introduces one. The rest could never be
|
|
201
|
+
marked automatically, which is the point.
|
|
181
202
|
|
|
182
203
|
|
|
183
204
|
## Which rules actually have teeth
|
|
@@ -107,6 +107,36 @@ export interface ClaimEvidenceClassification {
|
|
|
107
107
|
rule: Rule;
|
|
108
108
|
}
|
|
109
109
|
export type Classification = ClaimEvidenceClassification | DeterministicClassification | IfEditThenTestClassification | GitBranchPolicyClassification | CodeContentClassification | FileLifecycleClassification | NotARuleClassification | JudgmentClassification;
|
|
110
|
+
export declare const DIRECTIVE_LANGUAGE: RegExp;
|
|
111
|
+
/**
|
|
112
|
+
* Imperative instruction — a bare command verb starting a clause ("Use
|
|
113
|
+
* `gh pr merge`", "Run the tests first", "Keep functions small"). This is
|
|
114
|
+
* the other way a real rule is written when it doesn't use a modal.
|
|
115
|
+
*
|
|
116
|
+
* Anchored to a clause start (line start, or after sentence/bullet
|
|
117
|
+
* punctuation) on purpose: the same verbs appear mid-sentence in pure
|
|
118
|
+
* documentation ("the CLI can run migrations"), where they describe a
|
|
119
|
+
* capability rather than instruct the agent.
|
|
120
|
+
*/
|
|
121
|
+
export declare const IMPERATIVE_INSTRUCTION: RegExp;
|
|
122
|
+
/**
|
|
123
|
+
* A title that OPENS with an instruction is a rule, whatever it goes on
|
|
124
|
+
* to mention. "Never repeat the 2026-08-28 incident" is a directive that
|
|
125
|
+
* happens to name an incident; "Real incident (2026-08-28): ..." is a
|
|
126
|
+
* report that happens to contain the word never further along.
|
|
127
|
+
*/
|
|
128
|
+
export declare const TITLE_OPENS_WITH_DIRECTIVE: RegExp;
|
|
129
|
+
export declare function isEventRecord(rule: Rule): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* A heading that labels a command, with the command as its whole body:
|
|
132
|
+
* "Build release APK" over `.\gradlew assembleRelease`. The title reads as
|
|
133
|
+
* an imperative, but nothing here constrains the agent — it is a how-to, and
|
|
134
|
+
* there is no compliance to check.
|
|
135
|
+
*
|
|
136
|
+
* Guarded by TITLE_OPENS_WITH_DIRECTIVE so a genuine prohibition whose body
|
|
137
|
+
* is the forbidden command ("Never run: `rm -rf /`") is still a rule.
|
|
138
|
+
*/
|
|
139
|
+
export declare function isCommandDocumentation(rule: Rule): boolean;
|
|
110
140
|
/**
|
|
111
141
|
* A rule is only treated as deterministic when it names a specific,
|
|
112
142
|
* literal, checkable token (a CLI flag, a command, an exact string) in
|
package/dist/checks/classify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Normative language — the thing that makes a line a rule rather than a
|
|
2
2
|
// description. Deliberately broad on modals AND imperative verbs, because
|
|
3
3
|
// a wrongly-excluded rule is a silent miss.
|
|
4
|
-
const DIRECTIVE_LANGUAGE = /\b(never|always|must|should|shall|do not|don't|dont|cannot|can't|required?|requires|ensure|avoid|prefer|forbidden|prohibited|only|make sure|be sure|need|needs|needed|need to|has to|have to|expected to|responsible for)\b/i;
|
|
4
|
+
export const DIRECTIVE_LANGUAGE = /\b(never|always|must|should|shall|do not|don't|dont|cannot|can't|required?|requires|ensure|avoid|prefer|forbidden|prohibited|only|make sure|be sure|need|needs|needed|need to|has to|have to|expected to|responsible for)\b/i;
|
|
5
5
|
/**
|
|
6
6
|
* Imperative instruction — a bare command verb starting a clause ("Use
|
|
7
7
|
* `gh pr merge`", "Run the tests first", "Keep functions small"). This is
|
|
@@ -12,7 +12,7 @@ const DIRECTIVE_LANGUAGE = /\b(never|always|must|should|shall|do not|don't|dont|
|
|
|
12
12
|
* documentation ("the CLI can run migrations"), where they describe a
|
|
13
13
|
* capability rather than instruct the agent.
|
|
14
14
|
*/
|
|
15
|
-
const IMPERATIVE_INSTRUCTION = /(?:^|[.;:!?]\s+|^\s*[-*+]\s*|\n\s*[-*+]\s*)(use|run|keep|write|add|remove|delete|check|verify|test|commit|document|update|create|follow|apply|include|exclude|handle|validate|escape|sanitize|log|report|raise|throw|return|call|invoke|split|group|sort|name|place|put|store|read|load|save|close|open|start|stop|restart|install|build|deploy|review|refactor|rename|move|copy|merge|rebase|squash|tag|branch|push|pull|fetch|clone|stage|stash|lead|state|explain|describe|list|show|surface|flag|mark|label|note|treat|assume|confirm|ask|wait|stick|limit|cap|batch|cache|mock|stub|assert|expect|measure|quantify|label)\b/i;
|
|
15
|
+
export const IMPERATIVE_INSTRUCTION = /(?:^|[.;:!?]\s+|^\s*[-*+]\s*|\n\s*[-*+]\s*)(use|run|keep|write|add|remove|delete|check|verify|test|commit|document|update|create|follow|apply|include|exclude|handle|validate|escape|sanitize|log|report|raise|throw|return|call|invoke|split|group|sort|name|place|put|store|read|load|save|close|open|start|stop|restart|install|build|deploy|review|refactor|rename|move|copy|merge|rebase|squash|tag|branch|push|pull|fetch|clone|stage|stash|lead|state|explain|describe|list|show|surface|flag|mark|label|note|treat|assume|confirm|ask|wait|stick|limit|cap|batch|cache|mock|stub|assert|expect|measure|quantify|label)\b/i;
|
|
16
16
|
/**
|
|
17
17
|
* Deliberately inverted: tests for the presence of a DIRECTIVE, never for
|
|
18
18
|
* the shape of documentation.
|
|
@@ -60,8 +60,8 @@ const EVENT_RECORD_TITLE = /\b(incident|post-?mortem|retro(spective)?|outage|wha
|
|
|
60
60
|
* happens to name an incident; "Real incident (2026-08-28): ..." is a
|
|
61
61
|
* report that happens to contain the word never further along.
|
|
62
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|no)\b/i;
|
|
64
|
-
function isEventRecord(rule) {
|
|
63
|
+
export const TITLE_OPENS_WITH_DIRECTIVE = /^\s*[-*+\d.\s]*(never|always|must|do not|don'?t|dont|avoid|ensure|prefer|only|make sure|be sure|no)\b/i;
|
|
64
|
+
export function isEventRecord(rule) {
|
|
65
65
|
if (TITLE_OPENS_WITH_DIRECTIVE.test(rule.title))
|
|
66
66
|
return false;
|
|
67
67
|
if (IMPERATIVE_INSTRUCTION.test(rule.title))
|
|
@@ -126,7 +126,7 @@ function looksLikeBareCommand(text) {
|
|
|
126
126
|
* Guarded by TITLE_OPENS_WITH_DIRECTIVE so a genuine prohibition whose body
|
|
127
127
|
* is the forbidden command ("Never run: `rm -rf /`") is still a rule.
|
|
128
128
|
*/
|
|
129
|
-
function isCommandDocumentation(rule) {
|
|
129
|
+
export function isCommandDocumentation(rule) {
|
|
130
130
|
if (TITLE_OPENS_WITH_DIRECTIVE.test(rule.title))
|
|
131
131
|
return false;
|
|
132
132
|
if (FLAG_DOCUMENTATION.test(rule.text) || FLAG_DOCUMENTATION.test(rule.title))
|
package/dist/cli.js
CHANGED
|
@@ -488,6 +488,80 @@ async function runRules(opts) {
|
|
|
488
488
|
}
|
|
489
489
|
return;
|
|
490
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Marking WHICH clause of a rule is the prohibition.
|
|
493
|
+
*
|
|
494
|
+
* The one thing a rules file never says. Blocking on every backtick in a
|
|
495
|
+
* forbidding rule refused 62.8% of 16,336 real tool calls, and the worst
|
|
496
|
+
* survivor after two narrowings refused `npm run build` 112 times against
|
|
497
|
+
* a rule that recommends it. So the guard blocks on nothing here until a
|
|
498
|
+
* person names the clause, and this is where they name it.
|
|
499
|
+
*/
|
|
500
|
+
if (opts.forbid) {
|
|
501
|
+
const rule = findRule(opts.forbid);
|
|
502
|
+
if (!rule) {
|
|
503
|
+
console.error(`No rule in this project has the handle ${opts.forbid}.`);
|
|
504
|
+
console.error(`Handles come from \`rulereceipt check --show-skipped\`, and change if the rule's wording changes.`);
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const literal = opts.literal?.trim();
|
|
509
|
+
if (!literal) {
|
|
510
|
+
console.error(`--forbid needs --literal "<the exact command this rule bans>".`);
|
|
511
|
+
console.error(`Copy it from the rule itself; it has to appear in the rule's text.`);
|
|
512
|
+
process.exitCode = 1;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!`${rule.title}\n${rule.text ?? ""}`.includes(literal)) {
|
|
516
|
+
console.error(`That rule does not contain "${literal}".`);
|
|
517
|
+
console.error(`The mark has to name something the rule actually says, or a gate would`);
|
|
518
|
+
console.error(`refuse a command for a reason written nowhere.`);
|
|
519
|
+
process.exitCode = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const prior = overrides.get(opts.forbid)?.forbids ?? [];
|
|
523
|
+
const forbids = [...new Set([...prior, literal])];
|
|
524
|
+
saveOverride(cwd, {
|
|
525
|
+
hash: opts.forbid,
|
|
526
|
+
decision: overrides.get(opts.forbid)?.decision ?? "rule",
|
|
527
|
+
title: rule.title.replace(/\s+/g, " ").trim().slice(0, 200),
|
|
528
|
+
forbids,
|
|
529
|
+
});
|
|
530
|
+
console.log(`Saved to ${OVERRIDES_PATH}.`);
|
|
531
|
+
console.log(` "${rule.title.replace(/\s+/g, " ").trim().slice(0, 90)}"`);
|
|
532
|
+
console.log(` now blocks on: ${forbids.map((f) => `\`${f}\``).join(", ")}`);
|
|
533
|
+
console.log(`\nThis only takes effect if you run \`rulereceipt guard\` as a PreToolUse hook.`);
|
|
534
|
+
console.log(`Rewording the rule drops the mark, on purpose — it would otherwise carry`);
|
|
535
|
+
console.log(`your judgment onto words you never read.`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Every rule with its handle.
|
|
540
|
+
*
|
|
541
|
+
* --forbid needs a handle, and before this the only place handles were
|
|
542
|
+
* printed was `check --show-skipped`, which lists the items the classifier
|
|
543
|
+
* DISCARDED. A rule that is actually being checked had no handle anywhere,
|
|
544
|
+
* so the marking feature shipped in 0.1.39 could not be reached for any
|
|
545
|
+
* rule a user would want to mark. Found by trying to use it.
|
|
546
|
+
*/
|
|
547
|
+
if (opts.handles) {
|
|
548
|
+
if (rules.length === 0) {
|
|
549
|
+
console.log("No CLAUDE.md or AGENTS.md rules found in this project.");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
console.log(`${rules.length} rule${rules.length === 1 ? "" : "s"} in this project:\n`);
|
|
553
|
+
for (const r of rules) {
|
|
554
|
+
const h = ruleFingerprint(r);
|
|
555
|
+
const marked = overrides.get(h)?.forbids;
|
|
556
|
+
console.log(` ${h} ${r.title.replace(/\s+/g, " ").trim().slice(0, 72)}`);
|
|
557
|
+
if (marked?.length)
|
|
558
|
+
console.log(` blocks on: ${marked.map((f) => `\`${f}\``).join(", ")}`);
|
|
559
|
+
}
|
|
560
|
+
console.log(`\nMark which clause of a rule is the prohibition:`);
|
|
561
|
+
console.log(` rulereceipt rules --forbid <handle> --literal "<the banned command>"`);
|
|
562
|
+
console.log(`Only a marked clause can ever refuse a command, and only via \`rulereceipt guard\`.`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
491
565
|
if (opts.clear) {
|
|
492
566
|
console.log(clearOverride(cwd, opts.clear) ? `Removed the correction for ${opts.clear}.` : `No correction stored for ${opts.clear}.`);
|
|
493
567
|
return;
|
|
@@ -604,6 +678,9 @@ program
|
|
|
604
678
|
.description("correct what the classifier treats as a rule. Handles come from `check --show-skipped`.")
|
|
605
679
|
.option("--include <handle>", "treat this item as a real rule and check it from now on")
|
|
606
680
|
.option("--exclude <handle>", "treat this item as documentation and stop reporting it")
|
|
681
|
+
.option("--handles", "list every rule with its handle, for use with --forbid")
|
|
682
|
+
.option("--forbid <handle>", "mark which clause of this rule is the prohibition, so the guard may block on it")
|
|
683
|
+
.option("--literal <text>", "the exact banned command, used with --forbid; must appear in the rule")
|
|
607
684
|
.option("--clear <handle>", "remove a stored correction")
|
|
608
685
|
.option("--list", "show stored corrections (the default when no other flag is given)")
|
|
609
686
|
.option("--coverage", "show which rules a configured hook might actually be enforcing, and which are prose only")
|
package/dist/guard.js
CHANGED
|
@@ -3,7 +3,8 @@ import { classifyRules } from "./checks/classify.js";
|
|
|
3
3
|
import { runCodeContentChecks } from "./checks/codeContent.js";
|
|
4
4
|
import { runFileLifecycleChecks } from "./checks/fileLifecycle.js";
|
|
5
5
|
import { runGitBranchPolicyChecks } from "./checks/gitBranchPolicy.js";
|
|
6
|
-
import { loadOverrides, ruleFingerprint } from "./overrides.js";
|
|
6
|
+
import { loadOverrides, ruleFingerprint, ratifiedForbids } from "./overrides.js";
|
|
7
|
+
import { commandRunsLiteral } from "./checks/proposedAction.js";
|
|
7
8
|
function readStdin() {
|
|
8
9
|
return new Promise((resolve) => {
|
|
9
10
|
let data = "";
|
|
@@ -56,7 +57,52 @@ function structuredBlocks(cwd, event) {
|
|
|
56
57
|
}));
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
+
* A command ban blocks only where a person marked the clause.
|
|
61
|
+
*
|
|
62
|
+
* The unratified version was measured before shipping and cut: blocking on a
|
|
63
|
+
* rule's command literals refused 62.8% of 16,336 real tool calls. Two
|
|
64
|
+
* narrowings reached 2.49% and the residue had no matcher fix — a rule
|
|
65
|
+
* titled "Feature Validation" refused `npm run build` 112 times, because it
|
|
66
|
+
* forbids running Playwright unprompted and RECOMMENDS the build command,
|
|
67
|
+
* which is its only command-shaped literal. Another refused plain
|
|
68
|
+
* `git status`, its backticks holding both the ban and the alternative.
|
|
69
|
+
*
|
|
70
|
+
* Nothing in a rules file marks which backtick is the prohibition. So this
|
|
71
|
+
* path reads only what someone declared, and yields nothing otherwise. The
|
|
72
|
+
* declaration is keyed on the rule's content hash and re-checked against the
|
|
73
|
+
* rule's current text, so a reworded rule loses its mark rather than
|
|
74
|
+
* carrying a judgement onto words nobody read.
|
|
75
|
+
*
|
|
76
|
+
* The important property is what happens by default: an unmarked rule cannot
|
|
77
|
+
* block, at any confidence, ever. That is the whole difference between this
|
|
78
|
+
* and the version that refused two thirds of everything.
|
|
79
|
+
*/
|
|
80
|
+
function ratifiedLiteralBlocks(cwd, command) {
|
|
81
|
+
const overrides = loadOverrides(cwd);
|
|
82
|
+
const blocks = [];
|
|
83
|
+
// Read from the RULES, not from the classification. A human mark
|
|
84
|
+
// supersedes the classifier, including its refusal to classify — and that
|
|
85
|
+
// refusal is the common case here. "Never use `git push --force`; prefer
|
|
86
|
+
// `git push --force-with-lease`" goes to judgment via hasMixedPolarity,
|
|
87
|
+
// for a correct reason: literal matching cannot tell which half owns which
|
|
88
|
+
// token. A rule naming both the ban and the alternative is the canonical
|
|
89
|
+
// reason to have someone say which is which, so gating the mark behind the
|
|
90
|
+
// classifier made the feature unavailable in exactly the case that
|
|
91
|
+
// motivated it. Shipped that way in 0.1.39 and caught by running it.
|
|
92
|
+
for (const rule of loadRules(cwd)) {
|
|
93
|
+
if (overrides.get(ruleFingerprint(rule))?.decision === "notARule")
|
|
94
|
+
continue;
|
|
95
|
+
for (const literal of ratifiedForbids(overrides, rule)) {
|
|
96
|
+
if (!commandRunsLiteral(command, literal))
|
|
97
|
+
continue;
|
|
98
|
+
blocks.push({ rule, why: `the command about to run does \`${literal}\`, which this rule forbids (marked by you, not inferred)` });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return blocks;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Literal command bans do NOT block unless ratified. Measured, then cut.
|
|
60
106
|
*
|
|
61
107
|
* This was the point of the feature and it does not survive its own
|
|
62
108
|
* measurement. Replaying 16,336 real tool calls against every forbidding
|
|
@@ -149,7 +195,7 @@ export async function runGuard() {
|
|
|
149
195
|
role: "assistant", kind: "tool_use", toolName: "Bash",
|
|
150
196
|
input: { command: toolInput.command }, timestamp: "",
|
|
151
197
|
};
|
|
152
|
-
blocks = structuredBlocks(cwd, event);
|
|
198
|
+
blocks = [...structuredBlocks(cwd, event), ...ratifiedLiteralBlocks(cwd, toolInput.command)];
|
|
153
199
|
}
|
|
154
200
|
else if (tool === "Write" || tool === "Edit" || tool === "NotebookEdit") {
|
|
155
201
|
const event = {
|
package/dist/overrides.d.ts
CHANGED
|
@@ -26,6 +26,21 @@ export interface Override {
|
|
|
26
26
|
decision: Decision;
|
|
27
27
|
/** Stored for humans reading the file, and to explain a stale entry. */
|
|
28
28
|
title: string;
|
|
29
|
+
/**
|
|
30
|
+
* The literal(s) a person has marked as the PROHIBITION in this rule.
|
|
31
|
+
*
|
|
32
|
+
* A rules file does not say which of its backticks is the thing being
|
|
33
|
+
* banned. Measured: blocking on all of them refused 62.8% of 16,336 real
|
|
34
|
+
* tool calls, and the residue after two narrowings still refused
|
|
35
|
+
* `npm run build` 112 times, because the rule that named it forbids
|
|
36
|
+
* running Playwright and RECOMMENDS the build command. No matcher fixes
|
|
37
|
+
* that — the information is not in the text.
|
|
38
|
+
*
|
|
39
|
+
* So it is declared, once, and only what is declared can block. Absent
|
|
40
|
+
* means absent: there is deliberately no fallback to "probably the first
|
|
41
|
+
* literal", because that fallback is the bug.
|
|
42
|
+
*/
|
|
43
|
+
forbids?: string[];
|
|
29
44
|
}
|
|
30
45
|
export declare const OVERRIDES_PATH: string;
|
|
31
46
|
/**
|
|
@@ -60,3 +75,21 @@ export declare function clearOverride(cwd: string, hash: string): boolean;
|
|
|
60
75
|
* correction is no longer being applied instead of assuming it still is.
|
|
61
76
|
*/
|
|
62
77
|
export declare function staleOverrides(overrides: Map<string, Override>, rules: Rule[]): Override[];
|
|
78
|
+
/**
|
|
79
|
+
* The prohibition literals a person has declared for this rule, or none.
|
|
80
|
+
*
|
|
81
|
+
* Two conditions, both required, and both are refusals to infer:
|
|
82
|
+
*
|
|
83
|
+
* 1. The mark is keyed on the rule's CONTENT hash, so rewording the rule
|
|
84
|
+
* drops it rather than reattaching a judgement to text nobody read. Same
|
|
85
|
+
* reasoning as ruleFingerprint, one field down.
|
|
86
|
+
*
|
|
87
|
+
* 2. The literal must still appear in the rule. A mark that survives a
|
|
88
|
+
* partial edit and names something the rule no longer mentions would
|
|
89
|
+
* block on a phrase with nothing behind it — a gate refusing a command
|
|
90
|
+
* for a reason that is no longer written anywhere, which is the worst
|
|
91
|
+
* failure available to a gate.
|
|
92
|
+
*
|
|
93
|
+
* Returns [] for anything unratified. There is no fallback on purpose.
|
|
94
|
+
*/
|
|
95
|
+
export declare function ratifiedForbids(overrides: Map<string, Override>, rule: Rule): string[];
|
package/dist/overrides.js
CHANGED
|
@@ -32,7 +32,15 @@ export function loadOverrides(cwd) {
|
|
|
32
32
|
return map;
|
|
33
33
|
for (const o of parsed.overrides) {
|
|
34
34
|
if (typeof o?.hash === "string" && (o.decision === "rule" || o.decision === "notARule")) {
|
|
35
|
-
|
|
35
|
+
const forbids = Array.isArray(o.forbids)
|
|
36
|
+
? o.forbids.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
37
|
+
: undefined;
|
|
38
|
+
map.set(o.hash, {
|
|
39
|
+
hash: o.hash,
|
|
40
|
+
decision: o.decision,
|
|
41
|
+
title: String(o.title ?? ""),
|
|
42
|
+
...(forbids && forbids.length > 0 ? { forbids } : {}),
|
|
43
|
+
});
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -83,3 +91,27 @@ export function staleOverrides(overrides, rules) {
|
|
|
83
91
|
const live = new Set(rules.map(ruleFingerprint));
|
|
84
92
|
return [...overrides.values()].filter((o) => !live.has(o.hash));
|
|
85
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The prohibition literals a person has declared for this rule, or none.
|
|
96
|
+
*
|
|
97
|
+
* Two conditions, both required, and both are refusals to infer:
|
|
98
|
+
*
|
|
99
|
+
* 1. The mark is keyed on the rule's CONTENT hash, so rewording the rule
|
|
100
|
+
* drops it rather than reattaching a judgement to text nobody read. Same
|
|
101
|
+
* reasoning as ruleFingerprint, one field down.
|
|
102
|
+
*
|
|
103
|
+
* 2. The literal must still appear in the rule. A mark that survives a
|
|
104
|
+
* partial edit and names something the rule no longer mentions would
|
|
105
|
+
* block on a phrase with nothing behind it — a gate refusing a command
|
|
106
|
+
* for a reason that is no longer written anywhere, which is the worst
|
|
107
|
+
* failure available to a gate.
|
|
108
|
+
*
|
|
109
|
+
* Returns [] for anything unratified. There is no fallback on purpose.
|
|
110
|
+
*/
|
|
111
|
+
export function ratifiedForbids(overrides, rule) {
|
|
112
|
+
const entry = overrides.get(ruleFingerprint(rule));
|
|
113
|
+
if (!entry?.forbids)
|
|
114
|
+
return [];
|
|
115
|
+
const body = `${rule.title}\n${rule.text ?? ""}`;
|
|
116
|
+
return entry.forbids.filter((literal) => body.includes(literal));
|
|
117
|
+
}
|