@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,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApplyPatchParser = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const protocol_1 = require("../protocol");
|
|
7
|
+
const protocol_2 = require("../protocol");
|
|
8
|
+
const tooling_common_1 = require("@webpieces/tooling-common");
|
|
9
|
+
const BEGIN_PATCH = '*** Begin Patch';
|
|
10
|
+
const END_PATCH = '*** End Patch';
|
|
11
|
+
const ADD_FILE = '*** Add File: ';
|
|
12
|
+
const UPDATE_FILE = '*** Update File: ';
|
|
13
|
+
const DELETE_FILE = '*** Delete File: ';
|
|
14
|
+
const MOVE_TO = '*** Move to: ';
|
|
15
|
+
const HUNK_HEADER = '@@';
|
|
16
|
+
/**
|
|
17
|
+
* Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already
|
|
18
|
+
* understands, so a Codex edit is judged by the identical rules a Claude edit is.
|
|
19
|
+
*
|
|
20
|
+
* The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it
|
|
21
|
+
* resembles. Two properties of it drive every design choice here:
|
|
22
|
+
*
|
|
23
|
+
* - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of
|
|
24
|
+
* `FileOperation` (each with its own kind) rather than one kind for the call.
|
|
25
|
+
* - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore
|
|
26
|
+
* not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at
|
|
27
|
+
* it is how a guard silently judges the wrong bytes. It is rejected.
|
|
28
|
+
*
|
|
29
|
+
* Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the
|
|
30
|
+
* coordinator absolute), so a relative path is resolved against the payload's `cwd`.
|
|
31
|
+
*
|
|
32
|
+
* FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler
|
|
33
|
+
* turns into a deny naming the offending line. A parser that "did its best" on an envelope it did not
|
|
34
|
+
* understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is
|
|
35
|
+
* strictly worse than refusing the call.
|
|
36
|
+
*/
|
|
37
|
+
class ApplyPatchParser {
|
|
38
|
+
parse(command, cwd) {
|
|
39
|
+
const lines = command.split('\n');
|
|
40
|
+
const begin = lines.findIndex((l) => l.trim() === BEGIN_PATCH);
|
|
41
|
+
if (begin < 0) {
|
|
42
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] envelope has no '${BEGIN_PATCH}' line — refusing to judge a patch this parser does not recognise.`);
|
|
43
|
+
}
|
|
44
|
+
const end = lines.findIndex((l) => l.trim() === END_PATCH);
|
|
45
|
+
if (end < 0 || end < begin) {
|
|
46
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] envelope has no '${END_PATCH}' line after '${BEGIN_PATCH}' — refusing to judge a truncated patch.`);
|
|
47
|
+
}
|
|
48
|
+
const body = lines.slice(begin + 1, end);
|
|
49
|
+
const ops = [];
|
|
50
|
+
let index = 0;
|
|
51
|
+
while (index < body.length) {
|
|
52
|
+
const line = body[index];
|
|
53
|
+
if (line.startsWith(ADD_FILE)) {
|
|
54
|
+
index = this.parseAdd(body, index, cwd, ops);
|
|
55
|
+
}
|
|
56
|
+
else if (line.startsWith(DELETE_FILE)) {
|
|
57
|
+
ops.push(new protocol_1.FileOperation('Delete', new protocol_2.NormalizedToolInput(this.resolve(line.slice(DELETE_FILE.length), cwd), [])));
|
|
58
|
+
index += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (line.startsWith(UPDATE_FILE)) {
|
|
61
|
+
index = this.parseUpdate(body, index, cwd, ops);
|
|
62
|
+
}
|
|
63
|
+
else if (line.trim() === '') {
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] unexpected line outside any file section: ${JSON.stringify(line)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (ops.length === 0) {
|
|
71
|
+
throw new tooling_common_1.InformAiError('[apply-patch] envelope names no files — refusing to allow a patch whose targets could not be read.');
|
|
72
|
+
}
|
|
73
|
+
return ops;
|
|
74
|
+
}
|
|
75
|
+
/** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */
|
|
76
|
+
parseAdd(body, start, cwd, ops) {
|
|
77
|
+
const filePath = this.resolve(body[start].slice(ADD_FILE.length), cwd);
|
|
78
|
+
const content = [];
|
|
79
|
+
let index = start + 1;
|
|
80
|
+
while (index < body.length && !body[index].startsWith('*** ')) {
|
|
81
|
+
const line = body[index];
|
|
82
|
+
if (!line.startsWith('+')) {
|
|
83
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] '${ADD_FILE.trim()}' body line must begin with '+', got ${JSON.stringify(line)}`);
|
|
84
|
+
}
|
|
85
|
+
content.push(line.slice(1));
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
ops.push(new protocol_1.FileOperation('Write', new protocol_2.NormalizedToolInput(filePath, [new protocol_2.NormalizedEdit('', content.join('\n'))])));
|
|
89
|
+
return index;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.
|
|
93
|
+
*
|
|
94
|
+
* A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after
|
|
95
|
+
* the call, so it is the one a path-scoped rule must see; judging the source would let a rename
|
|
96
|
+
* into a guarded directory land unjudged.
|
|
97
|
+
*/
|
|
98
|
+
parseUpdate(body, start, cwd, ops) {
|
|
99
|
+
let index = start + 1;
|
|
100
|
+
let filePath = this.resolve(body[start].slice(UPDATE_FILE.length), cwd);
|
|
101
|
+
if (index < body.length && body[index].startsWith(MOVE_TO)) {
|
|
102
|
+
filePath = this.resolve(body[index].slice(MOVE_TO.length), cwd);
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
const edits = [];
|
|
106
|
+
while (index < body.length && !body[index].startsWith('*** ')) {
|
|
107
|
+
const header = body[index];
|
|
108
|
+
if (header.trim() !== HUNK_HEADER) {
|
|
109
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] malformed hunk header ${JSON.stringify(header)} — a Codex hunk header is a bare '@@' with no line numbers.`);
|
|
110
|
+
}
|
|
111
|
+
index += 1;
|
|
112
|
+
const oldLines = [];
|
|
113
|
+
const newLines = [];
|
|
114
|
+
while (index < body.length && !body[index].startsWith('*** ') && body[index].trim() !== HUNK_HEADER) {
|
|
115
|
+
const line = body[index];
|
|
116
|
+
// A bare empty line is a CONTEXT line whose content is empty — the leading space is
|
|
117
|
+
// routinely stripped by whatever carried the patch, so treating it as malformed would
|
|
118
|
+
// deny ordinary edits to files with blank lines.
|
|
119
|
+
if (line === '') {
|
|
120
|
+
oldLines.push('');
|
|
121
|
+
newLines.push('');
|
|
122
|
+
}
|
|
123
|
+
else if (line.startsWith(' ')) {
|
|
124
|
+
oldLines.push(line.slice(1));
|
|
125
|
+
newLines.push(line.slice(1));
|
|
126
|
+
}
|
|
127
|
+
else if (line.startsWith('-')) {
|
|
128
|
+
oldLines.push(line.slice(1));
|
|
129
|
+
}
|
|
130
|
+
else if (line.startsWith('+')) {
|
|
131
|
+
newLines.push(line.slice(1));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] hunk line must begin with ' ', '-' or '+', got ${JSON.stringify(line)}`);
|
|
135
|
+
}
|
|
136
|
+
index += 1;
|
|
137
|
+
}
|
|
138
|
+
edits.push(new protocol_2.NormalizedEdit(oldLines.join('\n'), newLines.join('\n')));
|
|
139
|
+
}
|
|
140
|
+
if (edits.length === 0) {
|
|
141
|
+
throw new tooling_common_1.InformAiError(`[apply-patch] '${UPDATE_FILE.trim()}' section for ${filePath} carries no '@@' hunk — refusing to allow an edit whose content could not be read.`);
|
|
142
|
+
}
|
|
143
|
+
ops.push(new protocol_1.FileOperation('Edit', new protocol_2.NormalizedToolInput(filePath, edits)));
|
|
144
|
+
return index;
|
|
145
|
+
}
|
|
146
|
+
resolve(rawPath, cwd) {
|
|
147
|
+
const trimmed = rawPath.trim();
|
|
148
|
+
if (trimmed === '') {
|
|
149
|
+
throw new tooling_common_1.InformAiError('[apply-patch] a file directive named an empty path.');
|
|
150
|
+
}
|
|
151
|
+
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.ApplyPatchParser = ApplyPatchParser;
|
|
155
|
+
//# sourceMappingURL=apply-patch-parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-patch-parse.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/apply-patch-parse.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { FileOperation } from '../protocol';\nimport { NormalizedEdit, NormalizedToolInput } from '../protocol';\nimport { InformAiError } from '@webpieces/tooling-common';\n\nconst BEGIN_PATCH = '*** Begin Patch';\nconst END_PATCH = '*** End Patch';\nconst ADD_FILE = '*** Add File: ';\nconst UPDATE_FILE = '*** Update File: ';\nconst DELETE_FILE = '*** Delete File: ';\nconst MOVE_TO = '*** Move to: ';\nconst HUNK_HEADER = '@@';\n\n/**\n * Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already\n * understands, so a Codex edit is judged by the identical rules a Claude edit is.\n *\n * The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it\n * resembles. Two properties of it drive every design choice here:\n *\n * - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of\n * `FileOperation` (each with its own kind) rather than one kind for the call.\n * - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore\n * not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at\n * it is how a guard silently judges the wrong bytes. It is rejected.\n *\n * Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the\n * coordinator absolute), so a relative path is resolved against the payload's `cwd`.\n *\n * FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler\n * turns into a deny naming the offending line. A parser that \"did its best\" on an envelope it did not\n * understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is\n * strictly worse than refusing the call.\n */\nexport class ApplyPatchParser {\n parse(command: string, cwd: string): readonly FileOperation[] {\n const lines = command.split('\\n');\n const begin = lines.findIndex((l: string): boolean => l.trim() === BEGIN_PATCH);\n if (begin < 0) {\n throw new InformAiError(`[apply-patch] envelope has no '${BEGIN_PATCH}' line — refusing to judge a patch this parser does not recognise.`);\n }\n const end = lines.findIndex((l: string): boolean => l.trim() === END_PATCH);\n if (end < 0 || end < begin) {\n throw new InformAiError(`[apply-patch] envelope has no '${END_PATCH}' line after '${BEGIN_PATCH}' — refusing to judge a truncated patch.`);\n }\n\n const body = lines.slice(begin + 1, end);\n const ops: FileOperation[] = [];\n let index = 0;\n while (index < body.length) {\n const line = body[index];\n if (line.startsWith(ADD_FILE)) {\n index = this.parseAdd(body, index, cwd, ops);\n } else if (line.startsWith(DELETE_FILE)) {\n ops.push(new FileOperation('Delete', new NormalizedToolInput(this.resolve(line.slice(DELETE_FILE.length), cwd), [])));\n index += 1;\n } else if (line.startsWith(UPDATE_FILE)) {\n index = this.parseUpdate(body, index, cwd, ops);\n } else if (line.trim() === '') {\n index += 1;\n } else {\n throw new InformAiError(`[apply-patch] unexpected line outside any file section: ${JSON.stringify(line)}`);\n }\n }\n if (ops.length === 0) {\n throw new InformAiError('[apply-patch] envelope names no files — refusing to allow a patch whose targets could not be read.');\n }\n return ops;\n }\n\n /** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */\n private parseAdd(body: readonly string[], start: number, cwd: string, ops: FileOperation[]): number {\n const filePath = this.resolve(body[start].slice(ADD_FILE.length), cwd);\n const content: string[] = [];\n let index = start + 1;\n while (index < body.length && !body[index].startsWith('*** ')) {\n const line = body[index];\n if (!line.startsWith('+')) {\n throw new InformAiError(`[apply-patch] '${ADD_FILE.trim()}' body line must begin with '+', got ${JSON.stringify(line)}`);\n }\n content.push(line.slice(1));\n index += 1;\n }\n ops.push(new FileOperation('Write', new NormalizedToolInput(filePath, [new NormalizedEdit('', content.join('\\n'))])));\n return index;\n }\n\n /**\n * `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.\n *\n * A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after\n * the call, so it is the one a path-scoped rule must see; judging the source would let a rename\n * into a guarded directory land unjudged.\n */\n private parseUpdate(body: readonly string[], start: number, cwd: string, ops: FileOperation[]): number {\n let index = start + 1;\n let filePath = this.resolve(body[start].slice(UPDATE_FILE.length), cwd);\n if (index < body.length && body[index].startsWith(MOVE_TO)) {\n filePath = this.resolve(body[index].slice(MOVE_TO.length), cwd);\n index += 1;\n }\n const edits: NormalizedEdit[] = [];\n while (index < body.length && !body[index].startsWith('*** ')) {\n const header = body[index];\n if (header.trim() !== HUNK_HEADER) {\n throw new InformAiError(`[apply-patch] malformed hunk header ${JSON.stringify(header)} — a Codex hunk header is a bare '@@' with no line numbers.`);\n }\n index += 1;\n const oldLines: string[] = [];\n const newLines: string[] = [];\n while (index < body.length && !body[index].startsWith('*** ') && body[index].trim() !== HUNK_HEADER) {\n const line = body[index];\n // A bare empty line is a CONTEXT line whose content is empty — the leading space is\n // routinely stripped by whatever carried the patch, so treating it as malformed would\n // deny ordinary edits to files with blank lines.\n if (line === '') {\n oldLines.push('');\n newLines.push('');\n } else if (line.startsWith(' ')) {\n oldLines.push(line.slice(1));\n newLines.push(line.slice(1));\n } else if (line.startsWith('-')) {\n oldLines.push(line.slice(1));\n } else if (line.startsWith('+')) {\n newLines.push(line.slice(1));\n } else {\n throw new InformAiError(`[apply-patch] hunk line must begin with ' ', '-' or '+', got ${JSON.stringify(line)}`);\n }\n index += 1;\n }\n edits.push(new NormalizedEdit(oldLines.join('\\n'), newLines.join('\\n')));\n }\n if (edits.length === 0) {\n throw new InformAiError(`[apply-patch] '${UPDATE_FILE.trim()}' section for ${filePath} carries no '@@' hunk — refusing to allow an edit whose content could not be read.`);\n }\n ops.push(new FileOperation('Edit', new NormalizedToolInput(filePath, edits)));\n return index;\n }\n\n private resolve(rawPath: string, cwd: string): string {\n const trimmed = rawPath.trim();\n if (trimmed === '') {\n throw new InformAiError('[apply-patch] a file directive named an empty path.');\n }\n return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EffectiveTree } from './effective-tree';
|
|
2
|
+
import { GovernedPath } from './target-tree';
|
|
3
|
+
import { EditContext, FileContext, BashContext } from './types';
|
|
4
|
+
import { ToolKind, NormalizedToolInput } from '../protocol';
|
|
5
|
+
export declare class BuiltContexts {
|
|
6
|
+
readonly fileContext: FileContext;
|
|
7
|
+
readonly editContexts: readonly EditContext[];
|
|
8
|
+
constructor(fileContext: FileContext, editContexts: readonly EditContext[]);
|
|
9
|
+
}
|
|
10
|
+
export declare function buildContexts(toolKind: ToolKind, input: NormalizedToolInput, workspaceRoot: string, governed: GovernedPath): BuiltContexts;
|
|
11
|
+
export declare function buildBashContext(command: string, tree: EffectiveTree): BashContext;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BuiltContexts = void 0;
|
|
4
|
+
exports.buildContexts = buildContexts;
|
|
5
|
+
exports.buildBashContext = buildBashContext;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
8
|
+
const strip_ts_noise_1 = require("./strip-ts-noise");
|
|
9
|
+
const disable_directives_1 = require("./disable-directives");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
class BuiltContexts {
|
|
12
|
+
fileContext;
|
|
13
|
+
editContexts;
|
|
14
|
+
constructor(fileContext, editContexts) {
|
|
15
|
+
this.fileContext = fileContext;
|
|
16
|
+
this.editContexts = editContexts;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.BuiltContexts = BuiltContexts;
|
|
20
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
21
|
+
function buildContexts(toolKind, input, workspaceRoot, governed) {
|
|
22
|
+
const filePath = input.filePath;
|
|
23
|
+
const relativePath = governed.relativePath;
|
|
24
|
+
const edits = input.edits;
|
|
25
|
+
const currentFileLines = readCurrentFileLines(filePath);
|
|
26
|
+
let linesAdded = 0;
|
|
27
|
+
let linesRemoved = 0;
|
|
28
|
+
for (const e of edits) {
|
|
29
|
+
linesAdded += countLines(e.newString);
|
|
30
|
+
linesRemoved += countLines(e.oldString);
|
|
31
|
+
}
|
|
32
|
+
const projectedFileLines = toolKind === 'Write'
|
|
33
|
+
? countLines(edits.length > 0 ? edits[0].newString : '')
|
|
34
|
+
: currentFileLines + linesAdded - linesRemoved;
|
|
35
|
+
const fileContext = new types_1.FileContext(toolKind, filePath, relativePath, workspaceRoot, currentFileLines, linesAdded, linesRemoved, projectedFileLines, governed.treeRoot, governed.treeRelativePath);
|
|
36
|
+
const editContexts = edits.map((e, idx) => {
|
|
37
|
+
const addedContent = e.newString;
|
|
38
|
+
const stripped = (0, strip_ts_noise_1.stripTsNoise)(addedContent);
|
|
39
|
+
const isLineDisabled = (0, disable_directives_1.createIsLineDisabled)(addedContent);
|
|
40
|
+
return new types_1.EditContext(toolKind, idx, edits.length, filePath, relativePath, workspaceRoot, addedContent, stripped, addedContent.split('\n'), stripped.split('\n'), e.oldString, isLineDisabled);
|
|
41
|
+
});
|
|
42
|
+
return new BuiltContexts(fileContext, editContexts);
|
|
43
|
+
}
|
|
44
|
+
// The bash context is built from the RESOLVED tree, not the shell's cwd: `tree.root` is the worktree
|
|
45
|
+
// the command acts on (what the guards must judge) while `tree.governedRoot` still owns the config.
|
|
46
|
+
// webpieces-disable no-function-outside-class -- sibling of buildContexts() above; this module is a pair of module-scope context factories and a lone class for one of them would break its shape
|
|
47
|
+
function buildBashContext(command, tree) {
|
|
48
|
+
return new types_1.BashContext(command, tree.root, tree.effectiveCwd, tree.governedRoot);
|
|
49
|
+
}
|
|
50
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
51
|
+
function readCurrentFileLines(filePath) {
|
|
52
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
55
|
+
return countLines(content);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// eslint-disable-next-line @webpieces/catch-error-pattern -- file-not-found is expected for new Write targets
|
|
59
|
+
void err;
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// webpieces-disable no-function-outside-class -- existing stateless module helper moved intact with its callers; ownership extraction preserves its functional API
|
|
64
|
+
function countLines(s) {
|
|
65
|
+
if (!s)
|
|
66
|
+
return 0;
|
|
67
|
+
return s.split('\n').length;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=build-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-context.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/core/build-context.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\n\nimport { EffectiveTree } from './effective-tree';\nimport { GovernedPath } from './target-tree';\nimport { stripTsNoise } from './strip-ts-noise';\nimport { createIsLineDisabled } from './disable-directives';\nimport { EditContext, FileContext, BashContext } from './types';\nimport { ToolKind, NormalizedToolInput, NormalizedEdit } from '../protocol';\n\nexport class BuiltContexts {\n readonly fileContext: FileContext;\n readonly editContexts: readonly EditContext[];\n\n constructor(fileContext: FileContext, editContexts: readonly EditContext[]) {\n this.fileContext = fileContext;\n this.editContexts = editContexts;\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 buildContexts(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n workspaceRoot: string,\n governed: GovernedPath,\n): BuiltContexts {\n const filePath = input.filePath;\n const relativePath = governed.relativePath;\n const edits = input.edits;\n\n const currentFileLines = readCurrentFileLines(filePath);\n let linesAdded = 0;\n let linesRemoved = 0;\n for (const e of edits) {\n linesAdded += countLines(e.newString);\n linesRemoved += countLines(e.oldString);\n }\n\n const projectedFileLines =\n toolKind === 'Write'\n ? countLines(edits.length > 0 ? edits[0].newString : '')\n : currentFileLines + linesAdded - linesRemoved;\n\n const fileContext = new FileContext(\n toolKind,\n filePath,\n relativePath,\n workspaceRoot,\n currentFileLines,\n linesAdded,\n linesRemoved,\n projectedFileLines,\n governed.treeRoot,\n governed.treeRelativePath,\n );\n\n const editContexts = edits.map((e, idx) => {\n const addedContent = e.newString;\n const stripped = stripTsNoise(addedContent);\n const isLineDisabled = createIsLineDisabled(addedContent);\n return new EditContext(\n toolKind,\n idx,\n edits.length,\n filePath,\n relativePath,\n workspaceRoot,\n addedContent,\n stripped,\n addedContent.split('\\n'),\n stripped.split('\\n'),\n e.oldString,\n isLineDisabled,\n );\n });\n\n return new BuiltContexts(fileContext, editContexts);\n}\n\n// The bash context is built from the RESOLVED tree, not the shell's cwd: `tree.root` is the worktree\n// the command acts on (what the guards must judge) while `tree.governedRoot` still owns the config.\n// webpieces-disable no-function-outside-class -- sibling of buildContexts() above; this module is a pair of module-scope context factories and a lone class for one of them would break its shape\nexport function buildBashContext(command: string, tree: EffectiveTree): BashContext {\n return new BashContext(command, tree.root, tree.effectiveCwd, tree.governedRoot);\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 readCurrentFileLines(filePath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n return countLines(content);\n } catch (err: unknown) {\n // eslint-disable-next-line @webpieces/catch-error-pattern -- file-not-found is expected for new Write targets\n void err;\n return 0;\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 countLines(s: string): number {\n if (!s) return 0;\n return s.split('\\n').length;\n}\n"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell-command scanning for the bash guards.
|
|
3
|
+
*
|
|
4
|
+
* A guard that bans a command family (`git merge`, `git push`, …) must answer one question
|
|
5
|
+
* precisely: *does this command actually invoke `git <subcommand>`?* A bare
|
|
6
|
+
* `/\bgit\s+merge\b/.test(command)` gets that wrong in both directions:
|
|
7
|
+
*
|
|
8
|
+
* - False positive: `grep 'git merge main' notes.md` or `echo "git rebase main"` merely MENTION
|
|
9
|
+
* the phrase. A diagnostic grep was blocked this way while triaging the incident that motivated
|
|
10
|
+
* the merge/rebase ban.
|
|
11
|
+
* - False positive: `\b` sits between `e` and `-`, so `/\bgit\s+merge\b/` matches the read-only
|
|
12
|
+
* `git merge-base origin/main HEAD` — which appears in this repo's own documented build command.
|
|
13
|
+
*
|
|
14
|
+
* Both classes vanish if you tokenize instead of substring-match: a command invokes git only when a
|
|
15
|
+
* segment's first word IS `git`, and the subcommand is then an exact token (`merge-base` is simply
|
|
16
|
+
* not the token `merge`). No lookahead regex needed.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The shell operator that JOINED one segment to the one before it. `'none'` for the first segment.
|
|
20
|
+
*
|
|
21
|
+
* This used to be a single boolean, `pipedInto`, and the widening is what makes composition
|
|
22
|
+
* judgeable. A pipe is only one of the joins whose MEANING differs: `cure && work` short-circuits, so
|
|
23
|
+
* the work cannot run when the cure failed, while `cure ; work` discards the cure's exit code and
|
|
24
|
+
* runs the work anyway. A boolean that answered only "was it a pipe?" threw that distinction away,
|
|
25
|
+
* and a guard cannot refuse the dangerous join while allowing the safe one without it.
|
|
26
|
+
*/
|
|
27
|
+
export type SegmentJoin = 'none' | '|' | '&&' | '||' | ';' | '&' | '\n' | '(' | ')';
|
|
28
|
+
/**
|
|
29
|
+
* One invoked segment of a command, plus the operator that joined it to its predecessor.
|
|
30
|
+
*
|
|
31
|
+
* `join === '|'` is what separates `git log | grep foo` (grep consumes the pipe — reads no file) from
|
|
32
|
+
* `grep foo src/` (grep reads the working tree). A guard that cares about which FILES a command
|
|
33
|
+
* reads cannot tell those apart from the segment text alone, because splitting on `|` throws exactly
|
|
34
|
+
* that fact away. `join === '&&'` vs `';'` is the same argument one operator over — see
|
|
35
|
+
* {@link SegmentJoin}. Data-only, so a class (per CLAUDE.md).
|
|
36
|
+
*/
|
|
37
|
+
export declare class CommandSegment {
|
|
38
|
+
text: string;
|
|
39
|
+
join: SegmentJoin;
|
|
40
|
+
constructor(text: string, join: SegmentJoin);
|
|
41
|
+
}
|
|
42
|
+
export declare class CommandScanner {
|
|
43
|
+
/**
|
|
44
|
+
* Split a raw command into individually-invoked segments.
|
|
45
|
+
*
|
|
46
|
+
* Splits on `&&`, `||`, `;`, `|`, `&`, newline, and the `(`/`)` of subshells and `$(…)` command
|
|
47
|
+
* substitution — the last of these matters, since it means `--base=$(git rebase main)` is scanned
|
|
48
|
+
* as its own `git rebase main` segment rather than hiding inside a `pnpm …` segment.
|
|
49
|
+
*
|
|
50
|
+
* Quoted spans are opaque: a separator inside quotes is literal text, so
|
|
51
|
+
* `git commit -m "fix; ship it"` stays one segment. (Corollary: a `$(…)` nested inside double
|
|
52
|
+
* quotes is not split out. Bash would expand it; we do not scan it. Contrived enough to accept.)
|
|
53
|
+
*/
|
|
54
|
+
commandSegments(command: string): readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* commandSegments, but each segment also carries the OPERATOR that joined it to its predecessor.
|
|
57
|
+
* Only a guard reasoning about which files a segment reads, or about whether a failed first
|
|
58
|
+
* segment can stop the rest, needs that; everything else uses commandSegments, which is this
|
|
59
|
+
* method with the join dropped.
|
|
60
|
+
*/
|
|
61
|
+
segmentsWithJoins(command: string): readonly CommandSegment[];
|
|
62
|
+
/**
|
|
63
|
+
* One segment's shell words, with wrappers/env-assignments stripped, so `words('sudo cat a b')`
|
|
64
|
+
* is `['cat', 'a', 'b']`. The public view of the same tokenizer gitSubcommand uses — a guard that
|
|
65
|
+
* must inspect a NON-git command's arguments (which paths does this `grep` actually read?) needs
|
|
66
|
+
* the tokens, and re-splitting on whitespace in the guard would get quoting wrong.
|
|
67
|
+
*/
|
|
68
|
+
words(segment: string): readonly string[];
|
|
69
|
+
/**
|
|
70
|
+
* The git subcommand a segment invokes, or null when the segment does not invoke git at all
|
|
71
|
+
* (a different program, a mere mention inside quotes, an empty segment).
|
|
72
|
+
*
|
|
73
|
+
* Returns the subcommand as an EXACT token: `git merge-base …` yields `'merge-base'`, never `'merge'`.
|
|
74
|
+
*/
|
|
75
|
+
gitSubcommand(segment: string): string | null;
|
|
76
|
+
/**
|
|
77
|
+
* gitSubcommand, for a caller that already holds the segment's effective words (ShellSegmentScan
|
|
78
|
+
* strips leading shell keywords, so `do git status` must be resolved from ITS words, not from the
|
|
79
|
+
* raw segment text where `do` is the command).
|
|
80
|
+
*/
|
|
81
|
+
gitSubcommandOf(words: readonly string[]): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* The ARGUMENTS following `git <subcommand>` in this segment, or null when the segment does not
|
|
84
|
+
* invoke that subcommand.
|
|
85
|
+
*
|
|
86
|
+
* `gitSubcommand` answers *which* subcommand; a guard that judges the subcommand's own flags needs
|
|
87
|
+
* the tokens after it, and slicing them in the guard would mean re-deriving where the subcommand
|
|
88
|
+
* sits — i.e. re-deriving the `sudo` / `env VAR=x` / `-C <path>` skipping this class exists to own.
|
|
89
|
+
* `git -C /x commit -m "msg"` yields `['-m', 'msg']`, never `['/x', 'commit', '-m', 'msg']`.
|
|
90
|
+
*
|
|
91
|
+
* The tokens are QUOTE-STRIPPED (see tokenize), so a quoted argument arrives as ONE token holding
|
|
92
|
+
* its literal text — newlines, backticks and all. That is what makes an argument's CONTENT
|
|
93
|
+
* inspectable at all.
|
|
94
|
+
*/
|
|
95
|
+
gitSubcommandArgs(segment: string, subcommand: string): readonly string[] | null;
|
|
96
|
+
/** Index of the subcommand token in already-prefix-stripped words, or -1 when git is not invoked. */
|
|
97
|
+
private gitSubcommandIndex;
|
|
98
|
+
/**
|
|
99
|
+
* The segment's words with the package-manager WRAPPER stripped, so `pnpm exec vitest run` and
|
|
100
|
+
* `vitest run` reduce to the same words. `pnpm --silent run build-all` → `['build-all']`.
|
|
101
|
+
*
|
|
102
|
+
* Lives HERE rather than in a guard because two guards now need it and they must agree: one blocks a
|
|
103
|
+
* whole-repo build, the other blocks piping a build's output. If they disagreed about whether
|
|
104
|
+
* `npx wp-build` is `wp-build`, one of them would have a spelling-shaped side door — which is exactly
|
|
105
|
+
* the failure mode the runner stripping exists to close.
|
|
106
|
+
*/
|
|
107
|
+
runnerStrippedWords(segment: string): readonly string[];
|
|
108
|
+
/** `./node_modules/.bin/nx` and `/usr/local/bin/pnpm.cmd` are the programs `nx` and `pnpm`. */
|
|
109
|
+
programName(token: string): string;
|
|
110
|
+
/** True when a package-manager runner precedes the program (`pnpm …`, `npx …`). */
|
|
111
|
+
viaRunner(segment: string): boolean;
|
|
112
|
+
/** True when this segment actually invokes `git <subcommand>`. */
|
|
113
|
+
invokesGit(segment: string, subcommand: string): boolean;
|
|
114
|
+
/** True when ANY segment of the command invokes one of `subcommands`. */
|
|
115
|
+
commandInvokesAnyGit(command: string, subcommands: readonly string[]): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Split one segment into shell words, dropping quote characters (so the ARGUMENT of
|
|
118
|
+
* `echo "git merge main"` is the single word `git merge main`, never the word `git`).
|
|
119
|
+
*/
|
|
120
|
+
private tokenize;
|
|
121
|
+
private stripPrefixes;
|
|
122
|
+
}
|