@webpieces/hook-runtime 0.0.1
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/package.json +37 -0
- package/src/adapters/agent-adapters.d.ts +13 -0
- package/src/adapters/agent-adapters.js +23 -0
- package/src/adapters/agent-adapters.js.map +1 -0
- package/src/adapters/agent-payload.d.ts +48 -0
- package/src/adapters/agent-payload.js +30 -0
- package/src/adapters/agent-payload.js.map +1 -0
- package/src/adapters/claude-code-adapter.d.ts +26 -0
- package/src/adapters/claude-code-adapter.js +69 -0
- package/src/adapters/claude-code-adapter.js.map +1 -0
- package/src/adapters/codex-adapter.d.ts +19 -0
- package/src/adapters/codex-adapter.js +48 -0
- package/src/adapters/codex-adapter.js.map +1 -0
- package/src/adapters/detect-ai.d.ts +70 -0
- package/src/adapters/detect-ai.js +82 -0
- package/src/adapters/detect-ai.js.map +1 -0
- package/src/core/apply-patch-parse.d.ts +36 -0
- package/src/core/apply-patch-parse.js +155 -0
- package/src/core/apply-patch-parse.js.map +1 -0
- package/src/core/build-context.d.ts +11 -0
- package/src/core/build-context.js +69 -0
- package/src/core/build-context.js.map +1 -0
- package/src/core/command-scan.d.ts +122 -0
- package/src/core/command-scan.js +282 -0
- package/src/core/command-scan.js.map +1 -0
- package/src/core/custom-rule-adapter.d.ts +23 -0
- package/src/core/custom-rule-adapter.js +50 -0
- package/src/core/custom-rule-adapter.js.map +1 -0
- package/src/core/delete-scoped-rules.d.ts +9 -0
- package/src/core/delete-scoped-rules.js +31 -0
- package/src/core/delete-scoped-rules.js.map +1 -0
- package/src/core/disable-directives.d.ts +8 -0
- package/src/core/disable-directives.js +86 -0
- package/src/core/disable-directives.js.map +1 -0
- package/src/core/effective-tree.d.ts +197 -0
- package/src/core/effective-tree.js +269 -0
- package/src/core/effective-tree.js.map +1 -0
- package/src/core/excluded-paths.d.ts +23 -0
- package/src/core/excluded-paths.js +54 -0
- package/src/core/excluded-paths.js.map +1 -0
- package/src/core/file-evaluation.d.ts +11 -0
- package/src/core/file-evaluation.js +45 -0
- package/src/core/file-evaluation.js.map +1 -0
- package/src/core/fix-hint.d.ts +40 -0
- package/src/core/fix-hint.js +53 -0
- package/src/core/fix-hint.js.map +1 -0
- package/src/core/glob.d.ts +1 -0
- package/src/core/glob.js +42 -0
- package/src/core/glob.js.map +1 -0
- package/src/core/report.d.ts +21 -0
- package/src/core/report.js +74 -0
- package/src/core/report.js.map +1 -0
- package/src/core/root-manifest.d.ts +1 -0
- package/src/core/root-manifest.js +21 -0
- package/src/core/root-manifest.js.map +1 -0
- package/src/core/rule-base.d.ts +40 -0
- package/src/core/rule-base.js +37 -0
- package/src/core/rule-base.js.map +1 -0
- package/src/core/rule-evaluation.d.ts +18 -0
- package/src/core/rule-evaluation.js +179 -0
- package/src/core/rule-evaluation.js.map +1 -0
- package/src/core/rules/shell-segment-scan.d.ts +64 -0
- package/src/core/rules/shell-segment-scan.js +88 -0
- package/src/core/rules/shell-segment-scan.js.map +1 -0
- package/src/core/shell-read-parity.d.ts +34 -0
- package/src/core/shell-read-parity.js +157 -0
- package/src/core/shell-read-parity.js.map +1 -0
- package/src/core/strip-ts-noise.d.ts +1 -0
- package/src/core/strip-ts-noise.js +191 -0
- package/src/core/strip-ts-noise.js.map +1 -0
- package/src/core/target-tree.d.ts +84 -0
- package/src/core/target-tree.js +148 -0
- package/src/core/target-tree.js.map +1 -0
- package/src/core/types.d.ts +149 -0
- package/src/core/types.js +190 -0
- package/src/core/types.js.map +1 -0
- package/src/harness-vocabulary.d.ts +2 -0
- package/src/harness-vocabulary.js +10 -0
- package/src/harness-vocabulary.js.map +1 -0
- package/src/hook-app.d.ts +15 -0
- package/src/hook-app.js +58 -0
- package/src/hook-app.js.map +1 -0
- package/src/hook-evaluator.d.ts +5 -0
- package/src/hook-evaluator.js +13 -0
- package/src/hook-evaluator.js.map +1 -0
- package/src/hook-ports.d.ts +9 -0
- package/src/hook-ports.js +40 -0
- package/src/hook-ports.js.map +1 -0
- package/src/index.d.ts +31 -0
- package/src/index.js +59 -0
- package/src/index.js.map +1 -0
- package/src/outcome.d.ts +14 -0
- package/src/outcome.js +20 -0
- package/src/outcome.js.map +1 -0
- package/src/protocol.d.ts +40 -0
- package/src/protocol.js +55 -0
- package/src/protocol.js.map +1 -0
- package/src/response.d.ts +2 -0
- package/src/response.js +21 -0
- package/src/response.js.map +1 -0
- package/src/to-error.d.ts +1 -0
- package/src/to-error.js +9 -0
- package/src/to-error.js.map +1 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared shell-command scanning for the bash guards.
|
|
4
|
+
*
|
|
5
|
+
* A guard that bans a command family (`git merge`, `git push`, …) must answer one question
|
|
6
|
+
* precisely: *does this command actually invoke `git <subcommand>`?* A bare
|
|
7
|
+
* `/\bgit\s+merge\b/.test(command)` gets that wrong in both directions:
|
|
8
|
+
*
|
|
9
|
+
* - False positive: `grep 'git merge main' notes.md` or `echo "git rebase main"` merely MENTION
|
|
10
|
+
* the phrase. A diagnostic grep was blocked this way while triaging the incident that motivated
|
|
11
|
+
* the merge/rebase ban.
|
|
12
|
+
* - False positive: `\b` sits between `e` and `-`, so `/\bgit\s+merge\b/` matches the read-only
|
|
13
|
+
* `git merge-base origin/main HEAD` — which appears in this repo's own documented build command.
|
|
14
|
+
*
|
|
15
|
+
* Both classes vanish if you tokenize instead of substring-match: a command invokes git only when a
|
|
16
|
+
* segment's first word IS `git`, and the subcommand is then an exact token (`merge-base` is simply
|
|
17
|
+
* not the token `merge`). No lookahead regex needed.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.CommandScanner = exports.CommandSegment = void 0;
|
|
21
|
+
// Wrappers/prefixes that may precede the real command word (`sudo git merge`, `GIT_DIR=x git merge`).
|
|
22
|
+
const COMMAND_PREFIXES = new Set(['sudo', 'command', 'nohup', 'time', 'env', 'exec']);
|
|
23
|
+
// git's own global flags that consume the FOLLOWING token as their value, so
|
|
24
|
+
// `git -C /some/path merge main` still resolves to the `merge` subcommand.
|
|
25
|
+
const GIT_FLAGS_WITH_VALUE = new Set([
|
|
26
|
+
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path',
|
|
27
|
+
]);
|
|
28
|
+
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
29
|
+
// Package-manager wrappers that precede the real program (`pnpm exec vitest`, `npx nx`, `yarn build-all`).
|
|
30
|
+
const RUNNERS = new Set(['pnpm', 'npm', 'yarn', 'npx', 'pnpx', 'bun', 'bunx']);
|
|
31
|
+
// The runner's own verb, consumed with it: `pnpm run build-all` and `pnpm build-all` are one command.
|
|
32
|
+
const RUNNER_VERBS = new Set(['run', 'run-script', 'exec', 'dlx', 'x']);
|
|
33
|
+
/**
|
|
34
|
+
* One invoked segment of a command, plus the operator that joined it to its predecessor.
|
|
35
|
+
*
|
|
36
|
+
* `join === '|'` is what separates `git log | grep foo` (grep consumes the pipe — reads no file) from
|
|
37
|
+
* `grep foo src/` (grep reads the working tree). A guard that cares about which FILES a command
|
|
38
|
+
* reads cannot tell those apart from the segment text alone, because splitting on `|` throws exactly
|
|
39
|
+
* that fact away. `join === '&&'` vs `';'` is the same argument one operator over — see
|
|
40
|
+
* {@link SegmentJoin}. Data-only, so a class (per CLAUDE.md).
|
|
41
|
+
*/
|
|
42
|
+
class CommandSegment {
|
|
43
|
+
text;
|
|
44
|
+
join;
|
|
45
|
+
constructor(text, join) {
|
|
46
|
+
this.text = text;
|
|
47
|
+
this.join = join;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.CommandSegment = CommandSegment;
|
|
51
|
+
class CommandScanner {
|
|
52
|
+
/**
|
|
53
|
+
* Split a raw command into individually-invoked segments.
|
|
54
|
+
*
|
|
55
|
+
* Splits on `&&`, `||`, `;`, `|`, `&`, newline, and the `(`/`)` of subshells and `$(…)` command
|
|
56
|
+
* substitution — the last of these matters, since it means `--base=$(git rebase main)` is scanned
|
|
57
|
+
* as its own `git rebase main` segment rather than hiding inside a `pnpm …` segment.
|
|
58
|
+
*
|
|
59
|
+
* Quoted spans are opaque: a separator inside quotes is literal text, so
|
|
60
|
+
* `git commit -m "fix; ship it"` stays one segment. (Corollary: a `$(…)` nested inside double
|
|
61
|
+
* quotes is not split out. Bash would expand it; we do not scan it. Contrived enough to accept.)
|
|
62
|
+
*/
|
|
63
|
+
commandSegments(command) {
|
|
64
|
+
return this.segmentsWithJoins(command).map((s) => s.text);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* commandSegments, but each segment also carries the OPERATOR that joined it to its predecessor.
|
|
68
|
+
* Only a guard reasoning about which files a segment reads, or about whether a failed first
|
|
69
|
+
* segment can stop the rest, needs that; everything else uses commandSegments, which is this
|
|
70
|
+
* method with the join dropped.
|
|
71
|
+
*/
|
|
72
|
+
segmentsWithJoins(command) {
|
|
73
|
+
const segments = [];
|
|
74
|
+
let current = '';
|
|
75
|
+
let quote = null;
|
|
76
|
+
let join = 'none'; // the operator that ENDED the previous segment
|
|
77
|
+
for (let i = 0; i < command.length; i++) {
|
|
78
|
+
const ch = command[i];
|
|
79
|
+
if (quote !== null) {
|
|
80
|
+
current += ch;
|
|
81
|
+
// A backslash-escaped quote does not close the span (only meaningful inside "…").
|
|
82
|
+
if (ch === quote && command[i - 1] !== '\\')
|
|
83
|
+
quote = null;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === '"' || ch === "'") {
|
|
87
|
+
quote = ch;
|
|
88
|
+
current += ch;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// The `&` of a REDIRECTION (`2>&1`, `1>&2`, `&>log`) is not a separator. Splitting on it
|
|
92
|
+
// tore `git fetch origin main 2>&1 | tail -5` into THREE segments — `git fetch … 2>`, `1`
|
|
93
|
+
// and `tail -5` — and the bare `1` is not an allowlisted command, so every guard that
|
|
94
|
+
// requires all segments to pass denied the command. That is `2>&1`, the single most common
|
|
95
|
+
// decoration an agent appends.
|
|
96
|
+
if (ch === '&' && command[i + 1] !== '&' && (current.trimEnd().endsWith('>') || command[i + 1] === '>')) {
|
|
97
|
+
current += ch;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (ch === '\n' || ch === ';' || ch === '|' || ch === '&' || ch === '(' || ch === ')') {
|
|
101
|
+
// Consume the second char of `&&` / `||` so it does not start an empty segment.
|
|
102
|
+
const doubled = (ch === '|' || ch === '&') && command[i + 1] === ch;
|
|
103
|
+
if (doubled)
|
|
104
|
+
i++;
|
|
105
|
+
segments.push(new CommandSegment(current, join));
|
|
106
|
+
// `|` pipes into the next segment; `||` is a separator, not a pipe.
|
|
107
|
+
join = doubled ? (ch + ch) : ch;
|
|
108
|
+
current = '';
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
current += ch;
|
|
112
|
+
}
|
|
113
|
+
segments.push(new CommandSegment(current, join));
|
|
114
|
+
return segments
|
|
115
|
+
.map((s) => new CommandSegment(s.text.trim(), s.join))
|
|
116
|
+
.filter((s) => s.text.length > 0);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One segment's shell words, with wrappers/env-assignments stripped, so `words('sudo cat a b')`
|
|
120
|
+
* is `['cat', 'a', 'b']`. The public view of the same tokenizer gitSubcommand uses — a guard that
|
|
121
|
+
* must inspect a NON-git command's arguments (which paths does this `grep` actually read?) needs
|
|
122
|
+
* the tokens, and re-splitting on whitespace in the guard would get quoting wrong.
|
|
123
|
+
*/
|
|
124
|
+
words(segment) {
|
|
125
|
+
return this.stripPrefixes(this.tokenize(segment));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The git subcommand a segment invokes, or null when the segment does not invoke git at all
|
|
129
|
+
* (a different program, a mere mention inside quotes, an empty segment).
|
|
130
|
+
*
|
|
131
|
+
* Returns the subcommand as an EXACT token: `git merge-base …` yields `'merge-base'`, never `'merge'`.
|
|
132
|
+
*/
|
|
133
|
+
gitSubcommand(segment) {
|
|
134
|
+
return this.gitSubcommandOf(this.stripPrefixes(this.tokenize(segment)));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* gitSubcommand, for a caller that already holds the segment's effective words (ShellSegmentScan
|
|
138
|
+
* strips leading shell keywords, so `do git status` must be resolved from ITS words, not from the
|
|
139
|
+
* raw segment text where `do` is the command).
|
|
140
|
+
*/
|
|
141
|
+
gitSubcommandOf(words) {
|
|
142
|
+
const tokens = this.stripPrefixes(words);
|
|
143
|
+
const at = this.gitSubcommandIndex(tokens);
|
|
144
|
+
return at === -1 ? null : tokens[at];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The ARGUMENTS following `git <subcommand>` in this segment, or null when the segment does not
|
|
148
|
+
* invoke that subcommand.
|
|
149
|
+
*
|
|
150
|
+
* `gitSubcommand` answers *which* subcommand; a guard that judges the subcommand's own flags needs
|
|
151
|
+
* the tokens after it, and slicing them in the guard would mean re-deriving where the subcommand
|
|
152
|
+
* sits — i.e. re-deriving the `sudo` / `env VAR=x` / `-C <path>` skipping this class exists to own.
|
|
153
|
+
* `git -C /x commit -m "msg"` yields `['-m', 'msg']`, never `['/x', 'commit', '-m', 'msg']`.
|
|
154
|
+
*
|
|
155
|
+
* The tokens are QUOTE-STRIPPED (see tokenize), so a quoted argument arrives as ONE token holding
|
|
156
|
+
* its literal text — newlines, backticks and all. That is what makes an argument's CONTENT
|
|
157
|
+
* inspectable at all.
|
|
158
|
+
*/
|
|
159
|
+
gitSubcommandArgs(segment, subcommand) {
|
|
160
|
+
const tokens = this.stripPrefixes(this.tokenize(segment));
|
|
161
|
+
const at = this.gitSubcommandIndex(tokens);
|
|
162
|
+
if (at === -1 || tokens[at] !== subcommand)
|
|
163
|
+
return null;
|
|
164
|
+
return tokens.slice(at + 1);
|
|
165
|
+
}
|
|
166
|
+
/** Index of the subcommand token in already-prefix-stripped words, or -1 when git is not invoked. */
|
|
167
|
+
gitSubcommandIndex(tokens) {
|
|
168
|
+
if (tokens.length === 0 || tokens[0] !== 'git')
|
|
169
|
+
return -1;
|
|
170
|
+
let i = 1;
|
|
171
|
+
while (i < tokens.length) {
|
|
172
|
+
const token = tokens[i];
|
|
173
|
+
if (GIT_FLAGS_WITH_VALUE.has(token)) {
|
|
174
|
+
i += 2;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// `--git-dir=/x` style (value attached) and any other global flag.
|
|
178
|
+
if (token.startsWith('-')) {
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
return i;
|
|
183
|
+
}
|
|
184
|
+
return -1;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The segment's words with the package-manager WRAPPER stripped, so `pnpm exec vitest run` and
|
|
188
|
+
* `vitest run` reduce to the same words. `pnpm --silent run build-all` → `['build-all']`.
|
|
189
|
+
*
|
|
190
|
+
* Lives HERE rather than in a guard because two guards now need it and they must agree: one blocks a
|
|
191
|
+
* whole-repo build, the other blocks piping a build's output. If they disagreed about whether
|
|
192
|
+
* `npx wp-build` is `wp-build`, one of them would have a spelling-shaped side door — which is exactly
|
|
193
|
+
* the failure mode the runner stripping exists to close.
|
|
194
|
+
*/
|
|
195
|
+
runnerStrippedWords(segment) {
|
|
196
|
+
let words = this.words(segment);
|
|
197
|
+
while (words.length > 0 && RUNNERS.has(this.programName(words[0]))) {
|
|
198
|
+
words = words.slice(1);
|
|
199
|
+
// The runner's own flags (`--silent`, `-r`) sit between it and the program.
|
|
200
|
+
while (words.length > 0 && words[0].startsWith('-'))
|
|
201
|
+
words = words.slice(1);
|
|
202
|
+
if (words.length > 0 && RUNNER_VERBS.has(words[0]))
|
|
203
|
+
words = words.slice(1);
|
|
204
|
+
}
|
|
205
|
+
return words;
|
|
206
|
+
}
|
|
207
|
+
/** `./node_modules/.bin/nx` and `/usr/local/bin/pnpm.cmd` are the programs `nx` and `pnpm`. */
|
|
208
|
+
programName(token) {
|
|
209
|
+
const base = token.split('/').pop() ?? token;
|
|
210
|
+
return base.endsWith('.cmd') ? base.slice(0, -'.cmd'.length) : base;
|
|
211
|
+
}
|
|
212
|
+
/** True when a package-manager runner precedes the program (`pnpm …`, `npx …`). */
|
|
213
|
+
viaRunner(segment) {
|
|
214
|
+
const raw = this.words(segment);
|
|
215
|
+
return raw.length > 0 && RUNNERS.has(this.programName(raw[0]));
|
|
216
|
+
}
|
|
217
|
+
/** True when this segment actually invokes `git <subcommand>`. */
|
|
218
|
+
invokesGit(segment, subcommand) {
|
|
219
|
+
return this.gitSubcommand(segment) === subcommand;
|
|
220
|
+
}
|
|
221
|
+
/** True when ANY segment of the command invokes one of `subcommands`. */
|
|
222
|
+
commandInvokesAnyGit(command, subcommands) {
|
|
223
|
+
return this.commandSegments(command).some((seg) => subcommands.some((sub) => this.invokesGit(seg, sub)));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Split one segment into shell words, dropping quote characters (so the ARGUMENT of
|
|
227
|
+
* `echo "git merge main"` is the single word `git merge main`, never the word `git`).
|
|
228
|
+
*/
|
|
229
|
+
tokenize(segment) {
|
|
230
|
+
const tokens = [];
|
|
231
|
+
let current = '';
|
|
232
|
+
let started = false;
|
|
233
|
+
let quote = null;
|
|
234
|
+
for (let i = 0; i < segment.length; i++) {
|
|
235
|
+
const ch = segment[i];
|
|
236
|
+
if (quote !== null) {
|
|
237
|
+
if (ch === quote)
|
|
238
|
+
quote = null;
|
|
239
|
+
else
|
|
240
|
+
current += ch;
|
|
241
|
+
started = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (ch === '"' || ch === "'") {
|
|
245
|
+
quote = ch;
|
|
246
|
+
started = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (/\s/.test(ch)) {
|
|
250
|
+
if (started) {
|
|
251
|
+
tokens.push(current);
|
|
252
|
+
current = '';
|
|
253
|
+
started = false;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
current += ch;
|
|
258
|
+
started = true;
|
|
259
|
+
}
|
|
260
|
+
if (started)
|
|
261
|
+
tokens.push(current);
|
|
262
|
+
return tokens;
|
|
263
|
+
}
|
|
264
|
+
stripPrefixes(tokens) {
|
|
265
|
+
let i = 0;
|
|
266
|
+
while (i < tokens.length) {
|
|
267
|
+
const token = tokens[i];
|
|
268
|
+
if (COMMAND_PREFIXES.has(token)) {
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ENV_ASSIGNMENT.test(token)) {
|
|
273
|
+
i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
return tokens.slice(i);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
exports.CommandScanner = CommandScanner;
|
|
282
|
+
//# sourceMappingURL=command-scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-scan.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/command-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared shell-command scanning for the bash guards.\n *\n * A guard that bans a command family (`git merge`, `git push`, …) must answer one question\n * precisely: *does this command actually invoke `git <subcommand>`?* A bare\n * `/\\bgit\\s+merge\\b/.test(command)` gets that wrong in both directions:\n *\n * - False positive: `grep 'git merge main' notes.md` or `echo \"git rebase main\"` merely MENTION\n * the phrase. A diagnostic grep was blocked this way while triaging the incident that motivated\n * the merge/rebase ban.\n * - False positive: `\\b` sits between `e` and `-`, so `/\\bgit\\s+merge\\b/` matches the read-only\n * `git merge-base origin/main HEAD` — which appears in this repo's own documented build command.\n *\n * Both classes vanish if you tokenize instead of substring-match: a command invokes git only when a\n * segment's first word IS `git`, and the subcommand is then an exact token (`merge-base` is simply\n * not the token `merge`). No lookahead regex needed.\n */\n\n// Wrappers/prefixes that may precede the real command word (`sudo git merge`, `GIT_DIR=x git merge`).\nconst COMMAND_PREFIXES: ReadonlySet<string> = new Set(['sudo', 'command', 'nohup', 'time', 'env', 'exec']);\n\n// git's own global flags that consume the FOLLOWING token as their value, so\n// `git -C /some/path merge main` still resolves to the `merge` subcommand.\nconst GIT_FLAGS_WITH_VALUE: ReadonlySet<string> = new Set([\n '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path',\n]);\n\nconst ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;\n\n// Package-manager wrappers that precede the real program (`pnpm exec vitest`, `npx nx`, `yarn build-all`).\nconst RUNNERS: ReadonlySet<string> = new Set(['pnpm', 'npm', 'yarn', 'npx', 'pnpx', 'bun', 'bunx']);\n\n// The runner's own verb, consumed with it: `pnpm run build-all` and `pnpm build-all` are one command.\nconst RUNNER_VERBS: ReadonlySet<string> = new Set(['run', 'run-script', 'exec', 'dlx', 'x']);\n\n/**\n * The shell operator that JOINED one segment to the one before it. `'none'` for the first segment.\n *\n * This used to be a single boolean, `pipedInto`, and the widening is what makes composition\n * judgeable. A pipe is only one of the joins whose MEANING differs: `cure && work` short-circuits, so\n * the work cannot run when the cure failed, while `cure ; work` discards the cure's exit code and\n * runs the work anyway. A boolean that answered only \"was it a pipe?\" threw that distinction away,\n * and a guard cannot refuse the dangerous join while allowing the safe one without it.\n */\nexport type SegmentJoin = 'none' | '|' | '&&' | '||' | ';' | '&' | '\\n' | '(' | ')';\n\n/**\n * One invoked segment of a command, plus the operator that joined it to its predecessor.\n *\n * `join === '|'` is what separates `git log | grep foo` (grep consumes the pipe — reads no file) from\n * `grep foo src/` (grep reads the working tree). A guard that cares about which FILES a command\n * reads cannot tell those apart from the segment text alone, because splitting on `|` throws exactly\n * that fact away. `join === '&&'` vs `';'` is the same argument one operator over — see\n * {@link SegmentJoin}. Data-only, so a class (per CLAUDE.md).\n */\nexport class CommandSegment {\n text: string;\n join: SegmentJoin;\n\n constructor(text: string, join: SegmentJoin) {\n this.text = text;\n this.join = join;\n }\n}\n\nexport class CommandScanner {\n /**\n * Split a raw command into individually-invoked segments.\n *\n * Splits on `&&`, `||`, `;`, `|`, `&`, newline, and the `(`/`)` of subshells and `$(…)` command\n * substitution — the last of these matters, since it means `--base=$(git rebase main)` is scanned\n * as its own `git rebase main` segment rather than hiding inside a `pnpm …` segment.\n *\n * Quoted spans are opaque: a separator inside quotes is literal text, so\n * `git commit -m \"fix; ship it\"` stays one segment. (Corollary: a `$(…)` nested inside double\n * quotes is not split out. Bash would expand it; we do not scan it. Contrived enough to accept.)\n */\n commandSegments(command: string): readonly string[] {\n return this.segmentsWithJoins(command).map((s: CommandSegment): string => s.text);\n }\n\n /**\n * commandSegments, but each segment also carries the OPERATOR that joined it to its predecessor.\n * Only a guard reasoning about which files a segment reads, or about whether a failed first\n * segment can stop the rest, needs that; everything else uses commandSegments, which is this\n * method with the join dropped.\n */\n segmentsWithJoins(command: string): readonly CommandSegment[] {\n const segments: CommandSegment[] = [];\n let current = '';\n let quote: string | null = null;\n let join: SegmentJoin = 'none'; // the operator that ENDED the previous segment\n\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n\n if (quote !== null) {\n current += ch;\n // A backslash-escaped quote does not close the span (only meaningful inside \"…\").\n if (ch === quote && command[i - 1] !== '\\\\') quote = null;\n continue;\n }\n\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n current += ch;\n continue;\n }\n\n // The `&` of a REDIRECTION (`2>&1`, `1>&2`, `&>log`) is not a separator. Splitting on it\n // tore `git fetch origin main 2>&1 | tail -5` into THREE segments — `git fetch … 2>`, `1`\n // and `tail -5` — and the bare `1` is not an allowlisted command, so every guard that\n // requires all segments to pass denied the command. That is `2>&1`, the single most common\n // decoration an agent appends.\n if (ch === '&' && command[i + 1] !== '&' && (current.trimEnd().endsWith('>') || command[i + 1] === '>')) {\n current += ch;\n continue;\n }\n\n if (ch === '\\n' || ch === ';' || ch === '|' || ch === '&' || ch === '(' || ch === ')') {\n // Consume the second char of `&&` / `||` so it does not start an empty segment.\n const doubled = (ch === '|' || ch === '&') && command[i + 1] === ch;\n if (doubled) i++;\n segments.push(new CommandSegment(current, join));\n // `|` pipes into the next segment; `||` is a separator, not a pipe.\n join = doubled ? ((ch + ch) as SegmentJoin) : (ch as SegmentJoin);\n current = '';\n continue;\n }\n\n current += ch;\n }\n segments.push(new CommandSegment(current, join));\n\n return segments\n .map((s: CommandSegment): CommandSegment => new CommandSegment(s.text.trim(), s.join))\n .filter((s: CommandSegment): boolean => s.text.length > 0);\n }\n\n /**\n * One segment's shell words, with wrappers/env-assignments stripped, so `words('sudo cat a b')`\n * is `['cat', 'a', 'b']`. The public view of the same tokenizer gitSubcommand uses — a guard that\n * must inspect a NON-git command's arguments (which paths does this `grep` actually read?) needs\n * the tokens, and re-splitting on whitespace in the guard would get quoting wrong.\n */\n words(segment: string): readonly string[] {\n return this.stripPrefixes(this.tokenize(segment));\n }\n\n /**\n * The git subcommand a segment invokes, or null when the segment does not invoke git at all\n * (a different program, a mere mention inside quotes, an empty segment).\n *\n * Returns the subcommand as an EXACT token: `git merge-base …` yields `'merge-base'`, never `'merge'`.\n */\n gitSubcommand(segment: string): string | null {\n return this.gitSubcommandOf(this.stripPrefixes(this.tokenize(segment)));\n }\n\n /**\n * gitSubcommand, for a caller that already holds the segment's effective words (ShellSegmentScan\n * strips leading shell keywords, so `do git status` must be resolved from ITS words, not from the\n * raw segment text where `do` is the command).\n */\n gitSubcommandOf(words: readonly string[]): string | null {\n const tokens = this.stripPrefixes(words);\n const at = this.gitSubcommandIndex(tokens);\n return at === -1 ? null : tokens[at];\n }\n\n /**\n * The ARGUMENTS following `git <subcommand>` in this segment, or null when the segment does not\n * invoke that subcommand.\n *\n * `gitSubcommand` answers *which* subcommand; a guard that judges the subcommand's own flags needs\n * the tokens after it, and slicing them in the guard would mean re-deriving where the subcommand\n * sits — i.e. re-deriving the `sudo` / `env VAR=x` / `-C <path>` skipping this class exists to own.\n * `git -C /x commit -m \"msg\"` yields `['-m', 'msg']`, never `['/x', 'commit', '-m', 'msg']`.\n *\n * The tokens are QUOTE-STRIPPED (see tokenize), so a quoted argument arrives as ONE token holding\n * its literal text — newlines, backticks and all. That is what makes an argument's CONTENT\n * inspectable at all.\n */\n gitSubcommandArgs(segment: string, subcommand: string): readonly string[] | null {\n const tokens = this.stripPrefixes(this.tokenize(segment));\n const at = this.gitSubcommandIndex(tokens);\n if (at === -1 || tokens[at] !== subcommand) return null;\n return tokens.slice(at + 1);\n }\n\n /** Index of the subcommand token in already-prefix-stripped words, or -1 when git is not invoked. */\n private gitSubcommandIndex(tokens: readonly string[]): number {\n if (tokens.length === 0 || tokens[0] !== 'git') return -1;\n\n let i = 1;\n while (i < tokens.length) {\n const token = tokens[i];\n if (GIT_FLAGS_WITH_VALUE.has(token)) { i += 2; continue; }\n // `--git-dir=/x` style (value attached) and any other global flag.\n if (token.startsWith('-')) { i++; continue; }\n return i;\n }\n return -1;\n }\n\n /**\n * The segment's words with the package-manager WRAPPER stripped, so `pnpm exec vitest run` and\n * `vitest run` reduce to the same words. `pnpm --silent run build-all` → `['build-all']`.\n *\n * Lives HERE rather than in a guard because two guards now need it and they must agree: one blocks a\n * whole-repo build, the other blocks piping a build's output. If they disagreed about whether\n * `npx wp-build` is `wp-build`, one of them would have a spelling-shaped side door — which is exactly\n * the failure mode the runner stripping exists to close.\n */\n runnerStrippedWords(segment: string): readonly string[] {\n let words = this.words(segment);\n while (words.length > 0 && RUNNERS.has(this.programName(words[0]))) {\n words = words.slice(1);\n // The runner's own flags (`--silent`, `-r`) sit between it and the program.\n while (words.length > 0 && words[0].startsWith('-')) words = words.slice(1);\n if (words.length > 0 && RUNNER_VERBS.has(words[0])) words = words.slice(1);\n }\n return words;\n }\n\n /** `./node_modules/.bin/nx` and `/usr/local/bin/pnpm.cmd` are the programs `nx` and `pnpm`. */\n programName(token: string): string {\n const base = token.split('/').pop() ?? token;\n return base.endsWith('.cmd') ? base.slice(0, -'.cmd'.length) : base;\n }\n\n /** True when a package-manager runner precedes the program (`pnpm …`, `npx …`). */\n viaRunner(segment: string): boolean {\n const raw = this.words(segment);\n return raw.length > 0 && RUNNERS.has(this.programName(raw[0]));\n }\n\n /** True when this segment actually invokes `git <subcommand>`. */\n invokesGit(segment: string, subcommand: string): boolean {\n return this.gitSubcommand(segment) === subcommand;\n }\n\n /** True when ANY segment of the command invokes one of `subcommands`. */\n commandInvokesAnyGit(command: string, subcommands: readonly string[]): boolean {\n return this.commandSegments(command).some((seg: string) =>\n subcommands.some((sub: string) => this.invokesGit(seg, sub)));\n }\n\n /**\n * Split one segment into shell words, dropping quote characters (so the ARGUMENT of\n * `echo \"git merge main\"` is the single word `git merge main`, never the word `git`).\n */\n private tokenize(segment: string): readonly string[] {\n const tokens: string[] = [];\n let current = '';\n let started = false;\n let quote: string | null = null;\n\n for (let i = 0; i < segment.length; i++) {\n const ch = segment[i];\n\n if (quote !== null) {\n if (ch === quote) quote = null;\n else current += ch;\n started = true;\n continue;\n }\n\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n started = true;\n continue;\n }\n\n if (/\\s/.test(ch)) {\n if (started) {\n tokens.push(current);\n current = '';\n started = false;\n }\n continue;\n }\n\n current += ch;\n started = true;\n }\n if (started) tokens.push(current);\n\n return tokens;\n }\n\n private stripPrefixes(tokens: readonly string[]): readonly string[] {\n let i = 0;\n while (i < tokens.length) {\n const token = tokens[i];\n if (COMMAND_PREFIXES.has(token)) { i++; continue; }\n if (ENV_ASSIGNMENT.test(token)) { i++; continue; }\n break;\n }\n return tokens.slice(i);\n }\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseRuleConfig } from '@webpieces/rules-sdk';
|
|
2
|
+
import { AbstractRule, RuleOptions } from '@webpieces/rules-config';
|
|
3
|
+
import type { PlainRule, Rule, RuleScope, EditContext, FileContext, BashContext, Violation } from './types';
|
|
4
|
+
import type { FixHint } from './fix-hint';
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a custom rule loaded from a `rulesDir` (a plain object) so it satisfies the same
|
|
7
|
+
* runtime `Rule` contract as the built-in rule classes: it gains `shouldRun()` (driven by the
|
|
8
|
+
* rule's config entry in webpieces.config.json) from AbstractRule, and it seeds `ctx.options`
|
|
9
|
+
* with the merged option bag before delegating to the plain rule's `check()` — preserving the
|
|
10
|
+
* exact behavior custom rules had under the old options-based runner.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CustomRuleAdapter extends AbstractRule<BaseRuleConfig> implements Rule {
|
|
13
|
+
readonly scope: RuleScope;
|
|
14
|
+
readonly files: readonly string[];
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly fixHint: FixHint;
|
|
17
|
+
readonly defaultOptions: RuleOptions;
|
|
18
|
+
private readonly impl;
|
|
19
|
+
private readonly rawConfig;
|
|
20
|
+
constructor(impl: PlainRule, rawConfig: RuleOptions);
|
|
21
|
+
private buildOptions;
|
|
22
|
+
check(ctx: EditContext | FileContext | BashContext): readonly Violation[];
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CustomRuleAdapter = void 0;
|
|
4
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a custom rule loaded from a `rulesDir` (a plain object) so it satisfies the same
|
|
7
|
+
* runtime `Rule` contract as the built-in rule classes: it gains `shouldRun()` (driven by the
|
|
8
|
+
* rule's config entry in webpieces.config.json) from AbstractRule, and it seeds `ctx.options`
|
|
9
|
+
* with the merged option bag before delegating to the plain rule's `check()` — preserving the
|
|
10
|
+
* exact behavior custom rules had under the old options-based runner.
|
|
11
|
+
*/
|
|
12
|
+
class CustomRuleAdapter extends rules_config_1.AbstractRule {
|
|
13
|
+
scope;
|
|
14
|
+
files;
|
|
15
|
+
description;
|
|
16
|
+
fixHint;
|
|
17
|
+
defaultOptions;
|
|
18
|
+
impl;
|
|
19
|
+
rawConfig;
|
|
20
|
+
constructor(impl, rawConfig) {
|
|
21
|
+
// A custom rule from `rulesDir` is keyed in webpieces.config.json by its own `name`, so the
|
|
22
|
+
// two identities coincide. Stated twice rather than defaulted — see AbstractRule.
|
|
23
|
+
super(rawConfig, impl.name, impl.name);
|
|
24
|
+
this.impl = impl;
|
|
25
|
+
this.scope = impl.scope;
|
|
26
|
+
this.files = impl.files;
|
|
27
|
+
this.description = impl.description;
|
|
28
|
+
this.fixHint = impl.fixHint;
|
|
29
|
+
this.defaultOptions = impl.defaultOptions;
|
|
30
|
+
this.rawConfig = rawConfig;
|
|
31
|
+
}
|
|
32
|
+
buildOptions() {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const key of Object.keys(this.defaultOptions))
|
|
35
|
+
out[key] = this.defaultOptions[key];
|
|
36
|
+
for (const key of Object.keys(this.rawConfig)) {
|
|
37
|
+
// 'mode' is the framework-level on/off switch, not a rule option.
|
|
38
|
+
if (key === 'mode')
|
|
39
|
+
continue;
|
|
40
|
+
out[key] = this.rawConfig[key];
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
check(ctx) {
|
|
45
|
+
ctx.options = this.buildOptions();
|
|
46
|
+
return this.impl.check(ctx);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.CustomRuleAdapter = CustomRuleAdapter;
|
|
50
|
+
//# sourceMappingURL=custom-rule-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-rule-adapter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/custom-rule-adapter.ts"],"names":[],"mappings":";;;AACA,0DAAoE;AAKpE;;;;;;GAMG;AACH,MAAa,iBAAkB,SAAQ,2BAA4B;IACtD,KAAK,CAAY;IACjB,KAAK,CAAoB;IACzB,WAAW,CAAS;IACpB,OAAO,CAAU;IACjB,cAAc,CAAc;IACpB,IAAI,CAAY;IAChB,SAAS,CAAc;IAExC,YAAY,IAAe,EAAE,SAAsB;QAC/C,4FAA4F;QAC5F,kFAAkF;QAClF,KAAK,CAAC,SAA2B,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QAC1C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAEO,YAAY;QAChB,MAAM,GAAG,GAAgB,EAAE,CAAC;QAC5B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QACxF,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5C,kEAAkE;YAClE,IAAI,GAAG,KAAK,MAAM;gBAAE,SAAS;YAC7B,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,KAAK,CAAC,GAA4C;QAC9C,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;CACJ;AArCD,8CAqCC","sourcesContent":["import { BaseRuleConfig } from '@webpieces/rules-sdk';\nimport { AbstractRule, RuleOptions } from '@webpieces/rules-config';\n\nimport type { PlainRule, Rule, RuleScope, EditContext, FileContext, BashContext, Violation } from './types';\nimport type { FixHint } from './fix-hint';\n\n/**\n * Wraps a custom rule loaded from a `rulesDir` (a plain object) so it satisfies the same\n * runtime `Rule` contract as the built-in rule classes: it gains `shouldRun()` (driven by the\n * rule's config entry in webpieces.config.json) from AbstractRule, and it seeds `ctx.options`\n * with the merged option bag before delegating to the plain rule's `check()` — preserving the\n * exact behavior custom rules had under the old options-based runner.\n */\nexport class CustomRuleAdapter extends AbstractRule<BaseRuleConfig> implements Rule {\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly description: string;\n readonly fixHint: FixHint;\n readonly defaultOptions: RuleOptions;\n private readonly impl: PlainRule;\n private readonly rawConfig: RuleOptions;\n\n constructor(impl: PlainRule, rawConfig: RuleOptions) {\n // A custom rule from `rulesDir` is keyed in webpieces.config.json by its own `name`, so the\n // two identities coincide. Stated twice rather than defaulted — see AbstractRule.\n super(rawConfig as BaseRuleConfig, impl.name, impl.name);\n this.impl = impl;\n this.scope = impl.scope;\n this.files = impl.files;\n this.description = impl.description;\n this.fixHint = impl.fixHint;\n this.defaultOptions = impl.defaultOptions;\n this.rawConfig = rawConfig;\n }\n\n private buildOptions(): RuleOptions {\n const out: RuleOptions = {};\n for (const key of Object.keys(this.defaultOptions)) out[key] = this.defaultOptions[key];\n for (const key of Object.keys(this.rawConfig)) {\n // 'mode' is the framework-level on/off switch, not a rule option.\n if (key === 'mode') continue;\n out[key] = this.rawConfig[key];\n }\n return out;\n }\n\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n ctx.options = this.buildOptions();\n return this.impl.check(ctx);\n }\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Rule } from './types';
|
|
2
|
+
import { ToolKind } from '../protocol';
|
|
3
|
+
export declare class DeleteScopedRules {
|
|
4
|
+
/**
|
|
5
|
+
* `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into
|
|
6
|
+
* judging one — today, none of them, so a delete is always allowed.
|
|
7
|
+
*/
|
|
8
|
+
narrow(toolKind: ToolKind, rules: readonly Rule[]): readonly Rule[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeleteScopedRules = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The rules that judge a FILE DELETION. Empty, deliberately, and changing that is a human's call.
|
|
6
|
+
*
|
|
7
|
+
* `Delete` is a new ToolKind, reachable only from Codex's `apply_patch` `*** Delete File:` directive —
|
|
8
|
+
* Claude Code has no delete tool at all. Every rule in this engine was written against the bytes an
|
|
9
|
+
* edit ADDS (an EditContext's `addedContent`), and a deletion has none, so running them on one would
|
|
10
|
+
* either produce nothing or produce a verdict about content that is going away. Defaulting all of them
|
|
11
|
+
* OFF keeps every existing rule's behaviour exactly what it was.
|
|
12
|
+
*
|
|
13
|
+
* Two families would arguably be RIGHT to fire here, and are listed rather than enabled because the
|
|
14
|
+
* decision is a policy call, not a refactor: rules that keep a barrel/index in step with the files it
|
|
15
|
+
* exports, and rules that keep a doc in step with the file it documents. Deleting a file is precisely
|
|
16
|
+
* when those two go stale.
|
|
17
|
+
*/
|
|
18
|
+
const DELETE_SCOPED_RULES = new Set([]);
|
|
19
|
+
class DeleteScopedRules {
|
|
20
|
+
/**
|
|
21
|
+
* `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into
|
|
22
|
+
* judging one — today, none of them, so a delete is always allowed.
|
|
23
|
+
*/
|
|
24
|
+
narrow(toolKind, rules) {
|
|
25
|
+
if (toolKind !== 'Delete')
|
|
26
|
+
return rules;
|
|
27
|
+
return rules.filter((r) => DELETE_SCOPED_RULES.has(r.name));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.DeleteScopedRules = DeleteScopedRules;
|
|
31
|
+
//# sourceMappingURL=delete-scoped-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-scoped-rules.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/delete-scoped-rules.ts"],"names":[],"mappings":";;;AAGA;;;;;;;;;;;;;GAaG;AACH,MAAM,mBAAmB,GAAwB,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;AAE7D,MAAa,iBAAiB;IAC1B;;;OAGG;IACH,MAAM,CAAC,QAAkB,EAAE,KAAsB;QAC7C,IAAI,QAAQ,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACxC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAO,EAAW,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/E,CAAC;CACJ;AATD,8CASC","sourcesContent":["import { Rule } from './types';\nimport { ToolKind } from '../protocol';\n\n/**\n * The rules that judge a FILE DELETION. Empty, deliberately, and changing that is a human's call.\n *\n * `Delete` is a new ToolKind, reachable only from Codex's `apply_patch` `*** Delete File:` directive —\n * Claude Code has no delete tool at all. Every rule in this engine was written against the bytes an\n * edit ADDS (an EditContext's `addedContent`), and a deletion has none, so running them on one would\n * either produce nothing or produce a verdict about content that is going away. Defaulting all of them\n * OFF keeps every existing rule's behaviour exactly what it was.\n *\n * Two families would arguably be RIGHT to fire here, and are listed rather than enabled because the\n * decision is a policy call, not a refactor: rules that keep a barrel/index in step with the files it\n * exports, and rules that keep a doc in step with the file it documents. Deleting a file is precisely\n * when those two go stale.\n */\nconst DELETE_SCOPED_RULES: ReadonlySet<string> = new Set([]);\n\nexport class DeleteScopedRules {\n /**\n * `rules` unchanged for every other ToolKind; for a Delete, narrowed to the rules that opted into\n * judging one — today, none of them, so a delete is always allowed.\n */\n narrow(toolKind: ToolKind, rules: readonly Rule[]): readonly Rule[] {\n if (toolKind !== 'Delete') return rules;\n return rules.filter((r: Rule): boolean => DELETE_SCOPED_RULES.has(r.name));\n }\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IsLineDisabled } from './types';
|
|
2
|
+
export declare class DirectiveIndex {
|
|
3
|
+
private readonly lineDisables;
|
|
4
|
+
constructor(lineDisables: Map<number, Set<string>>);
|
|
5
|
+
isLineDisabled(lineNum: number, ruleName: string): boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseDirectives(source: string): DirectiveIndex;
|
|
8
|
+
export declare function createIsLineDisabled(source: string): IsLineDisabled;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DirectiveIndex = void 0;
|
|
4
|
+
exports.parseDirectives = parseDirectives;
|
|
5
|
+
exports.createIsLineDisabled = createIsLineDisabled;
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
// Exactly ONE disable form: `// webpieces-disable <rule>[, <rule2>] -- reason`.
|
|
8
|
+
// A rule (or comma-list of rules) is REQUIRED — there is no bare/`*` "disable everything",
|
|
9
|
+
// no `ai-hook-disable` alias, and no `-file`/`-next`/`-all` variants. A disable applies to
|
|
10
|
+
// the same line (when code precedes the `//`) or the next non-comment line.
|
|
11
|
+
const DIRECTIVE_RE = new RegExp(`//\\s*${rules_config_1.WEBPIECES_DISABLE}\\s+([\\w-]+(?:\\s*,\\s*[\\w-]+)*)(?:\\s*--\\s*.*)?\\s*$`);
|
|
12
|
+
const COMMENT_PREFIX_RE = new RegExp(`^//\\s*${rules_config_1.WEBPIECES_DISABLE}`);
|
|
13
|
+
class DirectiveIndex {
|
|
14
|
+
lineDisables;
|
|
15
|
+
constructor(lineDisables) {
|
|
16
|
+
this.lineDisables = lineDisables;
|
|
17
|
+
}
|
|
18
|
+
isLineDisabled(lineNum, ruleName) {
|
|
19
|
+
const set = this.lineDisables.get(lineNum);
|
|
20
|
+
if (!set)
|
|
21
|
+
return false;
|
|
22
|
+
return set.has(ruleName);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.DirectiveIndex = DirectiveIndex;
|
|
26
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
27
|
+
function parseRuleList(raw) {
|
|
28
|
+
return raw
|
|
29
|
+
.split(',')
|
|
30
|
+
.map((s) => s.trim())
|
|
31
|
+
.filter((s) => s.length > 0 && s !== '*');
|
|
32
|
+
}
|
|
33
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
34
|
+
function nextTargetLine(lines, lineIdx) {
|
|
35
|
+
for (let j = lineIdx + 1; j < lines.length; j += 1) {
|
|
36
|
+
const trimmed = lines[j].trim();
|
|
37
|
+
if (trimmed === '')
|
|
38
|
+
continue;
|
|
39
|
+
if (COMMENT_PREFIX_RE.test(trimmed))
|
|
40
|
+
continue;
|
|
41
|
+
return j + 1;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
46
|
+
function addDisable(map, lineNum, rules) {
|
|
47
|
+
if (!map.has(lineNum))
|
|
48
|
+
map.set(lineNum, new Set());
|
|
49
|
+
const set = map.get(lineNum);
|
|
50
|
+
for (const r of rules)
|
|
51
|
+
set.add(r);
|
|
52
|
+
}
|
|
53
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
54
|
+
function resolveTarget(line, lines, i, map, rules) {
|
|
55
|
+
const beforeComment = line.slice(0, line.indexOf('//')).trim();
|
|
56
|
+
if (beforeComment !== '') {
|
|
57
|
+
addDisable(map, i + 1, rules);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const target = nextTargetLine(lines, i);
|
|
61
|
+
if (target !== null)
|
|
62
|
+
addDisable(map, target, rules);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
66
|
+
function parseDirectives(source) {
|
|
67
|
+
const lines = source.split('\n');
|
|
68
|
+
const lineDisables = new Map();
|
|
69
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
70
|
+
const line = lines[i];
|
|
71
|
+
const match = line.match(DIRECTIVE_RE);
|
|
72
|
+
if (!match)
|
|
73
|
+
continue;
|
|
74
|
+
const rules = parseRuleList(match[1]);
|
|
75
|
+
if (rules.length === 0)
|
|
76
|
+
continue;
|
|
77
|
+
resolveTarget(line, lines, i, lineDisables, rules);
|
|
78
|
+
}
|
|
79
|
+
return new DirectiveIndex(lineDisables);
|
|
80
|
+
}
|
|
81
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
82
|
+
function createIsLineDisabled(source) {
|
|
83
|
+
const index = parseDirectives(source);
|
|
84
|
+
return (lineNum, ruleName) => index.isLineDisabled(lineNum, ruleName);
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=disable-directives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disable-directives.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/disable-directives.ts"],"names":[],"mappings":"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","sourcesContent":["import { WEBPIECES_DISABLE } from '@webpieces/rules-config';\nimport type { IsLineDisabled } from './types';\n\n// Exactly ONE disable form: `// webpieces-disable <rule>[, <rule2>] -- reason`.\n// A rule (or comma-list of rules) is REQUIRED — there is no bare/`*` \"disable everything\",\n// no `ai-hook-disable` alias, and no `-file`/`-next`/`-all` variants. A disable applies to\n// the same line (when code precedes the `//`) or the next non-comment line.\nconst DIRECTIVE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*.*)?\\\\s*$`,\n);\nconst COMMENT_PREFIX_RE = new RegExp(`^//\\\\s*${WEBPIECES_DISABLE}`);\n\nexport class DirectiveIndex {\n private readonly lineDisables: Map<number, Set<string>>;\n\n constructor(lineDisables: Map<number, Set<string>>) {\n this.lineDisables = lineDisables;\n }\n\n isLineDisabled(lineNum: number, ruleName: string): boolean {\n const set = this.lineDisables.get(lineNum);\n if (!set) return false;\n return set.has(ruleName);\n }\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction parseRuleList(raw: string): readonly string[] {\n return raw\n .split(',')\n .map((s: string): string => s.trim())\n .filter((s: string): boolean => s.length > 0 && s !== '*');\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction nextTargetLine(lines: readonly string[], lineIdx: number): number | null {\n for (let j = lineIdx + 1; j < lines.length; j += 1) {\n const trimmed = lines[j].trim();\n if (trimmed === '') continue;\n if (COMMENT_PREFIX_RE.test(trimmed)) continue;\n return j + 1;\n }\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction addDisable(map: Map<number, Set<string>>, lineNum: number, rules: readonly string[]): void {\n if (!map.has(lineNum)) map.set(lineNum, new Set<string>());\n const set = map.get(lineNum)!;\n for (const r of rules) set.add(r);\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nfunction resolveTarget(\n line: string, lines: readonly string[], i: number,\n map: Map<number, Set<string>>, rules: readonly string[],\n): void {\n const beforeComment = line.slice(0, line.indexOf('//')).trim();\n if (beforeComment !== '') {\n addDisable(map, i + 1, rules);\n } else {\n const target = nextTargetLine(lines, i);\n if (target !== null) addDisable(map, target, rules);\n }\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nexport function parseDirectives(source: string): DirectiveIndex {\n const lines = source.split('\\n');\n const lineDisables = new Map<number, Set<string>>();\n\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i];\n const match = line.match(DIRECTIVE_RE);\n if (!match) continue;\n const rules = parseRuleList(match[1]);\n if (rules.length === 0) continue;\n resolveTarget(line, lines, i, lineDisables, rules);\n }\n return new DirectiveIndex(lineDisables);\n}\n\n// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API\nexport function createIsLineDisabled(source: string): IsLineDisabled {\n const index = parseDirectives(source);\n return (lineNum: number, ruleName: string): boolean => index.isLineDisabled(lineNum, ruleName);\n}\n"]}
|