rulereceipt 0.1.36 → 0.1.37
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 +13 -1
- package/dist/checks/codeContent.js +32 -1
- package/dist/checks/fileLifecycle.js +12 -1
- package/dist/checks/shellCommand.d.ts +24 -0
- package/dist/checks/shellCommand.js +43 -0
- package/dist/checks/testCommands.d.ts +1 -15
- package/dist/checks/testCommands.js +2 -34
- package/package.json +1 -1
package/dist/checks/classify.js
CHANGED
|
@@ -264,7 +264,19 @@ function isBranchName(literal) {
|
|
|
264
264
|
// A function/method-call shape ("print(", "analytics.track(") is a strong,
|
|
265
265
|
// simple signal that a backtick literal names actual CODE, not a CLI
|
|
266
266
|
// command or flag ("git push --force", "npm test" never look like this).
|
|
267
|
-
|
|
267
|
+
//
|
|
268
|
+
// It must be a CALL, not merely a parenthesis. The test used to be /\(/,
|
|
269
|
+
// which is true of a great deal of ordinary prose: measured 2026-09-15
|
|
270
|
+
// across 559 rules files, 74 of 1,086 literals reaching content matching
|
|
271
|
+
// (6.8%) were not code — "(e.g.", "(soft)", a markdown link fragment, three
|
|
272
|
+
// whole blocks of accounting formulae. "(in the" produced a real false
|
|
273
|
+
// accusation, reported as having been "actually written into a file", which
|
|
274
|
+
// is true of any file containing that phrase.
|
|
275
|
+
//
|
|
276
|
+
// An identifier immediately before the paren, optionally dotted or scoped,
|
|
277
|
+
// so "console.log(", "std::cout(" and "obj->run(" all qualify and a bare
|
|
278
|
+
// parenthesis does not.
|
|
279
|
+
const CODE_CONSTRUCT_PATTERN = /[A-Za-z_$][A-Za-z0-9_$]*(?:\s*(?:\.|::|->)\s*[A-Za-z_$][A-Za-z0-9_$]*)*\s*\(/;
|
|
268
280
|
// A file-path shape: a known config/source extension, or a path with a
|
|
269
281
|
// directory separator. Deliberately requires no spaces — a real path
|
|
270
282
|
// literal ("`.claude/settings.json`", "`config.yaml`") never has one,
|
|
@@ -29,6 +29,37 @@ function editedContentFromEvent(event) {
|
|
|
29
29
|
return input.new_source;
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the content contains this literal AS A CALL, not merely as a
|
|
34
|
+
* substring of a longer identifier.
|
|
35
|
+
*
|
|
36
|
+
* Found 2026-09-15 by checking a corpus FAIL rather than assuming it was
|
|
37
|
+
* legitimate: a rule forbidding `fetch()` matched a file containing
|
|
38
|
+
* `_metar_fetch()`. The literal was present verbatim, and entirely the wrong
|
|
39
|
+
* function. The same bare-substring test makes `main()` match `domain()` and
|
|
40
|
+
* `run()` match `rerun()`, and short generic call names are exactly what
|
|
41
|
+
* these rules tend to name.
|
|
42
|
+
*
|
|
43
|
+
* Only the LEADING boundary is checked. The trailing side is already pinned
|
|
44
|
+
* by the pattern itself — every literal reaching this checker ends in an
|
|
45
|
+
* open paren or a call — so requiring a boundary after it would reject the
|
|
46
|
+
* arguments.
|
|
47
|
+
*/
|
|
48
|
+
function containsCall(content, pattern) {
|
|
49
|
+
const leadsWithIdentifier = /^[A-Za-z0-9_$]/.test(pattern);
|
|
50
|
+
if (!leadsWithIdentifier)
|
|
51
|
+
return content.includes(pattern);
|
|
52
|
+
let from = 0;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const at = content.indexOf(pattern, from);
|
|
55
|
+
if (at === -1)
|
|
56
|
+
return false;
|
|
57
|
+
const before = at === 0 ? "" : content[at - 1];
|
|
58
|
+
if (!/[A-Za-z0-9_$.]/.test(before))
|
|
59
|
+
return true;
|
|
60
|
+
from = at + 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
export function runCodeContentChecks(classifications, events) {
|
|
33
64
|
const editedContents = [];
|
|
34
65
|
for (const event of events) {
|
|
@@ -41,7 +72,7 @@ export function runCodeContentChecks(classifications, events) {
|
|
|
41
72
|
let foundContent;
|
|
42
73
|
for (const content of editedContents) {
|
|
43
74
|
for (const pattern of patterns) {
|
|
44
|
-
if (content
|
|
75
|
+
if (containsCall(content, pattern)) {
|
|
45
76
|
foundPattern = pattern;
|
|
46
77
|
foundContent = content;
|
|
47
78
|
break;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { violation } from "../types.js";
|
|
2
2
|
import { isProjectPath } from "./projectPaths.js";
|
|
3
|
+
import { withoutHeredocs } from "./shellCommand.js";
|
|
3
4
|
/**
|
|
4
5
|
* Third structured-check primitive: only counts real MUTATIONS of a
|
|
5
6
|
* protected file, never reads of it. Real false-positive this fixes
|
|
@@ -43,7 +44,17 @@ function pathPattern(filePath) {
|
|
|
43
44
|
* it mutates after that is a scratch file, not the project's.
|
|
44
45
|
*/
|
|
45
46
|
const CD_INTO_TEMP = /\bcd\s+["']?(?:\/private)?\/(?:tmp|var\/folders)\b|\bcd\s+["']?[^\s"'&|;]*\/(?:scratchpad|node_modules)\b/;
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* A path named only inside a heredoc body was not touched by the command
|
|
49
|
+
* that contains it.
|
|
50
|
+
*
|
|
51
|
+
* Real case, 2026-09-15: a command editing landing/index.html through a
|
|
52
|
+
* Python heredoc was reported as modifying `.claude/`, because the HTML it
|
|
53
|
+
* inserts tells readers to put a hook in `.claude/settings.json`. Writing a
|
|
54
|
+
* path into a file is not mutating that path.
|
|
55
|
+
*/
|
|
56
|
+
function mutatesPathInBash(rawCommand, filePath) {
|
|
57
|
+
const command = withoutHeredocs(rawCommand);
|
|
47
58
|
const p = pathPattern(filePath);
|
|
48
59
|
const mutations = [
|
|
49
60
|
// rm / rmdir / unlink targeting the path
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts about shell command text that more than one checker needs.
|
|
3
|
+
*
|
|
4
|
+
* Created 2026-09-15. The heredoc guard below lived in testCommands.ts, was
|
|
5
|
+
* used only by the test-command matcher, and the file-mutation checker never
|
|
6
|
+
* saw it — so the same class of false accusation was fixed in one reader and
|
|
7
|
+
* left standing in the other. Anything that reasons about what a shell
|
|
8
|
+
* command DID, rather than what it says, belongs here.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Removes heredoc bodies from a shell command.
|
|
12
|
+
*
|
|
13
|
+
* A command that WRITES a test command is not a command that RUNS one.
|
|
14
|
+
* Found 2026-09-14 on a real session: two false failures whose "last test
|
|
15
|
+
* run" was a shell variable assignment. The actual match came from a
|
|
16
|
+
* heredoc further down, writing a demo fixture whose body contains the
|
|
17
|
+
* string `npm test`. The literal was being generated, never executed — and
|
|
18
|
+
* the tool then read its own report output as the failing result.
|
|
19
|
+
*
|
|
20
|
+
* Handles both quoted and bare delimiters, and leaves everything after the
|
|
21
|
+
* closing delimiter intact, because a real test run often follows the
|
|
22
|
+
* heredoc that set the fixture up.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withoutHeredocs(command: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts about shell command text that more than one checker needs.
|
|
3
|
+
*
|
|
4
|
+
* Created 2026-09-15. The heredoc guard below lived in testCommands.ts, was
|
|
5
|
+
* used only by the test-command matcher, and the file-mutation checker never
|
|
6
|
+
* saw it — so the same class of false accusation was fixed in one reader and
|
|
7
|
+
* left standing in the other. Anything that reasons about what a shell
|
|
8
|
+
* command DID, rather than what it says, belongs here.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Removes heredoc bodies from a shell command.
|
|
12
|
+
*
|
|
13
|
+
* A command that WRITES a test command is not a command that RUNS one.
|
|
14
|
+
* Found 2026-09-14 on a real session: two false failures whose "last test
|
|
15
|
+
* run" was a shell variable assignment. The actual match came from a
|
|
16
|
+
* heredoc further down, writing a demo fixture whose body contains the
|
|
17
|
+
* string `npm test`. The literal was being generated, never executed — and
|
|
18
|
+
* the tool then read its own report output as the failing result.
|
|
19
|
+
*
|
|
20
|
+
* Handles both quoted and bare delimiters, and leaves everything after the
|
|
21
|
+
* closing delimiter intact, because a real test run often follows the
|
|
22
|
+
* heredoc that set the fixture up.
|
|
23
|
+
*/
|
|
24
|
+
export function withoutHeredocs(command) {
|
|
25
|
+
const lines = command.split("\n");
|
|
26
|
+
const out = [];
|
|
27
|
+
let closing = null;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (closing !== null) {
|
|
30
|
+
if (line.trim() === closing)
|
|
31
|
+
closing = null;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const open = line.match(/<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/);
|
|
35
|
+
if (open) {
|
|
36
|
+
closing = open[1] ?? open[2] ?? open[3];
|
|
37
|
+
out.push(line.slice(0, open.index));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
out.push(line);
|
|
41
|
+
}
|
|
42
|
+
return out.join("\n");
|
|
43
|
+
}
|
|
@@ -19,21 +19,6 @@ import type { TranscriptEvent } from "../types.js";
|
|
|
19
19
|
* wrong.
|
|
20
20
|
*/
|
|
21
21
|
export declare const TEST_COMMAND: RegExp;
|
|
22
|
-
/**
|
|
23
|
-
* Removes heredoc bodies from a shell command.
|
|
24
|
-
*
|
|
25
|
-
* A command that WRITES a test command is not a command that RUNS one.
|
|
26
|
-
* Found 2026-09-14 on a real session: two false failures whose "last test
|
|
27
|
-
* run" was a shell variable assignment. The actual match came from a
|
|
28
|
-
* heredoc further down, writing a demo fixture whose body contains the
|
|
29
|
-
* string `npm test`. The literal was being generated, never executed — and
|
|
30
|
-
* the tool then read its own report output as the failing result.
|
|
31
|
-
*
|
|
32
|
-
* Handles both quoted and bare delimiters, and leaves everything after the
|
|
33
|
-
* closing delimiter intact, because a real test run often follows the
|
|
34
|
-
* heredoc that set the fixture up.
|
|
35
|
-
*/
|
|
36
|
-
export declare function withoutHeredocs(command: string): string;
|
|
37
22
|
/**
|
|
38
23
|
* How many times a single shell command invokes a test suite.
|
|
39
24
|
*
|
|
@@ -54,3 +39,4 @@ export declare function withoutHeredocs(command: string): string;
|
|
|
54
39
|
export declare function countTestRuns(command: string): number;
|
|
55
40
|
/** The first test command run in this session, or null if none ran. */
|
|
56
41
|
export declare function findTestRun(events: TranscriptEvent[]): string | null;
|
|
42
|
+
export { withoutHeredocs } from "./shellCommand.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withoutHeredocs } from "./shellCommand.js";
|
|
1
2
|
/**
|
|
2
3
|
* Commands that run a project's test suite.
|
|
3
4
|
*
|
|
@@ -18,40 +19,6 @@
|
|
|
18
19
|
* wrong.
|
|
19
20
|
*/
|
|
20
21
|
export const TEST_COMMAND = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|verify|check|ci)\b|\bnpx\s+(?:vitest|jest|mocha|ava)\b|\b(?:vitest|jest|mocha|pytest|phpunit|rspec|tox)\b|\bcargo\s+test\b|\bgo\s+test\b|\bmvn\s+(?:test|verify)\b|\bgradle\s+test\b|\bdotnet\s+test\b|\bpython\s+-m\s+(?:pytest|unittest)\b/i;
|
|
21
|
-
/**
|
|
22
|
-
* Removes heredoc bodies from a shell command.
|
|
23
|
-
*
|
|
24
|
-
* A command that WRITES a test command is not a command that RUNS one.
|
|
25
|
-
* Found 2026-09-14 on a real session: two false failures whose "last test
|
|
26
|
-
* run" was a shell variable assignment. The actual match came from a
|
|
27
|
-
* heredoc further down, writing a demo fixture whose body contains the
|
|
28
|
-
* string `npm test`. The literal was being generated, never executed — and
|
|
29
|
-
* the tool then read its own report output as the failing result.
|
|
30
|
-
*
|
|
31
|
-
* Handles both quoted and bare delimiters, and leaves everything after the
|
|
32
|
-
* closing delimiter intact, because a real test run often follows the
|
|
33
|
-
* heredoc that set the fixture up.
|
|
34
|
-
*/
|
|
35
|
-
export function withoutHeredocs(command) {
|
|
36
|
-
const lines = command.split("\n");
|
|
37
|
-
const out = [];
|
|
38
|
-
let closing = null;
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
if (closing !== null) {
|
|
41
|
-
if (line.trim() === closing)
|
|
42
|
-
closing = null;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const open = line.match(/<<-?\s*(?:'([^']+)'|"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/);
|
|
46
|
-
if (open) {
|
|
47
|
-
closing = open[1] ?? open[2] ?? open[3];
|
|
48
|
-
out.push(line.slice(0, open.index));
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
out.push(line);
|
|
52
|
-
}
|
|
53
|
-
return out.join("\n");
|
|
54
|
-
}
|
|
55
22
|
/**
|
|
56
23
|
* How many times a single shell command invokes a test suite.
|
|
57
24
|
*
|
|
@@ -85,3 +52,4 @@ export function findTestRun(events) {
|
|
|
85
52
|
}
|
|
86
53
|
return null;
|
|
87
54
|
}
|
|
55
|
+
export { withoutHeredocs } from "./shellCommand.js";
|