tamperward 2.7.0 → 2.8.0
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/cli/index.js +779 -434
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,9 +2,376 @@
|
|
|
2
2
|
|
|
3
3
|
// src/git/build.ts
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// src/disk.ts
|
|
7
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readlinkSync, realpathSync } from "node:fs";
|
|
6
8
|
import { join } from "node:path";
|
|
7
9
|
|
|
10
|
+
// src/policy.ts
|
|
11
|
+
import picomatch from "picomatch";
|
|
12
|
+
var POLICY_FILE = ".tamperward.yml";
|
|
13
|
+
var BLOCK_SINCE = {};
|
|
14
|
+
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
15
|
+
const out2 = { ...rules };
|
|
16
|
+
for (const [rule, since] of Object.entries(gates)) {
|
|
17
|
+
if (since > version && out2[rule]?.severity === "block") {
|
|
18
|
+
out2[rule] = { ...out2[rule], severity: "warn" };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out2;
|
|
22
|
+
}
|
|
23
|
+
function normalizeGlob(glob) {
|
|
24
|
+
return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
|
|
25
|
+
}
|
|
26
|
+
function mergeRules(base, user) {
|
|
27
|
+
const out2 = { ...base };
|
|
28
|
+
for (const [name, cfg] of Object.entries(user ?? {})) {
|
|
29
|
+
out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
|
|
30
|
+
}
|
|
31
|
+
return out2;
|
|
32
|
+
}
|
|
33
|
+
var CONTROL = /[\u0000-\u001f\u007f]/;
|
|
34
|
+
function hasControlChar(path) {
|
|
35
|
+
return CONTROL.test(path);
|
|
36
|
+
}
|
|
37
|
+
function escapeControl(path) {
|
|
38
|
+
return path.replace(/[\u0000-\u001f\u007f]/g, (ch) => {
|
|
39
|
+
const c = ch.charCodeAt(0);
|
|
40
|
+
if (c === 10) return "\\n";
|
|
41
|
+
if (c === 13) return "\\r";
|
|
42
|
+
if (c === 9) return "\\t";
|
|
43
|
+
return `\\x${c.toString(16).padStart(2, "0")}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
var listCache = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
function listMatcher(globs4) {
|
|
48
|
+
let re = listCache.get(globs4);
|
|
49
|
+
if (re === void 0) {
|
|
50
|
+
re = globs4.length === 0 ? null : new RegExp(globs4.map((g) => `(?:${picomatch.makeRe(g, { dot: true }).source})`).join("|"));
|
|
51
|
+
listCache.set(globs4, re);
|
|
52
|
+
}
|
|
53
|
+
return re;
|
|
54
|
+
}
|
|
55
|
+
function matchesAny(path, globs4) {
|
|
56
|
+
if (!globs4) return false;
|
|
57
|
+
const re = listMatcher(globs4);
|
|
58
|
+
return re !== null && re.test(path);
|
|
59
|
+
}
|
|
60
|
+
function protectedCategory(path, policy) {
|
|
61
|
+
const cats = Object.entries(policy.protected ?? {});
|
|
62
|
+
if (hasControlChar(path)) return cats[0]?.[0] ?? "tests";
|
|
63
|
+
for (const [cat, globs4] of cats) {
|
|
64
|
+
if (matchesAny(path, globs4)) return cat;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
var anyCache = /* @__PURE__ */ new WeakMap();
|
|
69
|
+
function allProtectedGlobs(policy) {
|
|
70
|
+
const p = policy.protected ?? {};
|
|
71
|
+
let all = anyCache.get(p);
|
|
72
|
+
if (!all) {
|
|
73
|
+
all = Object.values(p).flat();
|
|
74
|
+
anyCache.set(p, all);
|
|
75
|
+
}
|
|
76
|
+
return all;
|
|
77
|
+
}
|
|
78
|
+
function isProtected(path, policy, category) {
|
|
79
|
+
if (hasControlChar(path)) return true;
|
|
80
|
+
if (category) return matchesAny(path, policy.protected?.[category]);
|
|
81
|
+
return matchesAny(path, allProtectedGlobs(policy));
|
|
82
|
+
}
|
|
83
|
+
function isPolicyFile(path) {
|
|
84
|
+
return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
|
|
85
|
+
}
|
|
86
|
+
function isIgnored(path, policy) {
|
|
87
|
+
return matchesAny(path, policy.ignore);
|
|
88
|
+
}
|
|
89
|
+
function mergeProtected(base, user) {
|
|
90
|
+
const out2 = { ...base };
|
|
91
|
+
for (const [cat, globs4] of Object.entries(user ?? {})) {
|
|
92
|
+
const have = new Set(out2[cat] ?? []);
|
|
93
|
+
out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
|
|
94
|
+
}
|
|
95
|
+
return out2;
|
|
96
|
+
}
|
|
97
|
+
function defaultPolicy(version = 1) {
|
|
98
|
+
const p = {
|
|
99
|
+
version,
|
|
100
|
+
protected: {
|
|
101
|
+
// Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
|
|
102
|
+
// .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
|
|
103
|
+
// instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
|
|
104
|
+
// strengthening, not a narrowing. picomatch expands the brace.
|
|
105
|
+
// test-skip, test-deletion and test-content-removal all gate on this
|
|
106
|
+
// class, so a repo it does not match gets rules that can NEVER fire —
|
|
107
|
+
// protection that is silently absent rather than visibly off. The JS/TS
|
|
108
|
+
// list shipped alone; these are the conventional layouts of the other
|
|
109
|
+
// ecosystems `init` is run in. (P2-13, external review.)
|
|
110
|
+
tests: [
|
|
111
|
+
// JavaScript / TypeScript
|
|
112
|
+
"**/*.test.ts",
|
|
113
|
+
"**/*.spec.ts",
|
|
114
|
+
"**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
|
|
115
|
+
"**/__tests__/**",
|
|
116
|
+
// Python (pytest / unittest layouts)
|
|
117
|
+
"**/test_*.py",
|
|
118
|
+
"**/*_test.py",
|
|
119
|
+
"**/conftest.py",
|
|
120
|
+
// Go
|
|
121
|
+
"**/*_test.go",
|
|
122
|
+
// Rust (integration tests live in tests/, unit tests inline)
|
|
123
|
+
"**/tests/**/*.rs",
|
|
124
|
+
// Ruby (RSpec / minitest)
|
|
125
|
+
"**/*_spec.rb",
|
|
126
|
+
"**/*_test.rb",
|
|
127
|
+
// Java / Kotlin / Scala (Maven & Gradle convention)
|
|
128
|
+
"**/src/test/**",
|
|
129
|
+
// PHP (PHPUnit)
|
|
130
|
+
"**/*Test.php",
|
|
131
|
+
// C# (.NET)
|
|
132
|
+
"**/*Tests.cs",
|
|
133
|
+
"**/*Test.cs"
|
|
134
|
+
],
|
|
135
|
+
config: [
|
|
136
|
+
"**/jest.config.*",
|
|
137
|
+
"**/vitest.config.*",
|
|
138
|
+
// A `vitest.workspace.*` file REPLACES the root config's selection with the
|
|
139
|
+
// union of its projects, and a `vite.config.*` carrying a `test:` key is the
|
|
140
|
+
// vitest config when no vitest.config exists — both are where a spec can be
|
|
141
|
+
// dropped from the suite without a line of it changing. ADDITIVE, like every
|
|
142
|
+
// baseline widening (the policy-diff reads it as a strengthening).
|
|
143
|
+
"**/vitest.workspace.*",
|
|
144
|
+
"**/vite.config.*",
|
|
145
|
+
"**/tsconfig*.json",
|
|
146
|
+
"**/.eslintrc*",
|
|
147
|
+
"**/eslint.config.*",
|
|
148
|
+
"**/package.json",
|
|
149
|
+
// Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
|
|
150
|
+
// see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
|
|
151
|
+
// codecov `target` lived in files no glob protected, so lowering them fired
|
|
152
|
+
// nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
|
|
153
|
+
// as a strengthening). pyproject.toml carries [tool.coverage.report].
|
|
154
|
+
"**/.coveragerc",
|
|
155
|
+
"**/pyproject.toml",
|
|
156
|
+
"**/.nycrc",
|
|
157
|
+
"**/.nycrc.*",
|
|
158
|
+
"**/nyc.config.*",
|
|
159
|
+
"**/codecov.yml",
|
|
160
|
+
"**/.codecov.yml"
|
|
161
|
+
],
|
|
162
|
+
ci: [".github/workflows/**"],
|
|
163
|
+
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
164
|
+
// rewriting it from current output is the snapshot-update move the affordance
|
|
165
|
+
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
166
|
+
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
167
|
+
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
168
|
+
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
169
|
+
// The wiring IS the enforcement: settings.json carries the PreToolUse and
|
|
170
|
+
// Stop hooks, and a pre-commit config carries the commit-time gate. These
|
|
171
|
+
// were installed by `init` and protected by nothing, so editing them out
|
|
172
|
+
// fired no rule at all. (.git/hooks/** is outside every git view and so
|
|
173
|
+
// cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
|
|
174
|
+
hooks: [
|
|
175
|
+
".husky/**",
|
|
176
|
+
"**/lefthook.*",
|
|
177
|
+
// lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
|
|
178
|
+
// there switches the gate off without touching the file init pointed at.
|
|
179
|
+
"**/lefthook-local.*",
|
|
180
|
+
".tamperward.yml",
|
|
181
|
+
"**/.tamperward.yml",
|
|
182
|
+
".claude/settings.json",
|
|
183
|
+
".claude/settings.local.json",
|
|
184
|
+
"**/.pre-commit-config.yaml",
|
|
185
|
+
// CODEOWNERS is enforcement wiring, not documentation: it is the only
|
|
186
|
+
// thing that can require a human on a change to the workflow that
|
|
187
|
+
// decides whether the gate runs at all. GitHub reads it from any of
|
|
188
|
+
// these three locations. (H2.)
|
|
189
|
+
".github/CODEOWNERS",
|
|
190
|
+
"CODEOWNERS",
|
|
191
|
+
"docs/CODEOWNERS"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
rules: {
|
|
195
|
+
"test-deletion": { severity: "block" },
|
|
196
|
+
// content gutted from a surviving spec — the class Phase 3 measured walking
|
|
197
|
+
// through the block-count rule (rows, assertion arguments, setup). Severity
|
|
198
|
+
// backed by the real-commit corpus measurement recorded in the rule's SPEC row.
|
|
199
|
+
"test-content-removal": { severity: "block" },
|
|
200
|
+
"test-skip": { severity: "block" },
|
|
201
|
+
"ts-any-cast": { severity: "block" },
|
|
202
|
+
// the unambiguous explicit casts + ts-suppression directives
|
|
203
|
+
// broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
|
|
204
|
+
// block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
|
|
205
|
+
"ts-any-launder": { severity: "warn" },
|
|
206
|
+
"lint-suppression": { severity: "block" },
|
|
207
|
+
"coverage-lowering": { severity: "block" },
|
|
208
|
+
"ci-tampering": { severity: "block" },
|
|
209
|
+
"hook-tampering": { severity: "block" },
|
|
210
|
+
"no-verify": { severity: "block" },
|
|
211
|
+
// mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
|
|
212
|
+
// when intended output changes. WARN until the §7 evidence path earns block.
|
|
213
|
+
"snapshot-rewrite": { severity: "warn" },
|
|
214
|
+
// the watcher's transient class (modified-and-restored within a turn). WARN by
|
|
215
|
+
// default: a Stop block already forces restores, and a compliant restore reads
|
|
216
|
+
// as churn — block-by-default would deadlock the honest path. Harnesses that own
|
|
217
|
+
// that semantics raise it via TAMPERWARD_TRANSIENT=block.
|
|
218
|
+
"transient-protected-mutation": { severity: "warn" },
|
|
219
|
+
// the narrow graduation candidate: snapshot moved with NO accompanying change.
|
|
220
|
+
// ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
|
|
221
|
+
// (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
|
|
222
|
+
"snapshot-only-rewrite": { severity: "warn" },
|
|
223
|
+
// heuristic — warn until precision is measured (SPEC §7)
|
|
224
|
+
"assertion-weakening": { severity: "warn" },
|
|
225
|
+
"guard-removal": { severity: "warn" }
|
|
226
|
+
},
|
|
227
|
+
ignore: [],
|
|
228
|
+
signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
|
|
229
|
+
};
|
|
230
|
+
p.rules = applyVersionGates(p.rules, version);
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/disk.ts
|
|
235
|
+
var READ_CAP = 64 * 1024 * 1024;
|
|
236
|
+
var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
|
|
237
|
+
function errText(e) {
|
|
238
|
+
return e instanceof Error ? e.message : String(e);
|
|
239
|
+
}
|
|
240
|
+
function errCode(e) {
|
|
241
|
+
return String(e?.code ?? "");
|
|
242
|
+
}
|
|
243
|
+
function irregularName(st) {
|
|
244
|
+
if (st.isFIFO()) return "fifo";
|
|
245
|
+
if (st.isSocket()) return "socket";
|
|
246
|
+
if (st.isCharacterDevice()) return "character device";
|
|
247
|
+
if (st.isBlockDevice()) return "block device";
|
|
248
|
+
return "unknown file type";
|
|
249
|
+
}
|
|
250
|
+
function meta(st) {
|
|
251
|
+
return { mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
|
|
252
|
+
}
|
|
253
|
+
var O_READ = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
254
|
+
function readRegular(abs2, st) {
|
|
255
|
+
let fd;
|
|
256
|
+
try {
|
|
257
|
+
fd = openSync(abs2, O_READ);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (errCode(e) === "ELOOP") return inspectOnce(abs2);
|
|
260
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const now = fstatSync(fd);
|
|
264
|
+
if (!now.isFile()) return { kind: "irregular", content: null, ...meta(now), detail: irregularName(now) };
|
|
265
|
+
if (now.size > READ_CAP) return { kind: "oversize", content: null, ...meta(now), detail: `${now.size} bytes` };
|
|
266
|
+
const buf = Buffer.allocUnsafe(now.size);
|
|
267
|
+
let off2 = 0;
|
|
268
|
+
while (off2 < now.size) {
|
|
269
|
+
const n = readSync(fd, buf, off2, now.size - off2, off2);
|
|
270
|
+
if (n === 0) break;
|
|
271
|
+
off2 += n;
|
|
272
|
+
}
|
|
273
|
+
return { kind: "file", content: off2 === now.size ? buf : buf.subarray(0, off2), mode: now.mode, size: off2, mtimeMs: now.mtimeMs, detail: "" };
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
276
|
+
} finally {
|
|
277
|
+
closeSync(fd);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function classify(abs2, st) {
|
|
281
|
+
if (st.isSymbolicLink()) {
|
|
282
|
+
try {
|
|
283
|
+
const target = readlinkSync(abs2);
|
|
284
|
+
return { kind: "symlink", content: Buffer.from(target, "utf8"), ...meta(st), detail: target };
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: errText(e) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (st.isDirectory()) return { kind: "directory", content: null, ...meta(st), detail: "directory" };
|
|
290
|
+
if (!st.isFile()) return { kind: "irregular", content: null, ...meta(st), detail: irregularName(st) };
|
|
291
|
+
if (st.size > READ_CAP) return { kind: "oversize", content: null, ...meta(st), detail: `${st.size} bytes` };
|
|
292
|
+
return readRegular(abs2, st);
|
|
293
|
+
}
|
|
294
|
+
function inspectOnce(abs2) {
|
|
295
|
+
let st;
|
|
296
|
+
try {
|
|
297
|
+
st = lstatSync(abs2);
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const code = errCode(e);
|
|
300
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
301
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
302
|
+
}
|
|
303
|
+
if (st.isSymbolicLink() || !st.isFile()) return classify(abs2, st);
|
|
304
|
+
return { kind: "unreadable", content: null, ...meta(st), detail: "the path changed shape while it was being read" };
|
|
305
|
+
}
|
|
306
|
+
function inspectPath(abs2) {
|
|
307
|
+
let st;
|
|
308
|
+
try {
|
|
309
|
+
st = lstatSync(abs2);
|
|
310
|
+
} catch (e) {
|
|
311
|
+
const code = errCode(e);
|
|
312
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
313
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
314
|
+
}
|
|
315
|
+
return classify(abs2, st);
|
|
316
|
+
}
|
|
317
|
+
function inspectRel(cwd, rel) {
|
|
318
|
+
return inspectPath(join(cwd, rel));
|
|
319
|
+
}
|
|
320
|
+
function inspectResolved(abs2) {
|
|
321
|
+
let real;
|
|
322
|
+
try {
|
|
323
|
+
real = realpathSync(abs2);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
const code = errCode(e);
|
|
326
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { kind: "absent", content: null, ...NO_META, detail: "" };
|
|
327
|
+
return { kind: "unreadable", content: null, ...NO_META, detail: errText(e) };
|
|
328
|
+
}
|
|
329
|
+
return inspectPath(real);
|
|
330
|
+
}
|
|
331
|
+
function textOf(e) {
|
|
332
|
+
return e.content == null ? null : e.content.toString("utf8");
|
|
333
|
+
}
|
|
334
|
+
function describe(e, shown) {
|
|
335
|
+
switch (e.kind) {
|
|
336
|
+
case "symlink":
|
|
337
|
+
return `Protected path ${shown} is a symbolic link to ${escapeControl(e.detail)}. The gate never follows a link, so what the runner will read there cannot be judged.`;
|
|
338
|
+
case "directory":
|
|
339
|
+
return `Protected path ${shown} is a directory where a file is expected, so it cannot be judged.`;
|
|
340
|
+
case "irregular":
|
|
341
|
+
return `Protected path ${shown} is a ${e.detail}, not a regular file, so it cannot be read to judge it.`;
|
|
342
|
+
case "oversize":
|
|
343
|
+
return `Protected file ${shown} is ${e.detail}, above the ${READ_CAP / (1024 * 1024)} MiB the gate reads, so it is judged by name.`;
|
|
344
|
+
default:
|
|
345
|
+
return `Protected file ${shown} cannot be read to judge it (${e.detail}).`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function unjudgeableFinding(cwd, rel) {
|
|
349
|
+
const e = inspectRel(cwd, rel);
|
|
350
|
+
if (e.kind === "file" || e.kind === "absent") return null;
|
|
351
|
+
const shown = escapeControl(rel);
|
|
352
|
+
return {
|
|
353
|
+
rule: "hidden-drift",
|
|
354
|
+
severity: "block",
|
|
355
|
+
file: rel,
|
|
356
|
+
message: describe(e, shown),
|
|
357
|
+
evidence: `${shown}: ${e.kind}${e.detail ? ` (${escapeControl(e.detail)})` : ""}`,
|
|
358
|
+
remediation: "Put a regular file the gate can read at the path \u2014 no symbolic link, FIFO, socket or device, and under 64 MiB \u2014 through a tool call the gate can see, or remove it.",
|
|
359
|
+
signoff: { required: true, command: `tamperward allow hidden-drift --file ${shown} --reason "..."` }
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function unjudgeableProtected(cwd, policy, changes) {
|
|
363
|
+
const out2 = [];
|
|
364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const c of changes) {
|
|
366
|
+
if (c.kind !== "file" || c.op === "delete" || seen.has(c.path)) continue;
|
|
367
|
+
seen.add(c.path);
|
|
368
|
+
if (!isProtected(c.path, policy)) continue;
|
|
369
|
+
const f = unjudgeableFinding(cwd, c.path);
|
|
370
|
+
if (f) out2.push(f);
|
|
371
|
+
}
|
|
372
|
+
return out2;
|
|
373
|
+
}
|
|
374
|
+
|
|
8
375
|
// src/diff/parse.ts
|
|
9
376
|
var HUNK_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
10
377
|
var C_ESCAPES = { a: 7, b: 8, f: 12, n: 10, r: 13, t: 9, v: 11, "\\": 92, '"': 34 };
|
|
@@ -251,11 +618,7 @@ function blobAt(rev, path, cwd) {
|
|
|
251
618
|
}
|
|
252
619
|
}
|
|
253
620
|
function fromDisk(path, cwd) {
|
|
254
|
-
|
|
255
|
-
return readFileSync(join(cwd ?? process.cwd(), path), "utf8");
|
|
256
|
-
} catch {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
621
|
+
return textOf(inspectRel(cwd ?? process.cwd(), path));
|
|
259
622
|
}
|
|
260
623
|
function enrich(c, beforeReader, afterReader) {
|
|
261
624
|
if (c.kind !== "file") return c;
|
|
@@ -353,6 +716,9 @@ function underSkippedDir(rel) {
|
|
|
353
716
|
return rel.split("/").some((seg) => SKIP_DIRS.has(seg));
|
|
354
717
|
}
|
|
355
718
|
function ignoredPaths(opts = {}, keep) {
|
|
719
|
+
return ignoredTree(opts, keep).files;
|
|
720
|
+
}
|
|
721
|
+
function ignoredTree(opts = {}, keep) {
|
|
356
722
|
const files = [];
|
|
357
723
|
const dirs = [];
|
|
358
724
|
for (const entry of git([...IGNORED_LS, "--directory"], opts.cwd).split("\0").filter(Boolean)) {
|
|
@@ -364,7 +730,7 @@ function ignoredPaths(opts = {}, keep) {
|
|
|
364
730
|
const expanded = git([...IGNORED_LS, "--", ...dirs.map((d) => `:(literal)${d}`)], opts.cwd).split("\0").filter(Boolean);
|
|
365
731
|
for (const rel of expanded) if (!underSkippedDir(rel)) files.push(rel);
|
|
366
732
|
}
|
|
367
|
-
return files.filter(keep);
|
|
733
|
+
return { dirs, files: files.filter(keep) };
|
|
368
734
|
}
|
|
369
735
|
function ignoredAdds(opts = {}, keep, exclude) {
|
|
370
736
|
return ignoredPaths(opts, keep).filter((rel) => !exclude?.has(rel)).map((rel) => addOf(rel, opts.cwd));
|
|
@@ -576,212 +942,16 @@ var noVerify = {
|
|
|
576
942
|
};
|
|
577
943
|
|
|
578
944
|
// src/detectors/ts-any-cast.ts
|
|
579
|
-
import ts from "typescript";
|
|
580
|
-
|
|
581
|
-
// src/diff/select.ts
|
|
582
|
-
function addedLines(c) {
|
|
583
|
-
if (c.kind !== "file") return [];
|
|
584
|
-
return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "add"));
|
|
585
|
-
}
|
|
586
|
-
function removedLines(c) {
|
|
587
|
-
if (c.kind !== "file") return [];
|
|
588
|
-
return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "del"));
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// src/policy.ts
|
|
592
|
-
import picomatch from "picomatch";
|
|
593
|
-
var POLICY_FILE = ".tamperward.yml";
|
|
594
|
-
var BLOCK_SINCE = {};
|
|
595
|
-
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
596
|
-
const out2 = { ...rules };
|
|
597
|
-
for (const [rule, since] of Object.entries(gates)) {
|
|
598
|
-
if (since > version && out2[rule]?.severity === "block") {
|
|
599
|
-
out2[rule] = { ...out2[rule], severity: "warn" };
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return out2;
|
|
603
|
-
}
|
|
604
|
-
function normalizeGlob(glob) {
|
|
605
|
-
return glob.startsWith("/") ? glob.replace(/^\/+/, "") : glob;
|
|
606
|
-
}
|
|
607
|
-
function mergeRules(base, user) {
|
|
608
|
-
const out2 = { ...base };
|
|
609
|
-
for (const [name, cfg] of Object.entries(user ?? {})) {
|
|
610
|
-
out2[name] = { ...out2[name] ?? {}, ...cfg ?? {} };
|
|
611
|
-
}
|
|
612
|
-
return out2;
|
|
613
|
-
}
|
|
614
|
-
var cache = /* @__PURE__ */ new Map();
|
|
615
|
-
function matcher(glob) {
|
|
616
|
-
let m = cache.get(glob);
|
|
617
|
-
if (!m) {
|
|
618
|
-
m = picomatch(glob, { dot: true });
|
|
619
|
-
cache.set(glob, m);
|
|
620
|
-
}
|
|
621
|
-
return m;
|
|
622
|
-
}
|
|
623
|
-
function matchesAny(path, globs4) {
|
|
624
|
-
return !!globs4 && globs4.some((g) => matcher(g)(path));
|
|
625
|
-
}
|
|
626
|
-
function protectedCategory(path, policy) {
|
|
627
|
-
for (const [cat, globs4] of Object.entries(policy.protected ?? {})) {
|
|
628
|
-
if (matchesAny(path, globs4)) return cat;
|
|
629
|
-
}
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
function isProtected(path, policy, category) {
|
|
633
|
-
if (category) return matchesAny(path, policy.protected?.[category]);
|
|
634
|
-
return protectedCategory(path, policy) !== null;
|
|
635
|
-
}
|
|
636
|
-
function isPolicyFile(path) {
|
|
637
|
-
return path === POLICY_FILE || path.endsWith("/" + POLICY_FILE);
|
|
638
|
-
}
|
|
639
|
-
function isIgnored(path, policy) {
|
|
640
|
-
return matchesAny(path, policy.ignore);
|
|
641
|
-
}
|
|
642
|
-
function mergeProtected(base, user) {
|
|
643
|
-
const out2 = { ...base };
|
|
644
|
-
for (const [cat, globs4] of Object.entries(user ?? {})) {
|
|
645
|
-
const have = new Set(out2[cat] ?? []);
|
|
646
|
-
out2[cat] = [...out2[cat] ?? [], ...(globs4 ?? []).filter((g) => !have.has(g))];
|
|
647
|
-
}
|
|
648
|
-
return out2;
|
|
945
|
+
import ts from "typescript";
|
|
946
|
+
|
|
947
|
+
// src/diff/select.ts
|
|
948
|
+
function addedLines(c) {
|
|
949
|
+
if (c.kind !== "file") return [];
|
|
950
|
+
return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "add"));
|
|
649
951
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
protected: {
|
|
654
|
-
// Cover every JS/TS test extension, not just .test.ts — the multi-repo FP study found
|
|
655
|
-
// .test.tsx/.spec.tsx (hono, zustand) slipped the glob, so legit test-file casts blocked
|
|
656
|
-
// instead of warning. Kept ADDITIVE (originals + new extensions) so the policy-diff sees a
|
|
657
|
-
// strengthening, not a narrowing. picomatch expands the brace.
|
|
658
|
-
// test-skip, test-deletion and test-content-removal all gate on this
|
|
659
|
-
// class, so a repo it does not match gets rules that can NEVER fire —
|
|
660
|
-
// protection that is silently absent rather than visibly off. The JS/TS
|
|
661
|
-
// list shipped alone; these are the conventional layouts of the other
|
|
662
|
-
// ecosystems `init` is run in. (P2-13, external review.)
|
|
663
|
-
tests: [
|
|
664
|
-
// JavaScript / TypeScript
|
|
665
|
-
"**/*.test.ts",
|
|
666
|
-
"**/*.spec.ts",
|
|
667
|
-
"**/*.{test,spec}.{tsx,cts,mts,js,jsx,cjs,mjs}",
|
|
668
|
-
"**/__tests__/**",
|
|
669
|
-
// Python (pytest / unittest layouts)
|
|
670
|
-
"**/test_*.py",
|
|
671
|
-
"**/*_test.py",
|
|
672
|
-
"**/conftest.py",
|
|
673
|
-
// Go
|
|
674
|
-
"**/*_test.go",
|
|
675
|
-
// Rust (integration tests live in tests/, unit tests inline)
|
|
676
|
-
"**/tests/**/*.rs",
|
|
677
|
-
// Ruby (RSpec / minitest)
|
|
678
|
-
"**/*_spec.rb",
|
|
679
|
-
"**/*_test.rb",
|
|
680
|
-
// Java / Kotlin / Scala (Maven & Gradle convention)
|
|
681
|
-
"**/src/test/**",
|
|
682
|
-
// PHP (PHPUnit)
|
|
683
|
-
"**/*Test.php",
|
|
684
|
-
// C# (.NET)
|
|
685
|
-
"**/*Tests.cs",
|
|
686
|
-
"**/*Test.cs"
|
|
687
|
-
],
|
|
688
|
-
config: [
|
|
689
|
-
"**/jest.config.*",
|
|
690
|
-
"**/vitest.config.*",
|
|
691
|
-
// A `vitest.workspace.*` file REPLACES the root config's selection with the
|
|
692
|
-
// union of its projects, and a `vite.config.*` carrying a `test:` key is the
|
|
693
|
-
// vitest config when no vitest.config exists — both are where a spec can be
|
|
694
|
-
// dropped from the suite without a line of it changing. ADDITIVE, like every
|
|
695
|
-
// baseline widening (the policy-diff reads it as a strengthening).
|
|
696
|
-
"**/vitest.workspace.*",
|
|
697
|
-
"**/vite.config.*",
|
|
698
|
-
"**/tsconfig*.json",
|
|
699
|
-
"**/.eslintrc*",
|
|
700
|
-
"**/eslint.config.*",
|
|
701
|
-
"**/package.json",
|
|
702
|
-
// Coverage gates outside the Jest/Vitest shapes. coverage-lowering could only
|
|
703
|
-
// see a threshold it was shown: a Python `fail_under`, an nyc `lines`, a
|
|
704
|
-
// codecov `target` lived in files no glob protected, so lowering them fired
|
|
705
|
-
// nothing. ADDITIVE, like every baseline widening (the policy-diff reads it
|
|
706
|
-
// as a strengthening). pyproject.toml carries [tool.coverage.report].
|
|
707
|
-
"**/.coveragerc",
|
|
708
|
-
"**/pyproject.toml",
|
|
709
|
-
"**/.nycrc",
|
|
710
|
-
"**/.nycrc.*",
|
|
711
|
-
"**/nyc.config.*",
|
|
712
|
-
"**/codecov.yml",
|
|
713
|
-
"**/.codecov.yml"
|
|
714
|
-
],
|
|
715
|
-
ci: [".github/workflows/**"],
|
|
716
|
-
// Recorded expected outputs. An assertion stored as data is still an assertion;
|
|
717
|
-
// rewriting it from current output is the snapshot-update move the affordance
|
|
718
|
-
// experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
|
|
719
|
-
// tap-snapshots/** added from Phase-3 evidence: node-tar's recorded expectations
|
|
720
|
-
// live there as .cjs and were rewritten unseen (PHASE3.md §4).
|
|
721
|
-
snapshots: ["**/*.snap", "**/__snapshots__/**", "**/tap-snapshots/**", "**/golden/**", "**/*.golden.*"],
|
|
722
|
-
// The wiring IS the enforcement: settings.json carries the PreToolUse and
|
|
723
|
-
// Stop hooks, and a pre-commit config carries the commit-time gate. These
|
|
724
|
-
// were installed by `init` and protected by nothing, so editing them out
|
|
725
|
-
// fired no rule at all. (.git/hooks/** is outside every git view and so
|
|
726
|
-
// cannot be covered here — noted in SECURITY-ENVELOPE.md.) (P1-4, review.)
|
|
727
|
-
hooks: [
|
|
728
|
-
".husky/**",
|
|
729
|
-
"**/lefthook.*",
|
|
730
|
-
// lefthook merges `lefthook-local.yml` OVER `lefthook.yml`: a `skip: true`
|
|
731
|
-
// there switches the gate off without touching the file init pointed at.
|
|
732
|
-
"**/lefthook-local.*",
|
|
733
|
-
".tamperward.yml",
|
|
734
|
-
"**/.tamperward.yml",
|
|
735
|
-
".claude/settings.json",
|
|
736
|
-
".claude/settings.local.json",
|
|
737
|
-
"**/.pre-commit-config.yaml",
|
|
738
|
-
// CODEOWNERS is enforcement wiring, not documentation: it is the only
|
|
739
|
-
// thing that can require a human on a change to the workflow that
|
|
740
|
-
// decides whether the gate runs at all. GitHub reads it from any of
|
|
741
|
-
// these three locations. (H2.)
|
|
742
|
-
".github/CODEOWNERS",
|
|
743
|
-
"CODEOWNERS",
|
|
744
|
-
"docs/CODEOWNERS"
|
|
745
|
-
]
|
|
746
|
-
},
|
|
747
|
-
rules: {
|
|
748
|
-
"test-deletion": { severity: "block" },
|
|
749
|
-
// content gutted from a surviving spec — the class Phase 3 measured walking
|
|
750
|
-
// through the block-count rule (rows, assertion arguments, setup). Severity
|
|
751
|
-
// backed by the real-commit corpus measurement recorded in the rule's SPEC row.
|
|
752
|
-
"test-content-removal": { severity: "block" },
|
|
753
|
-
"test-skip": { severity: "block" },
|
|
754
|
-
"ts-any-cast": { severity: "block" },
|
|
755
|
-
// the unambiguous explicit casts + ts-suppression directives
|
|
756
|
-
// broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
|
|
757
|
-
// block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
|
|
758
|
-
"ts-any-launder": { severity: "warn" },
|
|
759
|
-
"lint-suppression": { severity: "block" },
|
|
760
|
-
"coverage-lowering": { severity: "block" },
|
|
761
|
-
"ci-tampering": { severity: "block" },
|
|
762
|
-
"hook-tampering": { severity: "block" },
|
|
763
|
-
"no-verify": { severity: "block" },
|
|
764
|
-
// mechanical but intent-ambiguous: updating a snapshot is the legitimate workflow
|
|
765
|
-
// when intended output changes. WARN until the §7 evidence path earns block.
|
|
766
|
-
"snapshot-rewrite": { severity: "warn" },
|
|
767
|
-
// the watcher's transient class (modified-and-restored within a turn). WARN by
|
|
768
|
-
// default: a Stop block already forces restores, and a compliant restore reads
|
|
769
|
-
// as churn — block-by-default would deadlock the honest path. Harnesses that own
|
|
770
|
-
// that semantics raise it via TAMPERWARD_TRANSIENT=block.
|
|
771
|
-
"transient-protected-mutation": { severity: "warn" },
|
|
772
|
-
// the narrow graduation candidate: snapshot moved with NO accompanying change.
|
|
773
|
-
// ~0.06% FP on 1,652 audited mainline commits, 7/7 TP on observed tampers
|
|
774
|
-
// (harness/fp-study/). warn until a version: gate graduates it (BLOCK_SINCE).
|
|
775
|
-
"snapshot-only-rewrite": { severity: "warn" },
|
|
776
|
-
// heuristic — warn until precision is measured (SPEC §7)
|
|
777
|
-
"assertion-weakening": { severity: "warn" },
|
|
778
|
-
"guard-removal": { severity: "warn" }
|
|
779
|
-
},
|
|
780
|
-
ignore: [],
|
|
781
|
-
signoff: { requiredFor: ["block"], ledger: ".tamperward/ledger.jsonl" }
|
|
782
|
-
};
|
|
783
|
-
p.rules = applyVersionGates(p.rules, version);
|
|
784
|
-
return p;
|
|
952
|
+
function removedLines(c) {
|
|
953
|
+
if (c.kind !== "file") return [];
|
|
954
|
+
return c.hunks.flatMap((h) => h.lines.filter((l) => l.type === "del"));
|
|
785
955
|
}
|
|
786
956
|
|
|
787
957
|
// src/detectors/files.ts
|
|
@@ -1164,12 +1334,12 @@ import ts2 from "typescript";
|
|
|
1164
1334
|
|
|
1165
1335
|
// src/detectors/repo.ts
|
|
1166
1336
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1167
|
-
var
|
|
1337
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1168
1338
|
function trackedFiles(ctx) {
|
|
1169
1339
|
if (!ctx) return null;
|
|
1170
1340
|
if (ctx.trackedFiles) return ctx.trackedFiles;
|
|
1171
1341
|
if (!ctx.cwd) return null;
|
|
1172
|
-
const hit =
|
|
1342
|
+
const hit = cache.get(ctx.cwd);
|
|
1173
1343
|
if (hit !== void 0) return hit;
|
|
1174
1344
|
let files = null;
|
|
1175
1345
|
try {
|
|
@@ -1182,10 +1352,54 @@ function trackedFiles(ctx) {
|
|
|
1182
1352
|
} catch {
|
|
1183
1353
|
files = null;
|
|
1184
1354
|
}
|
|
1185
|
-
|
|
1355
|
+
cache.set(ctx.cwd, files);
|
|
1186
1356
|
return files;
|
|
1187
1357
|
}
|
|
1188
1358
|
var branchCache = /* @__PURE__ */ new Map();
|
|
1359
|
+
var refsCache = /* @__PURE__ */ new Map();
|
|
1360
|
+
function branchRefs(ctx) {
|
|
1361
|
+
if (!ctx?.cwd) return null;
|
|
1362
|
+
const hit = refsCache.get(ctx.cwd);
|
|
1363
|
+
if (hit !== void 0) return hit;
|
|
1364
|
+
let refs = null;
|
|
1365
|
+
try {
|
|
1366
|
+
const out2 = execFileSync2("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads", "refs/remotes"], {
|
|
1367
|
+
cwd: ctx.cwd,
|
|
1368
|
+
encoding: "utf8",
|
|
1369
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1370
|
+
});
|
|
1371
|
+
refs = new Set(out2.split("\n").map((l) => l.trim()).filter(Boolean));
|
|
1372
|
+
} catch {
|
|
1373
|
+
refs = null;
|
|
1374
|
+
}
|
|
1375
|
+
refsCache.set(ctx.cwd, refs);
|
|
1376
|
+
return refs;
|
|
1377
|
+
}
|
|
1378
|
+
function branchExists(name, ctx) {
|
|
1379
|
+
const refs = branchRefs(ctx);
|
|
1380
|
+
if (!refs) return null;
|
|
1381
|
+
if (refs.has(name)) return true;
|
|
1382
|
+
for (const r of refs) if (r.endsWith("/" + name)) return true;
|
|
1383
|
+
return false;
|
|
1384
|
+
}
|
|
1385
|
+
function revIsHead(rev, ctx) {
|
|
1386
|
+
if (!ctx?.cwd || rev.startsWith("-")) return null;
|
|
1387
|
+
const parse3 = (r) => {
|
|
1388
|
+
try {
|
|
1389
|
+
return execFileSync2("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${r}^{commit}`], {
|
|
1390
|
+
cwd: ctx.cwd,
|
|
1391
|
+
encoding: "utf8",
|
|
1392
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1393
|
+
}).trim();
|
|
1394
|
+
} catch {
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
const head = parse3("HEAD");
|
|
1399
|
+
const target = parse3(rev);
|
|
1400
|
+
if (!head || !target) return null;
|
|
1401
|
+
return head === target;
|
|
1402
|
+
}
|
|
1189
1403
|
function defaultBranch(ctx) {
|
|
1190
1404
|
if (!ctx?.cwd) return null;
|
|
1191
1405
|
const hit = branchCache.get(ctx.cwd);
|
|
@@ -1205,15 +1419,16 @@ function defaultBranch(ctx) {
|
|
|
1205
1419
|
return name;
|
|
1206
1420
|
}
|
|
1207
1421
|
var dirOf = (t) => t.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1208
|
-
function containsProtected(token, policy, category, ctx) {
|
|
1422
|
+
function containsProtected(token, policy, category, ctx, except) {
|
|
1209
1423
|
const dir = dirOf(token);
|
|
1210
1424
|
if (!dir || dir === "." || dir === "/" || dir.startsWith("-")) return false;
|
|
1425
|
+
const counts = (p) => isProtected(p, policy, category) && !(except && isProtected(p, policy, except));
|
|
1211
1426
|
const files = trackedFiles(ctx);
|
|
1212
1427
|
if (files) {
|
|
1213
1428
|
const prefix = dir + "/";
|
|
1214
|
-
return files.some((f) => f.startsWith(prefix) &&
|
|
1429
|
+
return files.some((f) => f.startsWith(prefix) && counts(f));
|
|
1215
1430
|
}
|
|
1216
|
-
if (
|
|
1431
|
+
if (counts(dir + "/probe")) return true;
|
|
1217
1432
|
const base = dir.split("/").pop() ?? "";
|
|
1218
1433
|
return category === "tests" && /^(?:test|tests|__tests__|spec|specs)$/i.test(base);
|
|
1219
1434
|
}
|
|
@@ -1316,6 +1531,10 @@ function parseThresholds(src) {
|
|
|
1316
1531
|
if (!ts2.isPropertyAssignment(p)) continue;
|
|
1317
1532
|
const k = keyName(p.name);
|
|
1318
1533
|
if (k === null || isMetric(k)) continue;
|
|
1534
|
+
if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts2.SyntaxKind.TrueKeyword) {
|
|
1535
|
+
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1319
1538
|
const m = metricsOf(p.initializer, resolve5);
|
|
1320
1539
|
if (!m) continue;
|
|
1321
1540
|
if (k === "global") res.global = merge(res.global, m);
|
|
@@ -1409,7 +1628,7 @@ function sectionText(src, key2) {
|
|
|
1409
1628
|
return "";
|
|
1410
1629
|
}
|
|
1411
1630
|
}
|
|
1412
|
-
var BENIGN_EXEMPTION = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings)(?:\/|$|\*)|\.(?:test|spec|stories|d)\.|\.(?:config|setup)
|
|
1631
|
+
var BENIGN_EXEMPTION = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings|generated|__generated__|codegen|migrations?)(?:\/|$|\*)|\.(?:test|spec|stories|d|gen|generated|pb|e2e-spec)\.|\.(?:config|setup)\.|(?:^|[/!*])(?:manage|wsgi|asgi|__init__)\.py$|(?:^|[/!*])main\.[jt]sx?$|\.git\b|\.cache|\.next|\.nuxt|\.output|\.idea|\.d\.ts|\.json$|\.md$|\.css$|\.s[ac]ss$/i;
|
|
1413
1632
|
function compareMetrics(scope, before, after, out2) {
|
|
1414
1633
|
if (!before) return;
|
|
1415
1634
|
for (const m of METRICS) {
|
|
@@ -1553,20 +1772,28 @@ var SIMPLE_FILES = [
|
|
|
1553
1772
|
];
|
|
1554
1773
|
var lessThan = (a, b) => a === "auto" ? b !== "auto" : b === "auto" ? false : a < b;
|
|
1555
1774
|
var numOf = (s) => s === "auto" ? "auto" : Number(s);
|
|
1556
|
-
function
|
|
1557
|
-
const out2 = [];
|
|
1775
|
+
function* scopedLines(src) {
|
|
1558
1776
|
const stack = [];
|
|
1559
1777
|
for (const line of src.split("\n")) {
|
|
1560
|
-
|
|
1561
|
-
if (
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
while (stack.length && stack[stack.length - 1].ind >= ind) stack.pop();
|
|
1566
|
-
stack.push({ ind, key: m[2] });
|
|
1567
|
-
}
|
|
1568
|
-
scope = [...stack.slice(0, -1)].reverse().find((s) => /^(?:project|patch|changes)$/.test(s.key))?.key ?? "";
|
|
1778
|
+
const m = line.match(/^(\s*)([\w-]+):/);
|
|
1779
|
+
if (m) {
|
|
1780
|
+
const ind = m[1].length;
|
|
1781
|
+
while (stack.length && stack[stack.length - 1].ind >= ind) stack.pop();
|
|
1782
|
+
stack.push({ ind, key: m[2] });
|
|
1569
1783
|
}
|
|
1784
|
+
const scope = [...stack.slice(0, -1)].reverse().find((s) => /^(?:project|patch|changes)$/.test(s.key))?.key ?? "";
|
|
1785
|
+
yield [line, scope];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function scopedKeys(src, key2) {
|
|
1789
|
+
const out2 = [];
|
|
1790
|
+
for (const [line, scope] of scopedLines(src)) if (key2.test(line)) out2.push(scope);
|
|
1791
|
+
return out2;
|
|
1792
|
+
}
|
|
1793
|
+
function numbersOf(src, key2, scoped = false) {
|
|
1794
|
+
const out2 = [];
|
|
1795
|
+
for (const [line, inScope] of scopedLines(src)) {
|
|
1796
|
+
const scope = scoped ? inScope : "";
|
|
1570
1797
|
if (key2.global) {
|
|
1571
1798
|
for (const m of line.matchAll(key2)) out2.push({ v: numOf(m[1]), scope });
|
|
1572
1799
|
} else {
|
|
@@ -1645,6 +1872,9 @@ function simpleNarrowings(path, before, after) {
|
|
|
1645
1872
|
const off2 = (s) => new Set(s.split("\n").map((l) => l.match(/^\s*(project|patch):\s*off\b/)?.[1]).filter((x) => !!x));
|
|
1646
1873
|
const had = off2(before);
|
|
1647
1874
|
for (const s of off2(after)) if (!had.has(s)) out2.push(`codecov status.${s} switched off`);
|
|
1875
|
+
const informational = (s) => new Set(scopedKeys(s, /^\s*informational:\s*true\b/).filter((scope) => scope !== "patch" && scope !== "changes"));
|
|
1876
|
+
const was = informational(before);
|
|
1877
|
+
for (const scope of informational(after)) if (!was.has(scope)) out2.push(`codecov ${scope || "project"} status informational: true added \u2014 the status can no longer fail`);
|
|
1648
1878
|
}
|
|
1649
1879
|
return out2;
|
|
1650
1880
|
}
|
|
@@ -1688,13 +1918,14 @@ var SWITCHES = [
|
|
|
1688
1918
|
re: /"?check-coverage"?\s*[:=]\s*false\b/,
|
|
1689
1919
|
message: "check-coverage: false \u2014 nyc will no longer fail on the thresholds.",
|
|
1690
1920
|
remediation: "Keep check-coverage on; raise real coverage instead."
|
|
1691
|
-
},
|
|
1692
|
-
{
|
|
1693
|
-
re: /^\s*informational:\s*true\b/,
|
|
1694
|
-
message: "informational: true added \u2014 the codecov status can no longer fail.",
|
|
1695
|
-
remediation: "Remove informational; a status that cannot fail is not a gate."
|
|
1696
1921
|
}
|
|
1697
1922
|
];
|
|
1923
|
+
function dirHasSpec(configPath, policy, ctx) {
|
|
1924
|
+
const files = trackedFiles(ctx);
|
|
1925
|
+
if (!files) return null;
|
|
1926
|
+
const root = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/") + 1) : "";
|
|
1927
|
+
return files.some((f) => f.startsWith(root) && isProtected(f, policy, "tests"));
|
|
1928
|
+
}
|
|
1698
1929
|
var isJsConfig = (path) => /\.(?:[cm]?[jt]sx?|json)$/.test(path);
|
|
1699
1930
|
function gateMovedElsewhere(c, changes, policy) {
|
|
1700
1931
|
const before = c.before ?? "";
|
|
@@ -1787,6 +2018,7 @@ var coverageLowering = {
|
|
|
1787
2018
|
for (const s of SWITCHES) {
|
|
1788
2019
|
if (!s.re.test(l.content)) continue;
|
|
1789
2020
|
if (removed.some((r) => s.re.test(r.content))) continue;
|
|
2021
|
+
if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
|
|
1790
2022
|
out2.push(
|
|
1791
2023
|
makeFinding(RULE4, policy, {
|
|
1792
2024
|
file: c.path,
|
|
@@ -1838,7 +2070,7 @@ var RULE5 = "ci-tampering";
|
|
|
1838
2070
|
var USES = /^\s*-\s*uses:/;
|
|
1839
2071
|
var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
1840
2072
|
var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
1841
|
-
var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward)\b/;
|
|
2073
|
+
var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward|mocha|ava|oxlint|tsgo|mypy|golangci-lint|node\s+--test|biome\s+(?:ci|check|lint)|deno\s+(?:test|lint|check)|ruff\s+check)\b/;
|
|
1842
2074
|
var INVOKES_SCRIPT = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|tests|lint|typecheck|type-check|coverage)\b|\b(?:make|cargo|go)\s+test\b|\bgradle\w*\s+(?:test|check)\b/;
|
|
1843
2075
|
function isActiveWorkflow(path) {
|
|
1844
2076
|
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
@@ -1847,8 +2079,19 @@ var invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line
|
|
|
1847
2079
|
function commandCore(line) {
|
|
1848
2080
|
return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
|
|
1849
2081
|
}
|
|
2082
|
+
function foldExpressions(s) {
|
|
2083
|
+
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
2084
|
+
const v = foldConst(e.trim());
|
|
2085
|
+
if (v !== void 0 && v !== TRUTHY) return String(v);
|
|
2086
|
+
return /\b(?:matrix|strategy|inputs)\./.test(e) ? MATRIX_TOKEN : EXPR_TOKEN;
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
var EXPR_TOKEN = "__expr__";
|
|
2090
|
+
var MATRIX_TOKEN = "__matrix__";
|
|
2091
|
+
var MATRIX_FLAG = new RegExp(`(?:^|\\s)-{1,2}[\\w-]+(?:=|\\s+)${MATRIX_TOKEN}(?:/\\S*)?(?=\\s|$)`, "g");
|
|
1850
2092
|
function canonical(core) {
|
|
1851
|
-
let s = core.replace(/^(['"])(.*)\1$/, "$2").trim();
|
|
2093
|
+
let s = foldExpressions(core.replace(/^(['"])(.*)\1$/, "$2").trim()).replace(MATRIX_FLAG, " ").replace(/\s+/g, " ").trim();
|
|
2094
|
+
s = s.replace(/^(?:cd|pushd)\s+\S+\s*&&\s*/, "");
|
|
1852
2095
|
s = s.replace(/^(?:npx|pnpm\s+(?:exec|dlx)|yarn\s+(?:exec|dlx)|bunx|bun\s+x)\s+/, "exec ");
|
|
1853
2096
|
s = s.replace(/^(?:npm|pnpm|yarn|bun)\s+(?:run(?:-script)?\s+)?/, "exec ");
|
|
1854
2097
|
s = s.replace(/^exec\s+(?:t|tst)(?=\s|$)/, "exec test");
|
|
@@ -1878,8 +2121,8 @@ function checkKind(core) {
|
|
|
1878
2121
|
if (!invokesCheck(core)) return null;
|
|
1879
2122
|
const s = canonical(core);
|
|
1880
2123
|
if (/\btamperward\b/.test(s)) return "gate";
|
|
1881
|
-
if (/\b(?:lint|eslint)\b/i.test(s)) return "lint";
|
|
1882
|
-
if (/\b(?:typecheck|type-check|tsc)\b/i.test(s)) return "types";
|
|
2124
|
+
if (/\b(?:lint|eslint|biome|oxlint|golangci-lint|ruff)\b/i.test(s)) return "lint";
|
|
2125
|
+
if (/\b(?:typecheck|type-check|tsc|tsgo|mypy)\b|\bdeno\s+check\b/i.test(s)) return "types";
|
|
1883
2126
|
return "test";
|
|
1884
2127
|
}
|
|
1885
2128
|
function survives(removedCore, isUses, afterCores, addedCores) {
|
|
@@ -1888,13 +2131,15 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1888
2131
|
return afterCores.some((a) => usesRef(a) === ref) ? { state: "kept" } : { state: "gone" };
|
|
1889
2132
|
}
|
|
1890
2133
|
const r = canonical(removedCore);
|
|
2134
|
+
const kind = checkKind(removedCore);
|
|
2135
|
+
const narrowed = (args) => kind === "test" && narrowedByPositional(args);
|
|
1891
2136
|
let neutralised;
|
|
1892
2137
|
for (const raw of afterCores) {
|
|
1893
2138
|
const a = canonical(raw);
|
|
1894
2139
|
if (a === r) return { state: "kept" };
|
|
1895
2140
|
if (a.startsWith(r + " ")) {
|
|
1896
2141
|
const rest = a.slice(r.length);
|
|
1897
|
-
if (NEUTRALISING_SUFFIX.test(rest) ||
|
|
2142
|
+
if (NEUTRALISING_SUFFIX.test(rest) || narrowed(rest)) neutralised ??= raw;
|
|
1898
2143
|
else return { state: "kept" };
|
|
1899
2144
|
} else if (r.startsWith(a + " ") && invokesCheck(a)) {
|
|
1900
2145
|
return { state: "kept" };
|
|
@@ -1903,7 +2148,6 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1903
2148
|
if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
|
|
1904
2149
|
}
|
|
1905
2150
|
}
|
|
1906
|
-
const kind = checkKind(removedCore);
|
|
1907
2151
|
let respelled = false;
|
|
1908
2152
|
for (const raw of addedCores) {
|
|
1909
2153
|
if (!kind || checkKind(raw) !== kind) continue;
|
|
@@ -1913,7 +2157,7 @@ function survives(removedCore, isUses, afterCores, addedCores) {
|
|
|
1913
2157
|
continue;
|
|
1914
2158
|
}
|
|
1915
2159
|
const args = a.slice(a.indexOf(" ") + 1 || a.length);
|
|
1916
|
-
if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) ||
|
|
2160
|
+
if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args))) neutralised ??= raw;
|
|
1917
2161
|
else respelled = true;
|
|
1918
2162
|
}
|
|
1919
2163
|
if (respelled) return { state: "kept" };
|
|
@@ -2085,6 +2329,7 @@ function isTruthy(raw) {
|
|
|
2085
2329
|
const f = foldConst(m[1]);
|
|
2086
2330
|
return f !== void 0 && truthy(f);
|
|
2087
2331
|
}
|
|
2332
|
+
var SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
|
|
2088
2333
|
var indentOf = (l) => l.match(/^\s*/)[0].length;
|
|
2089
2334
|
var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
|
|
2090
2335
|
function stepOf(lines, i) {
|
|
@@ -2110,8 +2355,10 @@ function jobOf(lines, i) {
|
|
|
2110
2355
|
while (e < lines.length && (lines[e].trim() === "" || indentOf(lines[e]) > hi)) e++;
|
|
2111
2356
|
return [h, e];
|
|
2112
2357
|
}
|
|
2113
|
-
var
|
|
2114
|
-
var
|
|
2358
|
+
var REPORTER_ACTION = /^\s*-?\s*uses:\s*\S*?(?:[\w-]*-reporter|[\w-]*-results?[\w-]*|upload-[\w-]*|codecov[\w-]*|coveralls[\w-]*)(?:@|\s|$)/i;
|
|
2359
|
+
var usesCheck = (l) => USES.test(l) && CHECK.test(l) && !REPORTER_ACTION.test(l);
|
|
2360
|
+
var stepHasCheck = (lines, range) => range === null || lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l));
|
|
2361
|
+
var jobHasCheck = (lines, range) => lines.slice(range[0], range[1]).some((l) => invokesCheck(l) || usesCheck(l) || /^\s*uses:\s*\S/.test(l));
|
|
2115
2362
|
function runBlocks(lines) {
|
|
2116
2363
|
const out2 = [];
|
|
2117
2364
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2256,7 +2503,8 @@ function triggerNarrowings(before, after, opts) {
|
|
|
2256
2503
|
const now = after.branches.get(e);
|
|
2257
2504
|
if (!now || !after.events.has(e)) continue;
|
|
2258
2505
|
if (now.some(ALL)) continue;
|
|
2259
|
-
const
|
|
2506
|
+
const renamed = (b) => DEFAULT_BRANCH.test(b) && opts.hasBranch(b) !== false;
|
|
2507
|
+
const lostDefault = namesDefault(was) && !namesDefault(now) && !now.some(renamed);
|
|
2260
2508
|
const replaced = !namesDefault(was) && !namesDefault(now) && was.length > 0 && !was.some((b) => now.includes(b));
|
|
2261
2509
|
if (lostDefault || replaced) out2.push(`on.${e}.branches no longer names ${was.filter((b) => !now.includes(b)).join(", ")} (now [${now.join(", ")}])`);
|
|
2262
2510
|
}
|
|
@@ -2342,6 +2590,7 @@ var ciTampering = {
|
|
|
2342
2590
|
}
|
|
2343
2591
|
for (const l of addedLines(c)) {
|
|
2344
2592
|
if (!/^\s*-?\s*shell:\s*.*\{0\}/.test(l.content) || l.newLine == null) continue;
|
|
2593
|
+
if (SHELL_ERREXIT.test(uncommented(l.content.replace(/^\s*-?\s*shell:\s*/, "")))) continue;
|
|
2345
2594
|
const step = stepOf(afterLines, l.newLine - 1);
|
|
2346
2595
|
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
2347
2596
|
out2.push(
|
|
@@ -2355,7 +2604,7 @@ var ciTampering = {
|
|
|
2355
2604
|
);
|
|
2356
2605
|
}
|
|
2357
2606
|
if (c.before != null) {
|
|
2358
|
-
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx) };
|
|
2607
|
+
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
2359
2608
|
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
2360
2609
|
out2.push(
|
|
2361
2610
|
makeFinding(RULE5, policy, {
|
|
@@ -2380,7 +2629,7 @@ var ciTampering = {
|
|
|
2380
2629
|
}
|
|
2381
2630
|
for (const l of removedLines(c)) {
|
|
2382
2631
|
if (/^\s*#/.test(l.content)) continue;
|
|
2383
|
-
const isUsesLine =
|
|
2632
|
+
const isUsesLine = usesCheck(l.content);
|
|
2384
2633
|
const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
|
|
2385
2634
|
if (!isUsesLine && !isCheckCommand) continue;
|
|
2386
2635
|
const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
|
|
@@ -2411,11 +2660,11 @@ var ciTampering = {
|
|
|
2411
2660
|
};
|
|
2412
2661
|
|
|
2413
2662
|
// src/detectors/hook-tampering.ts
|
|
2414
|
-
import { existsSync, readFileSync as
|
|
2663
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
2415
2664
|
import { isAbsolute, join as join3, relative } from "node:path";
|
|
2416
2665
|
|
|
2417
2666
|
// src/wiring.ts
|
|
2418
|
-
import { readFileSync
|
|
2667
|
+
import { readFileSync } from "node:fs";
|
|
2419
2668
|
import { dirname, join as join2 } from "node:path";
|
|
2420
2669
|
import { fileURLToPath } from "node:url";
|
|
2421
2670
|
function shippedVersion() {
|
|
@@ -2423,7 +2672,7 @@ function shippedVersion() {
|
|
|
2423
2672
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2424
2673
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
2425
2674
|
try {
|
|
2426
|
-
const pkg = JSON.parse(
|
|
2675
|
+
const pkg = JSON.parse(readFileSync(join2(here, rel), "utf8"));
|
|
2427
2676
|
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
2428
2677
|
} catch {
|
|
2429
2678
|
}
|
|
@@ -4072,7 +4321,7 @@ function lefthookBase(c, changes, ctx) {
|
|
|
4072
4321
|
for (const n of names) {
|
|
4073
4322
|
try {
|
|
4074
4323
|
const p = join3(ctx.cwd, n);
|
|
4075
|
-
if (existsSync(p)) return parseDoc(
|
|
4324
|
+
if (existsSync(p)) return parseDoc(readFileSync2(p, "utf8"));
|
|
4076
4325
|
} catch {
|
|
4077
4326
|
}
|
|
4078
4327
|
}
|
|
@@ -4442,21 +4691,20 @@ function collect(root, runner) {
|
|
|
4442
4691
|
const entries = take(key2, e).map((pattern) => ({ pattern, key: key2 }));
|
|
4443
4692
|
sel.ignore = [...sel.ignore ?? [], ...entries];
|
|
4444
4693
|
};
|
|
4694
|
+
let projectList = null;
|
|
4445
4695
|
const projects = (e) => {
|
|
4446
4696
|
sel.present = true;
|
|
4447
|
-
|
|
4697
|
+
projectList = e;
|
|
4698
|
+
};
|
|
4699
|
+
const visit = (node) => {
|
|
4700
|
+
if (ts3.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
4701
|
+
sel.present = true;
|
|
4448
4702
|
sel.opaque = true;
|
|
4449
|
-
return;
|
|
4450
4703
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
else sel.opaque = true;
|
|
4704
|
+
if (ts3.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts3.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
4705
|
+
sel.present = true;
|
|
4706
|
+
sel.opaque = true;
|
|
4455
4707
|
}
|
|
4456
|
-
sel.projects = list2;
|
|
4457
|
-
if (list2.some((p) => p.opaque)) sel.opaque = true;
|
|
4458
|
-
};
|
|
4459
|
-
const visit = (node) => {
|
|
4460
4708
|
if (ts3.isPropertyAssignment(node)) {
|
|
4461
4709
|
const k = keyName2(node.name);
|
|
4462
4710
|
if (runner === "jest") {
|
|
@@ -4476,7 +4724,7 @@ function collect(root, runner) {
|
|
|
4476
4724
|
} else if (ownerKey2(node) === "test") {
|
|
4477
4725
|
if (k === "include") sel.include = take(k, node.initializer);
|
|
4478
4726
|
else if (k === "exclude") ignores(k, node.initializer);
|
|
4479
|
-
else if (k === "dir") {
|
|
4727
|
+
else if (k === "dir" || k === "root") {
|
|
4480
4728
|
const { dir, opaque } = dirValue(node.initializer);
|
|
4481
4729
|
sel.present = true;
|
|
4482
4730
|
if (opaque) sel.opaque = true;
|
|
@@ -4490,8 +4738,68 @@ function collect(root, runner) {
|
|
|
4490
4738
|
ts3.forEachChild(node, visit);
|
|
4491
4739
|
};
|
|
4492
4740
|
visit(root);
|
|
4741
|
+
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
4742
|
+
if (sel.projects?.some((p) => p.opaque)) sel.opaque = true;
|
|
4493
4743
|
return sel;
|
|
4494
4744
|
}
|
|
4745
|
+
function unwrapConfigCall(e) {
|
|
4746
|
+
let x = e;
|
|
4747
|
+
while (ts3.isParenthesizedExpression(x) || ts3.isAsExpression(x) || x.kind === ts3.SyntaxKind.SatisfiesExpression) {
|
|
4748
|
+
x = x.expression;
|
|
4749
|
+
}
|
|
4750
|
+
if (ts3.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
4751
|
+
return x;
|
|
4752
|
+
}
|
|
4753
|
+
var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
4754
|
+
function spreadsIntoConfig(node, runner) {
|
|
4755
|
+
const obj = node.parent;
|
|
4756
|
+
if (!ts3.isObjectLiteralExpression(obj)) return false;
|
|
4757
|
+
const keys = obj.properties.map((p) => ts3.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
4758
|
+
if (runner === "vitest") {
|
|
4759
|
+
if (keys.includes("test")) return true;
|
|
4760
|
+
let q = obj.parent;
|
|
4761
|
+
while (q && (ts3.isParenthesizedExpression(q) || ts3.isAsExpression(q) || q.kind === ts3.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
4762
|
+
return !!q && ts3.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
4763
|
+
}
|
|
4764
|
+
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
4765
|
+
}
|
|
4766
|
+
function extendsOf(el) {
|
|
4767
|
+
for (const p of el.properties) {
|
|
4768
|
+
if (!ts3.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
4769
|
+
if (p.initializer.kind === ts3.SyntaxKind.TrueKeyword) return true;
|
|
4770
|
+
if (p.initializer.kind === ts3.SyntaxKind.FalseKeyword) return null;
|
|
4771
|
+
return "path";
|
|
4772
|
+
}
|
|
4773
|
+
return null;
|
|
4774
|
+
}
|
|
4775
|
+
function readProject(el, root, runner) {
|
|
4776
|
+
const p = collect(el, runner);
|
|
4777
|
+
const ext = extendsOf(el);
|
|
4778
|
+
if (ext === "path") p.opaque = true;
|
|
4779
|
+
else if (ext === true && root) {
|
|
4780
|
+
p.include ??= root.include;
|
|
4781
|
+
p.regex ??= root.regex;
|
|
4782
|
+
p.ignore ??= root.ignore;
|
|
4783
|
+
p.roots ??= root.roots;
|
|
4784
|
+
p.base ??= root.base;
|
|
4785
|
+
if (root.opaque) p.opaque = true;
|
|
4786
|
+
}
|
|
4787
|
+
p.present = true;
|
|
4788
|
+
return p;
|
|
4789
|
+
}
|
|
4790
|
+
function readProjects(e, root, runner) {
|
|
4791
|
+
if (!ts3.isArrayLiteralExpression(e)) {
|
|
4792
|
+
root.opaque = true;
|
|
4793
|
+
return [];
|
|
4794
|
+
}
|
|
4795
|
+
const list2 = [];
|
|
4796
|
+
for (const el of e.elements) {
|
|
4797
|
+
const lit = unwrapConfigCall(el);
|
|
4798
|
+
if (ts3.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
|
|
4799
|
+
else root.opaque = true;
|
|
4800
|
+
}
|
|
4801
|
+
return list2;
|
|
4802
|
+
}
|
|
4495
4803
|
function workspaceArray(sf) {
|
|
4496
4804
|
for (const st of sf.statements) {
|
|
4497
4805
|
let e;
|
|
@@ -4513,14 +4821,9 @@ function parseSelection(src, runner, path = "") {
|
|
|
4513
4821
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
4514
4822
|
const arr = workspaceArray(sf);
|
|
4515
4823
|
if (arr) {
|
|
4516
|
-
|
|
4517
|
-
for (const el of arr.elements) {
|
|
4518
|
-
if (ts3.isObjectLiteralExpression(el)) list2.push(collect(el, runner));
|
|
4519
|
-
else sel.opaque = true;
|
|
4520
|
-
}
|
|
4521
|
-
sel.projects = list2;
|
|
4824
|
+
sel.projects = readProjects(arr, sel, runner);
|
|
4522
4825
|
sel.present = true;
|
|
4523
|
-
if (
|
|
4826
|
+
if (sel.projects.some((p) => p.opaque)) sel.opaque = true;
|
|
4524
4827
|
} else if (src.trim()) sel.opaque = true;
|
|
4525
4828
|
} else sel = collect(sf, runner);
|
|
4526
4829
|
} catch {
|
|
@@ -4580,8 +4883,8 @@ function predicate(sel, runner) {
|
|
|
4580
4883
|
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
4581
4884
|
const hit = ignoredBy(r);
|
|
4582
4885
|
if (hit) return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
4583
|
-
const key2 = sel.include ?
|
|
4584
|
-
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? [])})`;
|
|
4886
|
+
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
4887
|
+
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
4585
4888
|
}
|
|
4586
4889
|
};
|
|
4587
4890
|
}
|
|
@@ -4620,8 +4923,21 @@ function calleeName(expr) {
|
|
|
4620
4923
|
}
|
|
4621
4924
|
return null;
|
|
4622
4925
|
}
|
|
4623
|
-
var
|
|
4624
|
-
var
|
|
4926
|
+
var TABLE_METHOD = /^(?:each|for)$/;
|
|
4927
|
+
var isEachOf = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
|
|
4928
|
+
var isDescribeEach = (expr) => ts4.isPropertyAccessExpression(expr) && ts4.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
|
|
4929
|
+
function loopRows(iterable) {
|
|
4930
|
+
if (ts4.isArrayLiteralExpression(iterable)) {
|
|
4931
|
+
let n = 0;
|
|
4932
|
+
let open = false;
|
|
4933
|
+
for (const el of iterable.elements) {
|
|
4934
|
+
if (ts4.isSpreadElement(el)) open = true;
|
|
4935
|
+
else n++;
|
|
4936
|
+
}
|
|
4937
|
+
return { n: Math.max(n, open ? 0 : 1), open };
|
|
4938
|
+
}
|
|
4939
|
+
return { n: 1, open: true };
|
|
4940
|
+
}
|
|
4625
4941
|
function templateRows(node) {
|
|
4626
4942
|
const rows = node.template.getText().split("\n").filter((l) => /\S/.test(l.replace(/[`]/g, ""))).length - 1;
|
|
4627
4943
|
return Math.max(rows, 1);
|
|
@@ -4659,7 +4975,23 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
4659
4975
|
let n = 0;
|
|
4660
4976
|
let open = false;
|
|
4661
4977
|
const visit = (node, mult) => {
|
|
4978
|
+
if (ts4.isForOfStatement(node)) {
|
|
4979
|
+
const r = loopRows(node.expression);
|
|
4980
|
+
if (r.open) open = true;
|
|
4981
|
+
visit(node.statement, mult * r.n);
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
if (ts4.isForStatement(node) || ts4.isForInStatement(node) || ts4.isWhileStatement(node) || ts4.isDoStatement(node)) {
|
|
4985
|
+
open = true;
|
|
4986
|
+
}
|
|
4662
4987
|
if (ts4.isCallExpression(node)) {
|
|
4988
|
+
const callee = node.expression;
|
|
4989
|
+
if (ts4.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
|
|
4990
|
+
const r = loopRows(callee.expression);
|
|
4991
|
+
if (r.open) open = true;
|
|
4992
|
+
for (const a of node.arguments) visit(a, mult * r.n);
|
|
4993
|
+
return;
|
|
4994
|
+
}
|
|
4663
4995
|
const head = node.expression;
|
|
4664
4996
|
if (ts4.isCallExpression(head) && isDescribeEach(head.expression)) {
|
|
4665
4997
|
const r = eachRows(head);
|
|
@@ -4707,6 +5039,12 @@ function runnerSamples(configPath, policy, ctx) {
|
|
|
4707
5039
|
const own = files.filter((f) => f.startsWith(root) && langOf(f) === "js" && isProtected(f, policy, "tests") && !OTHER_RUNNER_DIR.test(f)).map((f) => f.slice(root.length));
|
|
4708
5040
|
return own.length ? own : CANONICAL_SAMPLES;
|
|
4709
5041
|
}
|
|
5042
|
+
var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
|
|
5043
|
+
function excludesOtherRunner(flag, cmd) {
|
|
5044
|
+
if (flag !== "--exclude") return false;
|
|
5045
|
+
const values = [...cmd.matchAll(/--exclude(?:=|\s+)(['"]?)(\S+?)\1(?=\s|$)/g)].map((m) => m[2]);
|
|
5046
|
+
return values.length > 0 && values.every((v) => OTHER_RUNNER_DIR.test(v.replace(/^\.\//, "").replace(/(?:\/\*+)*\/?$/, "") + "/"));
|
|
5047
|
+
}
|
|
4710
5048
|
function scriptNarrowings(before, after, path) {
|
|
4711
5049
|
if ((path.split("/").pop() ?? path) !== "package.json") return [];
|
|
4712
5050
|
const scriptsOf = (src) => {
|
|
@@ -4721,7 +5059,7 @@ function scriptNarrowings(before, after, path) {
|
|
|
4721
5059
|
const now = scriptsOf(after);
|
|
4722
5060
|
const out2 = [];
|
|
4723
5061
|
for (const [name, cmd] of Object.entries(now)) {
|
|
4724
|
-
if (
|
|
5062
|
+
if (!WHOLE_SUITE_SCRIPT.test(name) || !/\b(?:jest|vitest)\b/.test(cmd)) continue;
|
|
4725
5063
|
const prev = was[name] ?? "";
|
|
4726
5064
|
const flags = new RegExp(SUITE_NARROWING_FLAGS.source, "g");
|
|
4727
5065
|
for (const m of cmd.matchAll(flags)) {
|
|
@@ -4729,6 +5067,7 @@ function scriptNarrowings(before, after, path) {
|
|
|
4729
5067
|
if (!flag) continue;
|
|
4730
5068
|
const one = new RegExp(SUITE_NARROWING_FLAGS.source);
|
|
4731
5069
|
if (one.test(prev) && prev.includes(flag)) continue;
|
|
5070
|
+
if (excludesOtherRunner(flag, cmd)) continue;
|
|
4732
5071
|
out2.push({ script: name, flag, before: prev, after: cmd });
|
|
4733
5072
|
break;
|
|
4734
5073
|
}
|
|
@@ -4760,10 +5099,11 @@ var testDeletion = {
|
|
|
4760
5099
|
certainty: "mechanical",
|
|
4761
5100
|
run(changes, policy, _view, ctx) {
|
|
4762
5101
|
const out2 = [];
|
|
5102
|
+
const isSpec = (p) => isProtected(p, policy, "tests") && !isProtected(p, policy, "snapshots");
|
|
4763
5103
|
const addedTestLines = /* @__PURE__ */ new Set();
|
|
4764
5104
|
let addedTestBlocks = 0;
|
|
4765
5105
|
for (const c of changes) {
|
|
4766
|
-
if (c.kind !== "file" || c.after == null || !
|
|
5106
|
+
if (c.kind !== "file" || c.after == null || !isSpec(c.path)) continue;
|
|
4767
5107
|
if (c.op === "add") {
|
|
4768
5108
|
for (const l of significantLines(c.after, c.path)) addedTestLines.add(l);
|
|
4769
5109
|
addedTestBlocks += countTests(c.after, c.path, true).min;
|
|
@@ -4781,7 +5121,7 @@ var testDeletion = {
|
|
|
4781
5121
|
};
|
|
4782
5122
|
for (const c of changes) {
|
|
4783
5123
|
if (c.kind === "file") {
|
|
4784
|
-
const isTest =
|
|
5124
|
+
const isTest = isSpec(c.path);
|
|
4785
5125
|
if (c.op === "delete" && isTest) {
|
|
4786
5126
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
4787
5127
|
out2.push(
|
|
@@ -4792,7 +5132,7 @@ var testDeletion = {
|
|
|
4792
5132
|
remediation: "Fix the code under test, not the suite. Deleting a failing test to pass is the move this blocks."
|
|
4793
5133
|
})
|
|
4794
5134
|
);
|
|
4795
|
-
} else if (c.op === "rename" && c.oldPath &&
|
|
5135
|
+
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
|
|
4796
5136
|
out2.push(
|
|
4797
5137
|
makeFinding(RULE7, policy, {
|
|
4798
5138
|
file: c.path,
|
|
@@ -4853,22 +5193,22 @@ var testDeletion = {
|
|
|
4853
5193
|
const testToks = toks.filter((t) => {
|
|
4854
5194
|
const resolved = root !== null && /(?:^|\/)\.{1,2}(?:\/|$)/.test(t) ? joinRoot(root, t) : null;
|
|
4855
5195
|
const probe = resolved ?? t;
|
|
4856
|
-
if (!
|
|
5196
|
+
if (!isSpec(probe)) return false;
|
|
4857
5197
|
if (!listing || root === null) return true;
|
|
4858
5198
|
const looksLikeFile = /\.[A-Za-z0-9]+$/.test(probe.split("/").pop() ?? "");
|
|
4859
5199
|
if (looksLikeFile || listing.includes(resolved ?? inRepo(t))) return true;
|
|
4860
|
-
return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx);
|
|
5200
|
+
return containsProtected(resolved ?? inRepo(t), policy, "tests", ctx, "snapshots");
|
|
4861
5201
|
});
|
|
4862
|
-
const rootHasProtected = () => listing ? listing.some(
|
|
5202
|
+
const rootHasProtected = () => listing ? listing.some(isSpec) : true;
|
|
4863
5203
|
const dirToks = toks.filter((t) => {
|
|
4864
5204
|
if (t.startsWith("-") || testToks.includes(t)) return false;
|
|
4865
5205
|
const whole = /^(?:\.|\.\/|\*|\.\/\*)$/.test(t);
|
|
4866
5206
|
if (!whole && /[*?{}[\]]/.test(t)) return false;
|
|
4867
|
-
if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx);
|
|
5207
|
+
if (root === null) return whole ? false : containsProtected(t, policy, "tests", ctx, "snapshots");
|
|
4868
5208
|
const dir = whole ? root : joinRoot(root, t);
|
|
4869
5209
|
if (dir === null) return false;
|
|
4870
5210
|
if (dir === "") return rootHasProtected();
|
|
4871
|
-
return containsProtected(dir, policy, "tests", ctx);
|
|
5211
|
+
return containsProtected(dir, policy, "tests", ctx, "snapshots");
|
|
4872
5212
|
});
|
|
4873
5213
|
if (testToks.length === 0 && dirToks.length === 0) continue;
|
|
4874
5214
|
const named = [...testToks, ...dirToks];
|
|
@@ -4876,13 +5216,13 @@ var testDeletion = {
|
|
|
4876
5216
|
let why = null;
|
|
4877
5217
|
let evidence = seg;
|
|
4878
5218
|
const redirectTarget = seg.match(/>\s*(\S+)/)?.[1];
|
|
4879
|
-
const redirectsOntoTest = redirectTarget ?
|
|
5219
|
+
const redirectsOntoTest = redirectTarget ? isSpec(unquote(redirectTarget)) : false;
|
|
4880
5220
|
const findDeletes = cmd === "find" && /\s(?:-delete\b|-exec\s+(?:rm|unlink)\b|-execdir\s+(?:rm|unlink)\b)/.test(seg);
|
|
4881
5221
|
const gitRestoresOld = cmd === "git" && /^git\s+(?:checkout|restore)\b/.test(seg) && (() => {
|
|
4882
5222
|
const src = seg.match(/--source[= ]\s*(\S+)/)?.[1];
|
|
4883
5223
|
const revIdx = toks.findIndex((t, i) => i >= 2 && !t.startsWith("-") && !named.includes(t) && t !== "--");
|
|
4884
5224
|
const rev = src ?? (revIdx >= 0 ? toks[revIdx] : void 0);
|
|
4885
|
-
if (!rev || rev === "HEAD") return false;
|
|
5225
|
+
if (!rev || rev === "HEAD" || rev === "@" || revIsHead(rev, ctx) === true) return false;
|
|
4886
5226
|
if (src || seg.includes(" -- ")) return true;
|
|
4887
5227
|
return revIdx >= 0 && toks.some((t, i) => i > revIdx && named.includes(t));
|
|
4888
5228
|
})();
|
|
@@ -4898,7 +5238,7 @@ var testDeletion = {
|
|
|
4898
5238
|
why = "truncate empties a test file";
|
|
4899
5239
|
} else if (redirectsOntoTest) {
|
|
4900
5240
|
why = "a redirect overwrites/empties a test file";
|
|
4901
|
-
} else if (cmd === "cp" && testToks.length > 0 &&
|
|
5241
|
+
} else if (cmd === "cp" && testToks.length > 0 && isSpec(toks[toks.length - 1] ?? "") && !isSpec(toks[toks.length - 2] ?? "")) {
|
|
4902
5242
|
why = "cp overwrites a test file with something else";
|
|
4903
5243
|
} else if (cmd === "tee" && !toks.includes("-a") && !toks.includes("--append")) {
|
|
4904
5244
|
why = "tee overwrites a test file";
|
|
@@ -4908,7 +5248,7 @@ var testDeletion = {
|
|
|
4908
5248
|
why = "git checkout <rev> replaces a test file with an older version";
|
|
4909
5249
|
} else if (/\bmv\b/.test(seg)) {
|
|
4910
5250
|
const dest = toks[toks.length - 1];
|
|
4911
|
-
if (dest && testToks.length > 0 && !
|
|
5251
|
+
if (dest && testToks.length > 0 && !isSpec(dest)) {
|
|
4912
5252
|
why = "mv renames a test file out of the test glob";
|
|
4913
5253
|
evidence = `${testToks[0]} \u2192 ${dest}`;
|
|
4914
5254
|
} else if (dest && testToks.length === 0 && dirToks[0] !== dest && leavesGlob(dirToks[0] ?? "", dest, policy, ctx)) {
|
|
@@ -4945,7 +5285,7 @@ function eachTables(src, path) {
|
|
|
4945
5285
|
try {
|
|
4946
5286
|
const sf = ts5.createSourceFile("spec.ts", src, ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
4947
5287
|
const visit = (node) => {
|
|
4948
|
-
if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text
|
|
5288
|
+
if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts5.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
|
|
4949
5289
|
const arg = node.arguments[0];
|
|
4950
5290
|
if (arg && ts5.isArrayLiteralExpression(arg)) {
|
|
4951
5291
|
for (const el of arg.elements) {
|
|
@@ -4966,6 +5306,7 @@ function eachTables(src, path) {
|
|
|
4966
5306
|
}
|
|
4967
5307
|
return { rows, open };
|
|
4968
5308
|
}
|
|
5309
|
+
var TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
|
|
4969
5310
|
function significantLinesOrdered(src, path) {
|
|
4970
5311
|
const out2 = [];
|
|
4971
5312
|
const lang = langOf(path);
|
|
@@ -4986,11 +5327,14 @@ var testContentRemoval = {
|
|
|
4986
5327
|
for (const c of changes) {
|
|
4987
5328
|
if (c.kind !== "file" || c.after == null) continue;
|
|
4988
5329
|
if (isProtected(c.path, policy, "tests")) keptPool += ws(significantLinesOrdered(c.after, c.path).join("\n"));
|
|
4989
|
-
|
|
5330
|
+
let addedHere = "";
|
|
5331
|
+
if (c.op === "add") addedHere = ws(c.after);
|
|
4990
5332
|
else if (c.before != null) {
|
|
4991
5333
|
const had = new Set(c.before.split("\n").map((l) => l.trim()));
|
|
4992
|
-
|
|
5334
|
+
addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
|
|
4993
5335
|
}
|
|
5336
|
+
addedPool += addedHere;
|
|
5337
|
+
if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool += addedHere;
|
|
4994
5338
|
}
|
|
4995
5339
|
for (const c of changes) {
|
|
4996
5340
|
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
|
|
@@ -5212,7 +5556,7 @@ function hasBlocking(findings) {
|
|
|
5212
5556
|
}
|
|
5213
5557
|
|
|
5214
5558
|
// src/policy-load.ts
|
|
5215
|
-
import { readFileSync as
|
|
5559
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
|
|
5216
5560
|
import { isAbsolute as isAbsolute2, join as join4, posix } from "node:path";
|
|
5217
5561
|
import { parse as parse2 } from "yaml";
|
|
5218
5562
|
var PolicyError = class extends Error {
|
|
@@ -5339,7 +5683,7 @@ function parseOrThrow(src, where) {
|
|
|
5339
5683
|
function loadPolicy(cwd = process.cwd()) {
|
|
5340
5684
|
const path = join4(cwd, POLICY_FILE);
|
|
5341
5685
|
if (!existsSync2(path)) return defaultPolicy();
|
|
5342
|
-
return parseOrThrow(
|
|
5686
|
+
return parseOrThrow(readFileSync3(path, "utf8"), POLICY_FILE);
|
|
5343
5687
|
}
|
|
5344
5688
|
function loadPolicyAt(rev, cwd) {
|
|
5345
5689
|
const src = fileAt(rev, POLICY_FILE, { cwd });
|
|
@@ -5349,7 +5693,7 @@ function loadPolicyAt(rev, cwd) {
|
|
|
5349
5693
|
|
|
5350
5694
|
// src/signoff.ts
|
|
5351
5695
|
import { createHash } from "node:crypto";
|
|
5352
|
-
import { appendFileSync, mkdirSync, readFileSync as
|
|
5696
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
5353
5697
|
import { dirname as dirname2, join as join5 } from "node:path";
|
|
5354
5698
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
5355
5699
|
function fingerprint(rule, file, evidence) {
|
|
@@ -5367,7 +5711,7 @@ function readLedger(cwd, policy) {
|
|
|
5367
5711
|
const p = ledgerPath(cwd, policy);
|
|
5368
5712
|
if (!existsSync3(p)) return [];
|
|
5369
5713
|
const out2 = [];
|
|
5370
|
-
for (const line of
|
|
5714
|
+
for (const line of readFileSync4(p, "utf8").split("\n")) {
|
|
5371
5715
|
if (!line.trim()) continue;
|
|
5372
5716
|
try {
|
|
5373
5717
|
const e = JSON.parse(line);
|
|
@@ -5520,7 +5864,7 @@ function renderText(input, opts) {
|
|
|
5520
5864
|
for (const f of findings) {
|
|
5521
5865
|
const isBlock = f.severity === "block";
|
|
5522
5866
|
const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
|
|
5523
|
-
const loc = f.file ? `${stripControl(f.file)}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
5867
|
+
const loc = f.file ? `${stripControl(escapeControl(f.file))}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
5524
5868
|
out2.push(` ${mark} ${paint(stripControl(f.rule), BOLD, c)} ${paint(loc, DIM, c)}`);
|
|
5525
5869
|
for (const l of wrap(f.message, opts.width - 4)) out2.push(` ${l}`);
|
|
5526
5870
|
const row = (key2, lines) => {
|
|
@@ -5712,6 +6056,7 @@ function check(opts) {
|
|
|
5712
6056
|
}
|
|
5713
6057
|
const ignoredFiles = changes.filter((c) => isSuppressed(c, policy)).length;
|
|
5714
6058
|
let findings = evaluate(changes, policy, void 0, view, { cwd });
|
|
6059
|
+
if (view === "worktree") findings = findings.concat(unjudgeableProtected(cwd, policy, changes));
|
|
5715
6060
|
const { findings: remaining, cleared } = layer === "local" ? applyLocalSignoffs(findings, cwd, policy) : applyOobSignoffs(findings, oobFromEnv(), oobHeadFromEnv());
|
|
5716
6061
|
findings = remaining;
|
|
5717
6062
|
if (cleared.length) {
|
|
@@ -5736,24 +6081,19 @@ function runCheck(opts) {
|
|
|
5736
6081
|
}
|
|
5737
6082
|
|
|
5738
6083
|
// src/cli/hook.ts
|
|
5739
|
-
import { readFileSync as
|
|
6084
|
+
import { readFileSync as readFileSync9, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "node:fs";
|
|
5740
6085
|
import { createHash as createHash4 } from "node:crypto";
|
|
5741
|
-
import { join as join10 } from "node:path";
|
|
5742
6086
|
|
|
5743
6087
|
// src/adapters/claude/changes.ts
|
|
5744
6088
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
5745
|
-
import {
|
|
6089
|
+
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
5746
6090
|
import { tmpdir } from "node:os";
|
|
5747
6091
|
import { isAbsolute as isAbsolute3, join as join6, relative as relative2, resolve } from "node:path";
|
|
5748
6092
|
function asStr(v) {
|
|
5749
6093
|
return typeof v === "string" ? v : "";
|
|
5750
6094
|
}
|
|
5751
6095
|
function readDisk(path) {
|
|
5752
|
-
|
|
5753
|
-
return existsSync4(path) ? readFileSync6(path, "utf8") : null;
|
|
5754
|
-
} catch {
|
|
5755
|
-
return null;
|
|
5756
|
-
}
|
|
6096
|
+
return textOf(inspectResolved(path));
|
|
5757
6097
|
}
|
|
5758
6098
|
function abs(path, cwd) {
|
|
5759
6099
|
return resolve(cwd, path);
|
|
@@ -5847,7 +6187,7 @@ function formatDenial(blocks) {
|
|
|
5847
6187
|
const lead = blocks[0];
|
|
5848
6188
|
const lines = ["Tamperward blocked this change \u2014 it weakens a protected safety net to pass checks.", ""];
|
|
5849
6189
|
for (const f of blocks) {
|
|
5850
|
-
const loc = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
6190
|
+
const loc = f.file ? ` (${escapeControl(f.file)}${f.line ? `:${f.line}` : ""})` : "";
|
|
5851
6191
|
lines.push(` \u2022 ${f.rule}${loc}: ${f.message}`);
|
|
5852
6192
|
}
|
|
5853
6193
|
lines.push("");
|
|
@@ -5862,7 +6202,7 @@ function formatDenial(blocks) {
|
|
|
5862
6202
|
}
|
|
5863
6203
|
|
|
5864
6204
|
// src/session.ts
|
|
5865
|
-
import { existsSync as
|
|
6205
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5866
6206
|
import { dirname as dirname3, join as join7 } from "node:path";
|
|
5867
6207
|
var UNSAFE = /[^A-Za-z0-9_-]/g;
|
|
5868
6208
|
var SHA = /^[0-9a-f]{7,40}$/;
|
|
@@ -5876,8 +6216,8 @@ function turnBaseline(cwd, sessionId) {
|
|
|
5876
6216
|
try {
|
|
5877
6217
|
const p = baselinePath(cwd, sessionId);
|
|
5878
6218
|
if (!p) return null;
|
|
5879
|
-
if (
|
|
5880
|
-
const v =
|
|
6219
|
+
if (existsSync4(p)) {
|
|
6220
|
+
const v = readFileSync5(p, "utf8").trim();
|
|
5881
6221
|
if (SHA.test(v)) return v;
|
|
5882
6222
|
}
|
|
5883
6223
|
const head = headSha(cwd);
|
|
@@ -5903,7 +6243,7 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
5903
6243
|
|
|
5904
6244
|
// src/effect.ts
|
|
5905
6245
|
import { createHash as createHash2 } from "node:crypto";
|
|
5906
|
-
import { existsSync as
|
|
6246
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5907
6247
|
import { isAbsolute as isAbsolute4, join as join8 } from "node:path";
|
|
5908
6248
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5909
6249
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
@@ -5938,8 +6278,22 @@ var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
|
|
|
5938
6278
|
function contentHash(content) {
|
|
5939
6279
|
return sha(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"));
|
|
5940
6280
|
}
|
|
6281
|
+
function entryOf(e) {
|
|
6282
|
+
const hash = e.content != null ? sha(e.content) : e.kind === "unreadable" ? "unreadable" : `${e.kind}:${e.detail}`;
|
|
6283
|
+
return { hash, mode: e.mode, size: e.size, mtimeMs: e.mtimeMs };
|
|
6284
|
+
}
|
|
5941
6285
|
function snapshotProtected(cwd, policy, prev) {
|
|
6286
|
+
void prev;
|
|
5942
6287
|
const out2 = {};
|
|
6288
|
+
const keep = (rel) => isProtected(rel, policy);
|
|
6289
|
+
let pruned = /* @__PURE__ */ new Set();
|
|
6290
|
+
let listed = [];
|
|
6291
|
+
try {
|
|
6292
|
+
const t = ignoredTree({ cwd }, keep);
|
|
6293
|
+
pruned = new Set(t.dirs.map((d) => d.replace(/\/$/, "")));
|
|
6294
|
+
listed = t.files;
|
|
6295
|
+
} catch {
|
|
6296
|
+
}
|
|
5943
6297
|
const walk2 = (rel) => {
|
|
5944
6298
|
const abs2 = rel ? join8(cwd, rel) : cwd;
|
|
5945
6299
|
let names;
|
|
@@ -5951,25 +6305,19 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
5951
6305
|
for (const name of names) {
|
|
5952
6306
|
if (SKIP_DIRS2.has(name)) continue;
|
|
5953
6307
|
const r = rel ? `${rel}/${name}` : name;
|
|
6308
|
+
if (pruned.has(r)) continue;
|
|
5954
6309
|
let st;
|
|
5955
6310
|
try {
|
|
5956
|
-
st =
|
|
6311
|
+
st = lstatSync2(join8(cwd, r));
|
|
5957
6312
|
} catch {
|
|
5958
6313
|
continue;
|
|
5959
6314
|
}
|
|
5960
6315
|
if (st.isDirectory()) walk2(r);
|
|
5961
|
-
else if (
|
|
5962
|
-
let hash = "";
|
|
5963
|
-
try {
|
|
5964
|
-
hash = sha(readFileSync8(join8(cwd, r)));
|
|
5965
|
-
} catch {
|
|
5966
|
-
hash = "unreadable";
|
|
5967
|
-
}
|
|
5968
|
-
out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
|
|
5969
|
-
}
|
|
6316
|
+
else if (keep(r)) out2[r] = entryOf(inspectRel(cwd, r));
|
|
5970
6317
|
}
|
|
5971
6318
|
};
|
|
5972
6319
|
walk2("");
|
|
6320
|
+
for (const rel of listed) if (out2[rel] === void 0) out2[rel] = entryOf(inspectRel(cwd, rel));
|
|
5973
6321
|
return out2;
|
|
5974
6322
|
}
|
|
5975
6323
|
function driftBetween(expected, current) {
|
|
@@ -5987,9 +6335,9 @@ function driftBetween(expected, current) {
|
|
|
5987
6335
|
return { changed, deleted, added };
|
|
5988
6336
|
}
|
|
5989
6337
|
function loadTree(p) {
|
|
5990
|
-
if (!p || !
|
|
6338
|
+
if (!p || !existsSync5(p)) return null;
|
|
5991
6339
|
try {
|
|
5992
|
-
return JSON.parse(
|
|
6340
|
+
return JSON.parse(readFileSync6(p, "utf8"));
|
|
5993
6341
|
} catch {
|
|
5994
6342
|
return null;
|
|
5995
6343
|
}
|
|
@@ -6028,19 +6376,16 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
6028
6376
|
}
|
|
6029
6377
|
|
|
6030
6378
|
// src/cli/watch.ts
|
|
6031
|
-
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2,
|
|
6379
|
+
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
|
|
6032
6380
|
import { join as join9 } from "node:path";
|
|
6033
6381
|
import { createHash as createHash3 } from "node:crypto";
|
|
6034
6382
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
6035
6383
|
function snap(abs2) {
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
} catch {
|
|
6042
|
-
return { mode: null, size: null, hash: null };
|
|
6043
|
-
}
|
|
6384
|
+
const e = inspectPath(abs2);
|
|
6385
|
+
if (e.kind === "absent" || e.kind === "unreadable") return { mode: null, size: null, hash: null };
|
|
6386
|
+
if (e.kind !== "file" || e.content == null) return { mode: e.mode, size: null, hash: null };
|
|
6387
|
+
const hash = createHash3("sha256").update(e.content).digest("hex").slice(0, 16);
|
|
6388
|
+
return { mode: e.mode, size: e.size, hash };
|
|
6044
6389
|
}
|
|
6045
6390
|
function defaultEventLog(cwd) {
|
|
6046
6391
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
@@ -6066,7 +6411,7 @@ function watchTree(dir, cb) {
|
|
|
6066
6411
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
6067
6412
|
cb(kind, child);
|
|
6068
6413
|
try {
|
|
6069
|
-
if (
|
|
6414
|
+
if (statSync(join9(dir, child)).isDirectory()) addDir(child);
|
|
6070
6415
|
} catch {
|
|
6071
6416
|
}
|
|
6072
6417
|
});
|
|
@@ -6087,7 +6432,7 @@ function watchTree(dir, cb) {
|
|
|
6087
6432
|
const child = rel ? `${rel}/${name}` : name;
|
|
6088
6433
|
if (SKIP2.test(child + "/")) continue;
|
|
6089
6434
|
try {
|
|
6090
|
-
if (
|
|
6435
|
+
if (statSync(join9(dir, child)).isDirectory()) walk2(child);
|
|
6091
6436
|
} catch {
|
|
6092
6437
|
}
|
|
6093
6438
|
}
|
|
@@ -6132,17 +6477,17 @@ function runWatch(args) {
|
|
|
6132
6477
|
}
|
|
6133
6478
|
|
|
6134
6479
|
// src/detectors/fs-events.ts
|
|
6135
|
-
import { readFileSync as
|
|
6480
|
+
import { readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
6136
6481
|
var RULE11 = "transient-protected-mutation";
|
|
6137
6482
|
function readEvents(log, offset) {
|
|
6138
6483
|
let size = 0;
|
|
6139
6484
|
try {
|
|
6140
|
-
size =
|
|
6485
|
+
size = statSync2(log).size;
|
|
6141
6486
|
} catch {
|
|
6142
6487
|
return { events: [], newOffset: offset };
|
|
6143
6488
|
}
|
|
6144
6489
|
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
6145
|
-
const raw =
|
|
6490
|
+
const raw = readFileSync8(log, "utf8").slice(offset);
|
|
6146
6491
|
const events = [];
|
|
6147
6492
|
for (const line of raw.split("\n")) {
|
|
6148
6493
|
if (!line.trim()) continue;
|
|
@@ -6195,7 +6540,7 @@ var HookInputError = class extends Error {
|
|
|
6195
6540
|
};
|
|
6196
6541
|
function readStdin() {
|
|
6197
6542
|
try {
|
|
6198
|
-
return
|
|
6543
|
+
return readFileSync9(0, "utf8");
|
|
6199
6544
|
} catch (e) {
|
|
6200
6545
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
6201
6546
|
}
|
|
@@ -6221,7 +6566,7 @@ function recordDenylog(blocks) {
|
|
|
6221
6566
|
} catch {
|
|
6222
6567
|
}
|
|
6223
6568
|
}
|
|
6224
|
-
function
|
|
6569
|
+
function errText2(e) {
|
|
6225
6570
|
const m = e instanceof Error ? e.message : String(e);
|
|
6226
6571
|
return m.length > 200 ? m.slice(0, 200) + "\u2026" : m;
|
|
6227
6572
|
}
|
|
@@ -6253,11 +6598,8 @@ function fileChanges(changes) {
|
|
|
6253
6598
|
return changes.filter((c) => c.kind === "file");
|
|
6254
6599
|
}
|
|
6255
6600
|
function hashOnDisk(cwd, rel) {
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
} catch {
|
|
6259
|
-
return null;
|
|
6260
|
-
}
|
|
6601
|
+
const e = inspectRel(cwd, rel);
|
|
6602
|
+
return e.content == null ? null : contentHash(e.content);
|
|
6261
6603
|
}
|
|
6262
6604
|
function gitMode(mode) {
|
|
6263
6605
|
return (mode & 64) !== 0 ? "100755" : "100644";
|
|
@@ -6304,6 +6646,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
6304
6646
|
const candidates = base ? [base, "HEAD", ""] : ["HEAD", ""];
|
|
6305
6647
|
for (const rel of [...d.changed, ...d.deleted]) {
|
|
6306
6648
|
if (seen.has(rel)) continue;
|
|
6649
|
+
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
6650
|
+
if (unjudgeable) {
|
|
6651
|
+
blocks.push(unjudgeable);
|
|
6652
|
+
continue;
|
|
6653
|
+
}
|
|
6307
6654
|
const after = fileOnDisk(rel, { cwd });
|
|
6308
6655
|
const trustedBlob = fileAt(trusted, rel, { cwd });
|
|
6309
6656
|
const atGitState = after != null && trustedBlob != null && contentHash(after) === contentHash(trustedBlob);
|
|
@@ -6352,6 +6699,11 @@ function uncoveredDrift(cwd, policy, expected, current, covered, base, turnTree)
|
|
|
6352
6699
|
}
|
|
6353
6700
|
for (const rel of d.added) {
|
|
6354
6701
|
if (seen.has(rel)) continue;
|
|
6702
|
+
const unjudgeable = unjudgeableFinding(cwd, rel);
|
|
6703
|
+
if (unjudgeable) {
|
|
6704
|
+
blocks.push(unjudgeable);
|
|
6705
|
+
continue;
|
|
6706
|
+
}
|
|
6355
6707
|
const after = fileOnDisk(rel, { cwd });
|
|
6356
6708
|
if (after == null) {
|
|
6357
6709
|
blocks.push({
|
|
@@ -6391,7 +6743,8 @@ function effectDriftBlocks(cwd, sessionId, policy) {
|
|
|
6391
6743
|
(c) => c.kind === "file" && (drifted.has(c.path) || c.oldPath != null && drifted.has(c.oldPath))
|
|
6392
6744
|
);
|
|
6393
6745
|
const gap = uncoveredDrift(cwd, policy, expected, current, scoped, base, loadTurnTree(cwd, sessionId));
|
|
6394
|
-
const
|
|
6746
|
+
const judged = [...scoped, ...gap.changes];
|
|
6747
|
+
const blocks = evaluate(judged, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(gap.blocks, unjudgeableProtected(cwd, policy, judged));
|
|
6395
6748
|
if (blocks.length > 0) return blocks;
|
|
6396
6749
|
savePtree(cwd, sessionId, current);
|
|
6397
6750
|
return null;
|
|
@@ -6428,7 +6781,7 @@ function preToolUseVerdict(input) {
|
|
|
6428
6781
|
if (blocks.length === 0) sanctionPredictedWrites(cwd, input.session_id, policy, changes);
|
|
6429
6782
|
return verdict(blocks, "PreToolUse");
|
|
6430
6783
|
} catch (e) {
|
|
6431
|
-
return failClosed("PreToolUse",
|
|
6784
|
+
return failClosed("PreToolUse", errText2(e));
|
|
6432
6785
|
}
|
|
6433
6786
|
}
|
|
6434
6787
|
function cursorPath(cwd, sessionId) {
|
|
@@ -6440,10 +6793,10 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
6440
6793
|
} };
|
|
6441
6794
|
const log = defaultEventLog(cwd);
|
|
6442
6795
|
const cp = cursorPath(cwd, sessionId);
|
|
6443
|
-
if (!
|
|
6796
|
+
if (!existsSync6(log)) return none;
|
|
6444
6797
|
let offset = 0;
|
|
6445
|
-
if (cp &&
|
|
6446
|
-
const n = Number(
|
|
6798
|
+
if (cp && existsSync6(cp)) {
|
|
6799
|
+
const n = Number(readFileSync9(cp, "utf8"));
|
|
6447
6800
|
offset = Number.isFinite(n) ? n : 0;
|
|
6448
6801
|
}
|
|
6449
6802
|
const { events, newOffset } = readEvents(log, offset);
|
|
@@ -6455,12 +6808,8 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
6455
6808
|
} };
|
|
6456
6809
|
const persistent = new Set(changes.filter((c) => c.kind === "file").map((c) => c.path));
|
|
6457
6810
|
const finalHash = (path) => {
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
return createHash4("sha256").update(readFileSync11(`${cwd}/${path}`)).digest("hex").slice(0, 16);
|
|
6461
|
-
} catch {
|
|
6462
|
-
return null;
|
|
6463
|
-
}
|
|
6811
|
+
const e = inspectRel(cwd, path);
|
|
6812
|
+
return e.kind === "file" && e.content != null ? contentHash(e.content) : null;
|
|
6464
6813
|
};
|
|
6465
6814
|
const findings = transientFindings(events, persistent, policy, finalHash);
|
|
6466
6815
|
recordWarns(findings.filter((f) => f.severity !== "block"));
|
|
@@ -6501,7 +6850,7 @@ function stopVerdict(input) {
|
|
|
6501
6850
|
changes = changes.concat(gap.changes);
|
|
6502
6851
|
hiddenBlocks = gap.blocks;
|
|
6503
6852
|
}
|
|
6504
|
-
blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks);
|
|
6853
|
+
blocks = evaluate(changes, policy, void 0, "turn", { cwd }).filter((f) => f.severity === "block").concat(hiddenBlocks, unjudgeableProtected(cwd, policy, changes));
|
|
6505
6854
|
const transient = turnTransientBlocks(cwd, input.session_id, policy, changes);
|
|
6506
6855
|
blocks = blocks.concat(transient.blocks);
|
|
6507
6856
|
commitCursor = transient.commit;
|
|
@@ -6510,7 +6859,7 @@ function stopVerdict(input) {
|
|
|
6510
6859
|
saveTurnTree(cwd, input.session_id, current);
|
|
6511
6860
|
}
|
|
6512
6861
|
} catch (e) {
|
|
6513
|
-
return failClosed("Stop",
|
|
6862
|
+
return failClosed("Stop", errText2(e));
|
|
6514
6863
|
}
|
|
6515
6864
|
if (blocks.length === 0) {
|
|
6516
6865
|
advanceTurnBaseline(cwd, input.session_id);
|
|
@@ -6526,28 +6875,28 @@ function preToolUseFromRaw(raw) {
|
|
|
6526
6875
|
try {
|
|
6527
6876
|
return preToolUseVerdict(parseInput(raw));
|
|
6528
6877
|
} catch (e) {
|
|
6529
|
-
return failClosed("PreToolUse",
|
|
6878
|
+
return failClosed("PreToolUse", errText2(e));
|
|
6530
6879
|
}
|
|
6531
6880
|
}
|
|
6532
6881
|
function stopFromRaw(raw) {
|
|
6533
6882
|
try {
|
|
6534
6883
|
return stopVerdict(parseInput(raw));
|
|
6535
6884
|
} catch (e) {
|
|
6536
|
-
return failClosed("Stop",
|
|
6885
|
+
return failClosed("Stop", errText2(e));
|
|
6537
6886
|
}
|
|
6538
6887
|
}
|
|
6539
6888
|
function runHookClaude() {
|
|
6540
6889
|
try {
|
|
6541
6890
|
return emit(preToolUseFromRaw(readStdin()));
|
|
6542
6891
|
} catch (e) {
|
|
6543
|
-
return emit(failClosed("PreToolUse",
|
|
6892
|
+
return emit(failClosed("PreToolUse", errText2(e)));
|
|
6544
6893
|
}
|
|
6545
6894
|
}
|
|
6546
6895
|
function runSweepClaude() {
|
|
6547
6896
|
try {
|
|
6548
6897
|
return emit(stopFromRaw(readStdin()));
|
|
6549
6898
|
} catch (e) {
|
|
6550
|
-
return emit(failClosed("Stop",
|
|
6899
|
+
return emit(failClosed("Stop", errText2(e)));
|
|
6551
6900
|
}
|
|
6552
6901
|
}
|
|
6553
6902
|
|
|
@@ -6600,9 +6949,9 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
6600
6949
|
// src/cli/init.ts
|
|
6601
6950
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
6602
6951
|
import { createHash as createHash5 } from "node:crypto";
|
|
6603
|
-
import { existsSync as
|
|
6604
|
-
import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as
|
|
6605
|
-
var
|
|
6952
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
6953
|
+
import { basename, dirname as dirname4, isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve2 } from "node:path";
|
|
6954
|
+
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
6606
6955
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6607
6956
|
function gitConfig(cwd, key2) {
|
|
6608
6957
|
try {
|
|
@@ -6751,13 +7100,13 @@ function readWorkflowMark(src) {
|
|
|
6751
7100
|
if (!m) return null;
|
|
6752
7101
|
return { version: m[1], hash: m[2], body: src.replace(WORKFLOW_MARK_RE, "") };
|
|
6753
7102
|
}
|
|
6754
|
-
function toolSet(
|
|
6755
|
-
const m = String(
|
|
7103
|
+
function toolSet(matcher) {
|
|
7104
|
+
const m = String(matcher ?? "").trim();
|
|
6756
7105
|
if (m === "" || m === "*") return null;
|
|
6757
7106
|
return new Set(m.split("|").map((t) => t.trim()).filter(Boolean));
|
|
6758
7107
|
}
|
|
6759
|
-
function missingTools(
|
|
6760
|
-
const have = toolSet(
|
|
7108
|
+
function missingTools(matcher) {
|
|
7109
|
+
const have = toolSet(matcher);
|
|
6761
7110
|
if (have === null) return [];
|
|
6762
7111
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
6763
7112
|
}
|
|
@@ -6777,12 +7126,12 @@ function hooksShapeError(hooks) {
|
|
|
6777
7126
|
return null;
|
|
6778
7127
|
}
|
|
6779
7128
|
function planPolicy(cwd) {
|
|
6780
|
-
const path =
|
|
6781
|
-
if (
|
|
7129
|
+
const path = join10(cwd, POLICY_FILE);
|
|
7130
|
+
if (existsSync7(path)) {
|
|
6782
7131
|
try {
|
|
6783
7132
|
loadPolicy(cwd);
|
|
6784
7133
|
} catch (e) {
|
|
6785
|
-
return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${
|
|
7134
|
+
return { item: "policy", path: POLICY_FILE, status: "error", detail: `present but does not load \u2014 ${errText3(e)}; fix it, then re-run init (left untouched)` };
|
|
6786
7135
|
}
|
|
6787
7136
|
return { item: "policy", path: POLICY_FILE, status: "ok", detail: "already present and loads \u2014 left untouched" };
|
|
6788
7137
|
}
|
|
@@ -6796,12 +7145,12 @@ function planPolicy(cwd) {
|
|
|
6796
7145
|
}
|
|
6797
7146
|
function planClaudeHooks(cwd) {
|
|
6798
7147
|
const rel = ".claude/settings.json";
|
|
6799
|
-
const path =
|
|
7148
|
+
const path = join10(cwd, rel);
|
|
6800
7149
|
let settings = {};
|
|
6801
|
-
if (
|
|
7150
|
+
if (existsSync7(path)) {
|
|
6802
7151
|
let parsed;
|
|
6803
7152
|
try {
|
|
6804
|
-
parsed = JSON.parse(
|
|
7153
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
6805
7154
|
} catch {
|
|
6806
7155
|
return {
|
|
6807
7156
|
item: "agent",
|
|
@@ -6835,7 +7184,7 @@ function planClaudeHooks(cwd) {
|
|
|
6835
7184
|
return {
|
|
6836
7185
|
item: "agent",
|
|
6837
7186
|
path: rel,
|
|
6838
|
-
status:
|
|
7187
|
+
status: existsSync7(path) ? "update" : "create",
|
|
6839
7188
|
detail: [
|
|
6840
7189
|
needPre && "wire PreToolUse deny",
|
|
6841
7190
|
needStop && "wire Stop sweep",
|
|
@@ -6894,7 +7243,7 @@ function hooksDir(cwd) {
|
|
|
6894
7243
|
if (out2) return resolve2(cwd, out2);
|
|
6895
7244
|
} catch {
|
|
6896
7245
|
}
|
|
6897
|
-
return
|
|
7246
|
+
return existsSync7(join10(cwd, ".git")) ? join10(cwd, ".git", "hooks") : null;
|
|
6898
7247
|
}
|
|
6899
7248
|
function display(cwd, path) {
|
|
6900
7249
|
const rel = relative3(cwd, path);
|
|
@@ -6921,30 +7270,30 @@ function planPreCommit(cwd) {
|
|
|
6921
7270
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
6922
7271
|
let path;
|
|
6923
7272
|
let note;
|
|
6924
|
-
if (
|
|
6925
|
-
path =
|
|
7273
|
+
if (existsSync7(join10(cwd, ".husky"))) {
|
|
7274
|
+
path = join10(cwd, ".husky", "pre-commit");
|
|
6926
7275
|
note = "husky";
|
|
6927
7276
|
} else {
|
|
6928
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) =>
|
|
7277
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync7(join10(cwd, c))));
|
|
6929
7278
|
if (managed) {
|
|
6930
7279
|
return skip(`${managed.name} manages this repository's hooks and regenerates them on install, so a line in the hooks directory would not last \u2014 ${managed.hint}`);
|
|
6931
7280
|
}
|
|
6932
7281
|
const dir = hooksDir(cwd);
|
|
6933
7282
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
6934
7283
|
if (basename(dir) === "_" && basename(dirname4(dir)) === ".husky") {
|
|
6935
|
-
path =
|
|
7284
|
+
path = join10(dirname4(dir), "pre-commit");
|
|
6936
7285
|
note = "husky";
|
|
6937
7286
|
} else {
|
|
6938
|
-
path =
|
|
7287
|
+
path = join10(dir, "pre-commit");
|
|
6939
7288
|
const rel2 = display(cwd, dir);
|
|
6940
|
-
note = rel2 ===
|
|
7289
|
+
note = rel2 === join10(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
6941
7290
|
}
|
|
6942
7291
|
}
|
|
6943
7292
|
const rel = display(cwd, path);
|
|
6944
|
-
if (
|
|
7293
|
+
if (existsSync7(path) && !statSync3(path).isFile()) {
|
|
6945
7294
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
6946
7295
|
}
|
|
6947
|
-
const existing =
|
|
7296
|
+
const existing = existsSync7(path) ? readFileSync10(path, "utf8") : null;
|
|
6948
7297
|
if (existing === null) {
|
|
6949
7298
|
return {
|
|
6950
7299
|
item: "pre-commit",
|
|
@@ -7033,10 +7382,10 @@ ${block.join("\n")}
|
|
|
7033
7382
|
function planCodeowners(cwd) {
|
|
7034
7383
|
const rel = ".github/CODEOWNERS";
|
|
7035
7384
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
7036
|
-
(r) =>
|
|
7385
|
+
(r) => existsSync7(join10(cwd, r))
|
|
7037
7386
|
);
|
|
7038
|
-
const path =
|
|
7039
|
-
const existing = existingRel ?
|
|
7387
|
+
const path = join10(cwd, existingRel ?? rel);
|
|
7388
|
+
const existing = existingRel ? readFileSync10(path, "utf8") : null;
|
|
7040
7389
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
7041
7390
|
if (missing.length === 0) {
|
|
7042
7391
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -7069,12 +7418,12 @@ ${CODEOWNERS_MARK}
|
|
|
7069
7418
|
}
|
|
7070
7419
|
function planWorkflow(cwd, force) {
|
|
7071
7420
|
const rel = ".github/workflows/tamperward.yml";
|
|
7072
|
-
const path =
|
|
7421
|
+
const path = join10(cwd, rel);
|
|
7073
7422
|
const write = () => {
|
|
7074
7423
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7075
7424
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
7076
7425
|
};
|
|
7077
|
-
if (!
|
|
7426
|
+
if (!existsSync7(path)) {
|
|
7078
7427
|
return {
|
|
7079
7428
|
item: "ci",
|
|
7080
7429
|
path: rel,
|
|
@@ -7085,7 +7434,7 @@ function planWorkflow(cwd, force) {
|
|
|
7085
7434
|
}
|
|
7086
7435
|
let src;
|
|
7087
7436
|
try {
|
|
7088
|
-
src =
|
|
7437
|
+
src = readFileSync10(path, "utf8");
|
|
7089
7438
|
} catch {
|
|
7090
7439
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
7091
7440
|
}
|
|
@@ -7123,7 +7472,7 @@ function planned(item, path, plan) {
|
|
|
7123
7472
|
try {
|
|
7124
7473
|
return plan();
|
|
7125
7474
|
} catch (e) {
|
|
7126
|
-
return { item, path, status: "error", detail: `cannot plan this item \u2014 ${
|
|
7475
|
+
return { item, path, status: "error", detail: `cannot plan this item \u2014 ${errText3(e)}` };
|
|
7127
7476
|
}
|
|
7128
7477
|
}
|
|
7129
7478
|
function planInit(cwd, opts = {}) {
|
|
@@ -7147,7 +7496,7 @@ function runInit(opts) {
|
|
|
7147
7496
|
applied++;
|
|
7148
7497
|
} catch (e) {
|
|
7149
7498
|
a.status = "error";
|
|
7150
|
-
a.detail = `could not be written \u2014 ${
|
|
7499
|
+
a.detail = `could not be written \u2014 ${errText3(e)}`;
|
|
7151
7500
|
a.apply = void 0;
|
|
7152
7501
|
}
|
|
7153
7502
|
}
|
|
@@ -7184,15 +7533,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
7184
7533
|
// src/cli/verify.ts
|
|
7185
7534
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
7186
7535
|
import { createHash as createHash8 } from "node:crypto";
|
|
7187
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
7536
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync13, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "node:fs";
|
|
7188
7537
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
7189
|
-
import { dirname as dirname5, join as
|
|
7538
|
+
import { dirname as dirname5, join as join13, resolve as resolve3 } from "node:path";
|
|
7190
7539
|
|
|
7191
7540
|
// src/git/trusted.ts
|
|
7192
7541
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7193
7542
|
import { createHash as createHash6 } from "node:crypto";
|
|
7194
|
-
import { existsSync as
|
|
7195
|
-
import { isAbsolute as isAbsolute6, join as
|
|
7543
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
7544
|
+
import { isAbsolute as isAbsolute6, join as join11 } from "node:path";
|
|
7196
7545
|
function trustedGitEnv() {
|
|
7197
7546
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
7198
7547
|
}
|
|
@@ -7205,7 +7554,7 @@ function gitDirOf(cwd) {
|
|
|
7205
7554
|
env: trustedGitEnv()
|
|
7206
7555
|
}).trim();
|
|
7207
7556
|
if (!d) return null;
|
|
7208
|
-
return isAbsolute6(d) ? d :
|
|
7557
|
+
return isAbsolute6(d) ? d : join11(cwd, d);
|
|
7209
7558
|
} catch {
|
|
7210
7559
|
return null;
|
|
7211
7560
|
}
|
|
@@ -7230,9 +7579,9 @@ function objectRewriteState(cwd) {
|
|
|
7230
7579
|
parts.push(`${rel}:<no-git-dir>`);
|
|
7231
7580
|
continue;
|
|
7232
7581
|
}
|
|
7233
|
-
const p =
|
|
7582
|
+
const p = join11(dir, rel);
|
|
7234
7583
|
try {
|
|
7235
|
-
parts.push(`${rel}:` + (
|
|
7584
|
+
parts.push(`${rel}:` + (existsSync8(p) ? readFileSync11(p, "utf8").trim() : ""));
|
|
7236
7585
|
} catch {
|
|
7237
7586
|
parts.push(`${rel}:<unreadable>`);
|
|
7238
7587
|
}
|
|
@@ -7243,8 +7592,8 @@ function objectRewriteState(cwd) {
|
|
|
7243
7592
|
// src/fingerprint.ts
|
|
7244
7593
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7245
7594
|
import { createHash as createHash7 } from "node:crypto";
|
|
7246
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
7247
|
-
import { join as
|
|
7595
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync12, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
7596
|
+
import { join as join12 } from "node:path";
|
|
7248
7597
|
function treeFingerprint(cwd) {
|
|
7249
7598
|
const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
7250
7599
|
cwd,
|
|
@@ -7256,17 +7605,13 @@ function treeFingerprint(cwd) {
|
|
|
7256
7605
|
for (const rel of files) {
|
|
7257
7606
|
h.update(rel);
|
|
7258
7607
|
h.update("\0");
|
|
7259
|
-
|
|
7260
|
-
h.update(readFileSync14(join13(cwd, rel)));
|
|
7261
|
-
} catch {
|
|
7262
|
-
h.update("<unreadable>");
|
|
7263
|
-
}
|
|
7608
|
+
h.update(inspectRel(cwd, rel).content ?? "<unreadable>");
|
|
7264
7609
|
h.update("\0");
|
|
7265
7610
|
}
|
|
7266
7611
|
return h.digest("hex");
|
|
7267
7612
|
}
|
|
7268
7613
|
function depsFingerprint(cwd) {
|
|
7269
|
-
const root =
|
|
7614
|
+
const root = join12(cwd, "node_modules");
|
|
7270
7615
|
const h = createHash7("sha256");
|
|
7271
7616
|
const walk2 = (dir) => {
|
|
7272
7617
|
let entries;
|
|
@@ -7276,20 +7621,20 @@ function depsFingerprint(cwd) {
|
|
|
7276
7621
|
return;
|
|
7277
7622
|
}
|
|
7278
7623
|
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7279
|
-
const p =
|
|
7624
|
+
const p = join12(dir, e.name);
|
|
7280
7625
|
if (e.isDirectory()) walk2(p);
|
|
7281
7626
|
else if (e.isFile()) {
|
|
7282
7627
|
h.update(p);
|
|
7283
7628
|
try {
|
|
7284
|
-
h.update(
|
|
7285
|
-
h.update(String(
|
|
7629
|
+
h.update(readFileSync12(p));
|
|
7630
|
+
h.update(String(statSync4(p).mode));
|
|
7286
7631
|
} catch {
|
|
7287
7632
|
h.update("<unreadable>");
|
|
7288
7633
|
}
|
|
7289
7634
|
} else if (e.isSymbolicLink()) {
|
|
7290
7635
|
h.update(p);
|
|
7291
7636
|
try {
|
|
7292
|
-
h.update(
|
|
7637
|
+
h.update(readlinkSync2(p));
|
|
7293
7638
|
} catch {
|
|
7294
7639
|
h.update("<unreadable-link>");
|
|
7295
7640
|
}
|
|
@@ -7297,7 +7642,7 @@ function depsFingerprint(cwd) {
|
|
|
7297
7642
|
}
|
|
7298
7643
|
};
|
|
7299
7644
|
try {
|
|
7300
|
-
if (!
|
|
7645
|
+
if (!statSync4(root).isDirectory()) return "none";
|
|
7301
7646
|
} catch {
|
|
7302
7647
|
return "none";
|
|
7303
7648
|
}
|
|
@@ -7396,30 +7741,30 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
7396
7741
|
function materialize(cwd, dest) {
|
|
7397
7742
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
7398
7743
|
for (const rel of listed) {
|
|
7399
|
-
const src =
|
|
7744
|
+
const src = join13(cwd, rel);
|
|
7400
7745
|
let st;
|
|
7401
7746
|
try {
|
|
7402
|
-
st =
|
|
7747
|
+
st = statSync5(src);
|
|
7403
7748
|
} catch {
|
|
7404
7749
|
continue;
|
|
7405
7750
|
}
|
|
7406
7751
|
if (!st.isFile()) continue;
|
|
7407
|
-
const out2 =
|
|
7752
|
+
const out2 = join13(dest, rel);
|
|
7408
7753
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
7409
7754
|
dropSymlink(out2);
|
|
7410
|
-
if (
|
|
7411
|
-
writeFileSync6(out2,
|
|
7755
|
+
if (lstatSync3(src).isSymbolicLink()) {
|
|
7756
|
+
writeFileSync6(out2, readFileSync13(src));
|
|
7412
7757
|
} else {
|
|
7413
7758
|
cpSync(src, out2, { dereference: true });
|
|
7414
7759
|
}
|
|
7415
7760
|
chmodSync2(out2, st.mode);
|
|
7416
7761
|
}
|
|
7417
7762
|
const nm = resolve3(cwd, "node_modules");
|
|
7418
|
-
if (
|
|
7763
|
+
if (existsSync9(nm)) symlinkSync(nm, join13(dest, "node_modules"), "dir");
|
|
7419
7764
|
}
|
|
7420
7765
|
function dropSymlink(p) {
|
|
7421
7766
|
try {
|
|
7422
|
-
if (
|
|
7767
|
+
if (lstatSync3(p).isSymbolicLink()) rmSync3(p, { force: true });
|
|
7423
7768
|
} catch {
|
|
7424
7769
|
}
|
|
7425
7770
|
}
|
|
@@ -7448,11 +7793,11 @@ function overlayDigest(dest, paths) {
|
|
|
7448
7793
|
for (const rel of [...paths].sort()) {
|
|
7449
7794
|
h.update(rel);
|
|
7450
7795
|
h.update("\0");
|
|
7451
|
-
const p =
|
|
7796
|
+
const p = join13(dest, rel);
|
|
7452
7797
|
try {
|
|
7453
|
-
const st =
|
|
7798
|
+
const st = lstatSync3(p);
|
|
7454
7799
|
h.update(String(st.mode));
|
|
7455
|
-
h.update(st.isSymbolicLink() ?
|
|
7800
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync13(p));
|
|
7456
7801
|
} catch {
|
|
7457
7802
|
h.update("<gone>");
|
|
7458
7803
|
}
|
|
@@ -7475,7 +7820,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
7475
7820
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
7476
7821
|
}
|
|
7477
7822
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
7478
|
-
const out2 =
|
|
7823
|
+
const out2 = join13(dest, e.path);
|
|
7479
7824
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
7480
7825
|
rmSync3(out2, { force: true });
|
|
7481
7826
|
if (e.mode === "120000") {
|
|
@@ -7490,7 +7835,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
7490
7835
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
7491
7836
|
for (const rel of inCopy) {
|
|
7492
7837
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
7493
|
-
rmSync3(
|
|
7838
|
+
rmSync3(join13(dest, rel), { force: true });
|
|
7494
7839
|
removed++;
|
|
7495
7840
|
}
|
|
7496
7841
|
return { restored, removed };
|
|
@@ -7532,8 +7877,8 @@ function suiteEnv(scratch) {
|
|
|
7532
7877
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
7533
7878
|
env[k] = v;
|
|
7534
7879
|
}
|
|
7535
|
-
const userRc =
|
|
7536
|
-
const globalRc =
|
|
7880
|
+
const userRc = join13(scratch, "npmrc-user");
|
|
7881
|
+
const globalRc = join13(scratch, "npmrc-global");
|
|
7537
7882
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
7538
7883
|
env.npm_config_userconfig = userRc;
|
|
7539
7884
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -7543,8 +7888,8 @@ function suiteEnv(scratch) {
|
|
|
7543
7888
|
}
|
|
7544
7889
|
function runSuite(dir, cmd, budgetSecs) {
|
|
7545
7890
|
const t0 = Date.now();
|
|
7546
|
-
const outDir = mkdtempSync2(
|
|
7547
|
-
const outFile =
|
|
7891
|
+
const outDir = mkdtempSync2(join13(tmpdir2(), "tw-verify-run-"));
|
|
7892
|
+
const outFile = join13(outDir, "result.json");
|
|
7548
7893
|
try {
|
|
7549
7894
|
spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
7550
7895
|
cwd: dir,
|
|
@@ -7557,7 +7902,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
7557
7902
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
7558
7903
|
let r;
|
|
7559
7904
|
try {
|
|
7560
|
-
r = JSON.parse(
|
|
7905
|
+
r = JSON.parse(readFileSync13(outFile, "utf8"));
|
|
7561
7906
|
} catch {
|
|
7562
7907
|
return { exit: null, secs };
|
|
7563
7908
|
}
|
|
@@ -7608,8 +7953,8 @@ function runVerify(opts) {
|
|
|
7608
7953
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
7609
7954
|
return 2;
|
|
7610
7955
|
}
|
|
7611
|
-
const visRoot = mkdtempSync2(
|
|
7612
|
-
const visDir =
|
|
7956
|
+
const visRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-vis-"));
|
|
7957
|
+
const visDir = join13(visRoot, "t");
|
|
7613
7958
|
const cleanup = (dirs) => {
|
|
7614
7959
|
if (opts.keep) return;
|
|
7615
7960
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
@@ -7637,8 +7982,8 @@ function runVerify(opts) {
|
|
|
7637
7982
|
out2("pristine run executes through it too. Failing closed, not open.");
|
|
7638
7983
|
return 2;
|
|
7639
7984
|
}
|
|
7640
|
-
const priRoot = mkdtempSync2(
|
|
7641
|
-
const priDir =
|
|
7985
|
+
const priRoot = mkdtempSync2(join13(tmpdir2(), "tw-verify-pri-"));
|
|
7986
|
+
const priDir = join13(priRoot, "t");
|
|
7642
7987
|
let restored = [];
|
|
7643
7988
|
let removedAdded = 0;
|
|
7644
7989
|
try {
|
|
@@ -7750,7 +8095,7 @@ function parseVerify(args) {
|
|
|
7750
8095
|
|
|
7751
8096
|
// src/cli/run.ts
|
|
7752
8097
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
7753
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
8098
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "node:fs";
|
|
7754
8099
|
import { resolve as resolve4 } from "node:path";
|
|
7755
8100
|
var out = (s) => process.stdout.write(s + "\n");
|
|
7756
8101
|
var err = (s) => process.stderr.write(s + "\n");
|
|
@@ -7759,7 +8104,7 @@ function git3(args, cwd) {
|
|
|
7759
8104
|
}
|
|
7760
8105
|
function nowTicks() {
|
|
7761
8106
|
try {
|
|
7762
|
-
return Math.floor(parseFloat(
|
|
8107
|
+
return Math.floor(parseFloat(readFileSync14("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
7763
8108
|
} catch {
|
|
7764
8109
|
return Number.POSITIVE_INFINITY;
|
|
7765
8110
|
}
|
|
@@ -7768,7 +8113,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
7768
8113
|
const out2 = [];
|
|
7769
8114
|
let real;
|
|
7770
8115
|
try {
|
|
7771
|
-
real =
|
|
8116
|
+
real = realpathSync2(cwd);
|
|
7772
8117
|
} catch {
|
|
7773
8118
|
return out2;
|
|
7774
8119
|
}
|
|
@@ -7783,13 +8128,13 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
7783
8128
|
if (n === process.pid) continue;
|
|
7784
8129
|
let held;
|
|
7785
8130
|
try {
|
|
7786
|
-
held =
|
|
8131
|
+
held = realpathSync2(`/proc/${pid}/cwd`);
|
|
7787
8132
|
} catch {
|
|
7788
8133
|
continue;
|
|
7789
8134
|
}
|
|
7790
8135
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
7791
8136
|
try {
|
|
7792
|
-
const stat =
|
|
8137
|
+
const stat = readFileSync14(`/proc/${pid}/stat`, "utf8");
|
|
7793
8138
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
7794
8139
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
7795
8140
|
} catch {
|