tamperward 2.10.1 → 2.10.3
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 +295 -105
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -153,6 +153,18 @@ function defaultPolicy(version = 1) {
|
|
|
153
153
|
// as a strengthening). pyproject.toml carries [tool.coverage.report].
|
|
154
154
|
"**/.coveragerc",
|
|
155
155
|
"**/pyproject.toml",
|
|
156
|
+
// pytest's selection configuration. These decide WHICH TESTS RUN, so a
|
|
157
|
+
// narrowing in them removes a test as surely as deleting the file. The
|
|
158
|
+
// pristine boundary already refused to inherit them (verify's
|
|
159
|
+
// VERIFICATION_SURFACE, 1.14.2); this is the in-loop half, so the gate can
|
|
160
|
+
// say so at the tool call instead of only at adjudication. `tox.ini` and
|
|
161
|
+
// `setup.cfg` are shared files — the suite-config detector only treats them
|
|
162
|
+
// as pytest configs when they carry the section pytest reads, so ordinary
|
|
163
|
+
// packaging edits are untouched.
|
|
164
|
+
"**/pytest.ini",
|
|
165
|
+
"**/.pytest.ini",
|
|
166
|
+
"**/tox.ini",
|
|
167
|
+
"**/setup.cfg",
|
|
156
168
|
"**/.nycrc",
|
|
157
169
|
"**/.nycrc.*",
|
|
158
170
|
"**/nyc.config.*",
|
|
@@ -1334,6 +1346,8 @@ import ts2 from "typescript";
|
|
|
1334
1346
|
|
|
1335
1347
|
// src/detectors/repo.ts
|
|
1336
1348
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1349
|
+
import { readFileSync } from "node:fs";
|
|
1350
|
+
import { join as join2 } from "node:path";
|
|
1337
1351
|
var cache = /* @__PURE__ */ new Map();
|
|
1338
1352
|
function trackedFiles(ctx) {
|
|
1339
1353
|
if (!ctx) return null;
|
|
@@ -1355,6 +1369,23 @@ function trackedFiles(ctx) {
|
|
|
1355
1369
|
cache.set(ctx.cwd, files);
|
|
1356
1370
|
return files;
|
|
1357
1371
|
}
|
|
1372
|
+
var contentCache = /* @__PURE__ */ new Map();
|
|
1373
|
+
function trackedContent(path, ctx) {
|
|
1374
|
+
if (!ctx) return null;
|
|
1375
|
+
if (ctx.trackedContents) return ctx.trackedContents[path] ?? null;
|
|
1376
|
+
if (!ctx.cwd) return null;
|
|
1377
|
+
const key2 = ctx.cwd + "\0" + path;
|
|
1378
|
+
const hit = contentCache.get(key2);
|
|
1379
|
+
if (hit !== void 0) return hit;
|
|
1380
|
+
let src = null;
|
|
1381
|
+
try {
|
|
1382
|
+
src = readFileSync(join2(ctx.cwd, path), "utf8");
|
|
1383
|
+
} catch {
|
|
1384
|
+
src = null;
|
|
1385
|
+
}
|
|
1386
|
+
contentCache.set(key2, src);
|
|
1387
|
+
return src;
|
|
1388
|
+
}
|
|
1358
1389
|
var branchCache = /* @__PURE__ */ new Map();
|
|
1359
1390
|
var refsCache = /* @__PURE__ */ new Map();
|
|
1360
1391
|
function branchRefs(ctx) {
|
|
@@ -2660,20 +2691,20 @@ var ciTampering = {
|
|
|
2660
2691
|
};
|
|
2661
2692
|
|
|
2662
2693
|
// src/detectors/hook-tampering.ts
|
|
2663
|
-
import { existsSync as existsSync2, readFileSync as
|
|
2664
|
-
import { isAbsolute as isAbsolute3, join as
|
|
2694
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2695
|
+
import { isAbsolute as isAbsolute3, join as join4, relative } from "node:path";
|
|
2665
2696
|
|
|
2666
2697
|
// src/wiring.ts
|
|
2667
|
-
import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2698
|
+
import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
2668
2699
|
import { homedir } from "node:os";
|
|
2669
|
-
import { basename, dirname, isAbsolute, join as
|
|
2700
|
+
import { basename, dirname, isAbsolute, join as join3, posix } from "node:path";
|
|
2670
2701
|
import { fileURLToPath } from "node:url";
|
|
2671
2702
|
function shippedVersion() {
|
|
2672
2703
|
try {
|
|
2673
2704
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2674
2705
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
2675
2706
|
try {
|
|
2676
|
-
const pkg = JSON.parse(
|
|
2707
|
+
const pkg = JSON.parse(readFileSync2(join3(here, rel), "utf8"));
|
|
2677
2708
|
if (pkg.name === "tamperward" && pkg.version) return pkg.version;
|
|
2678
2709
|
} catch {
|
|
2679
2710
|
}
|
|
@@ -4551,8 +4582,8 @@ function lefthookBase(c, changes, ctx) {
|
|
|
4551
4582
|
if (!ctx?.cwd) return null;
|
|
4552
4583
|
for (const n of names) {
|
|
4553
4584
|
try {
|
|
4554
|
-
const p =
|
|
4555
|
-
if (existsSync2(p)) return parseDoc(
|
|
4585
|
+
const p = join4(ctx.cwd, n);
|
|
4586
|
+
if (existsSync2(p)) return parseDoc(readFileSync3(p, "utf8"));
|
|
4556
4587
|
} catch {
|
|
4557
4588
|
}
|
|
4558
4589
|
}
|
|
@@ -4673,7 +4704,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
4673
4704
|
if (!ok) return false;
|
|
4674
4705
|
if (!ctx?.cwd) return false;
|
|
4675
4706
|
const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
|
|
4676
|
-
const entry = inspectRel(
|
|
4707
|
+
const entry = inspectRel(join4(ctx.cwd, root), ".husky/pre-commit");
|
|
4677
4708
|
if (entry.kind !== "file") return false;
|
|
4678
4709
|
const gate = textOf(entry);
|
|
4679
4710
|
if (gate == null) return false;
|
|
@@ -4953,6 +4984,8 @@ import ts3 from "typescript";
|
|
|
4953
4984
|
var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
|
|
4954
4985
|
var JEST_DEFAULT_IGNORE = ["/node_modules/"];
|
|
4955
4986
|
var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
4987
|
+
var PYTEST_DEFAULT_INCLUDE = ["**/test_*.py", "**/*_test.py"];
|
|
4988
|
+
var PYTEST_DEFAULT_IGNORE = ["**/build/**", "**/dist/**", "**/node_modules/**", "**/venv/**", "**/.*/**"];
|
|
4956
4989
|
var VITEST_DEFAULT_EXCLUDE = [
|
|
4957
4990
|
"**/node_modules/**",
|
|
4958
4991
|
"**/dist/**",
|
|
@@ -4970,6 +5003,13 @@ var CANONICAL_SAMPLES = [
|
|
|
4970
5003
|
"a.spec.js",
|
|
4971
5004
|
"test/a.test.mjs"
|
|
4972
5005
|
];
|
|
5006
|
+
var PYTEST_CANONICAL_SAMPLES = [
|
|
5007
|
+
"tests/test_a.py",
|
|
5008
|
+
"tests/a_test.py",
|
|
5009
|
+
"test_a.py",
|
|
5010
|
+
"src/tests/test_a.py",
|
|
5011
|
+
"tests/conftest.py"
|
|
5012
|
+
];
|
|
4973
5013
|
function keyName2(name) {
|
|
4974
5014
|
if (ts3.isIdentifier(name) || ts3.isStringLiteral(name)) return name.text;
|
|
4975
5015
|
return null;
|
|
@@ -5004,8 +5044,107 @@ function runnerOf(path, content) {
|
|
|
5004
5044
|
if (/^jest\.config\./.test(base) || base === "package.json") return "jest";
|
|
5005
5045
|
if (/^vitest\.(?:config|workspace)\./.test(base)) return "vitest";
|
|
5006
5046
|
if (/^vite\.config\./.test(base) && content != null && /(?:^|[\s,{])["']?test["']?\s*:/m.test(content)) return "vitest";
|
|
5047
|
+
if (base === "pytest.ini" || base === ".pytest.ini") return "pytest";
|
|
5048
|
+
if (content == null) return null;
|
|
5049
|
+
if (base === "tox.ini" && /^\s*\[pytest\]/m.test(content)) return "pytest";
|
|
5050
|
+
if (base === "setup.cfg" && /^\s*\[tool:pytest\]/m.test(content)) return "pytest";
|
|
5051
|
+
if (base === "pyproject.toml" && /^\s*\[tool\.pytest\.ini_options\]/m.test(content)) return "pytest";
|
|
5052
|
+
return null;
|
|
5053
|
+
}
|
|
5054
|
+
var PYTEST_INI_ORDER = ["pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"];
|
|
5055
|
+
function claimsPytest(name, src) {
|
|
5056
|
+
if (name === "pytest.ini" || name === ".pytest.ini") return true;
|
|
5057
|
+
const section = name === "pyproject.toml" ? /^\s*\[tool\.pytest\.ini_options\]\s*$/m : name === "tox.ini" ? /^\s*\[pytest\]\s*$/m : name === "setup.cfg" ? /^\s*\[tool:pytest\]\s*$/m : null;
|
|
5058
|
+
return section !== null && section.test(src);
|
|
5059
|
+
}
|
|
5060
|
+
function effectivePytestFile(files) {
|
|
5061
|
+
for (const name of PYTEST_INI_ORDER) {
|
|
5062
|
+
const content = files.get(name);
|
|
5063
|
+
if (content == null) continue;
|
|
5064
|
+
if (claimsPytest(name, content)) return { path: name, content };
|
|
5065
|
+
}
|
|
5007
5066
|
return null;
|
|
5008
5067
|
}
|
|
5068
|
+
function effectivePytestConfig(path, ctx) {
|
|
5069
|
+
if (path.includes("/")) return false;
|
|
5070
|
+
const base = path;
|
|
5071
|
+
const ORDER = PYTEST_INI_ORDER;
|
|
5072
|
+
const rank = ORDER.indexOf(base);
|
|
5073
|
+
if (rank < 0) return false;
|
|
5074
|
+
const files = ctx?.trackedFiles;
|
|
5075
|
+
if (!files) return true;
|
|
5076
|
+
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
5077
|
+
return !ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
5078
|
+
}
|
|
5079
|
+
function pytestBlock(src) {
|
|
5080
|
+
const lines = src.split(/\r?\n/);
|
|
5081
|
+
const head = /^\s*\[(pytest|tool:pytest|tool\.pytest\.ini_options)\]\s*$/;
|
|
5082
|
+
let i = lines.findIndex((l) => head.test(l));
|
|
5083
|
+
if (i < 0) return null;
|
|
5084
|
+
const out2 = {};
|
|
5085
|
+
let key2 = null;
|
|
5086
|
+
for (i += 1; i < lines.length; i++) {
|
|
5087
|
+
const line = lines[i];
|
|
5088
|
+
if (/^\s*\[/.test(line)) break;
|
|
5089
|
+
if (/^\s*[#;]/.test(line) || !line.trim()) {
|
|
5090
|
+
continue;
|
|
5091
|
+
}
|
|
5092
|
+
const m = line.match(/^(\w[\w.-]*)\s*[=:]\s*(.*)$/);
|
|
5093
|
+
if (m) {
|
|
5094
|
+
key2 = m[1];
|
|
5095
|
+
out2[key2] = m[2].trim();
|
|
5096
|
+
continue;
|
|
5097
|
+
}
|
|
5098
|
+
if (key2 && /^\s+\S/.test(line)) out2[key2] += " " + line.trim();
|
|
5099
|
+
}
|
|
5100
|
+
return out2;
|
|
5101
|
+
}
|
|
5102
|
+
function pytestTokens(raw) {
|
|
5103
|
+
const v = raw.trim();
|
|
5104
|
+
if (v.startsWith("[")) {
|
|
5105
|
+
return [...v.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
|
|
5106
|
+
}
|
|
5107
|
+
const unq2 = v.replace(/^"([\s\S]*)"$/, "$1").replace(/^'([\s\S]*)'$/, "$1");
|
|
5108
|
+
return [...unq2.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean);
|
|
5109
|
+
}
|
|
5110
|
+
var pathGlobs = (p) => {
|
|
5111
|
+
const clean = p.replace(/^\.\//, "").replace(/::.*$/, "").replace(/\/+$/, "");
|
|
5112
|
+
return clean ? [clean, clean + "/**"] : [];
|
|
5113
|
+
};
|
|
5114
|
+
function pytestSelection(src) {
|
|
5115
|
+
const sel = empty();
|
|
5116
|
+
const blk = pytestBlock(src);
|
|
5117
|
+
if (!blk) return sel;
|
|
5118
|
+
sel.present = true;
|
|
5119
|
+
const ignore = blk.norecursedirs !== void 0 ? pytestTokens(blk.norecursedirs).flatMap((d) => pathGlobs(d).map((pattern) => ({ pattern, key: "norecursedirs" }))) : PYTEST_DEFAULT_IGNORE.map((pattern) => ({ pattern, key: "norecursedirs" }));
|
|
5120
|
+
if (blk.testpaths !== void 0) sel.roots = pytestTokens(blk.testpaths).map((t) => t.replace(/^\.\//, ""));
|
|
5121
|
+
if (blk.python_files !== void 0) sel.include = pytestTokens(blk.python_files).map((g) => g.includes("/") ? g : "**/" + g);
|
|
5122
|
+
for (const key2 of ["python_classes", "python_functions"]) {
|
|
5123
|
+
if (blk[key2] !== void 0) ignore.push({ pattern: "\0" + key2, key: key2 });
|
|
5124
|
+
}
|
|
5125
|
+
const opts = blk.addopts !== void 0 ? pytestTokens(blk.addopts) : [];
|
|
5126
|
+
for (let j = 0; j < opts.length; j++) {
|
|
5127
|
+
const o = opts[j];
|
|
5128
|
+
const val = (inline) => o.includes("=") ? o.slice(o.indexOf("=") + 1) : opts[++j] ?? inline;
|
|
5129
|
+
if (o === "--ignore" || o.startsWith("--ignore=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--ignore" });
|
|
5130
|
+
else if (o === "--ignore-glob" || o.startsWith("--ignore-glob=")) {
|
|
5131
|
+
const g = val("");
|
|
5132
|
+
ignore.push({ pattern: g, key: "--ignore-glob" });
|
|
5133
|
+
if (!g.startsWith("**/") && !g.startsWith("/")) ignore.push({ pattern: "**/" + g, key: "--ignore-glob" });
|
|
5134
|
+
} else if (o === "--deselect" || o.startsWith("--deselect=")) for (const g of pathGlobs(val(""))) ignore.push({ pattern: g, key: "--deselect" });
|
|
5135
|
+
else if (o === "-k" || o.startsWith("-k=")) {
|
|
5136
|
+
for (const w of (val("").match(/\bnot\s+([A-Za-z_][\w]*)/g) ?? []).map((t) => t.replace(/^not\s+/, "")))
|
|
5137
|
+
ignore.push({ pattern: `**/*${w}*`, key: "-k" });
|
|
5138
|
+
} else if (o === "-m" || o.startsWith("-m=")) {
|
|
5139
|
+
ignore.push({ pattern: "\0-m " + val(""), key: "-m" });
|
|
5140
|
+
} else if (o === "-p" || o.startsWith("-p=") || o.length > 2 && o.startsWith("-p")) {
|
|
5141
|
+
const v = o === "-p" ? opts[++j] ?? "" : o.startsWith("-p=") ? o.slice(3) : o.slice(2);
|
|
5142
|
+
if (v.startsWith("no:")) ignore.push({ pattern: "\0-p " + v, key: "-p" });
|
|
5143
|
+
} else if (o === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
|
|
5144
|
+
}
|
|
5145
|
+
sel.ignore = ignore;
|
|
5146
|
+
return sel;
|
|
5147
|
+
}
|
|
5009
5148
|
var isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
|
|
5010
5149
|
var empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
|
|
5011
5150
|
var stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
@@ -5154,6 +5293,7 @@ function workspaceArray(sf) {
|
|
|
5154
5293
|
}
|
|
5155
5294
|
function parseSelection(src, runner, path = "") {
|
|
5156
5295
|
let sel = empty();
|
|
5296
|
+
if (runner === "pytest") return pytestSelection(src);
|
|
5157
5297
|
try {
|
|
5158
5298
|
const sf = ts3.createSourceFile("cfg.ts", asExpression2(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
|
|
5159
5299
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
@@ -5202,10 +5342,14 @@ function predicate(sel, runner) {
|
|
|
5202
5342
|
why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
5203
5343
|
};
|
|
5204
5344
|
}
|
|
5205
|
-
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : VITEST_DEFAULT_INCLUDE);
|
|
5206
|
-
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE :
|
|
5345
|
+
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5346
|
+
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
5347
|
+
pattern,
|
|
5348
|
+
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
5349
|
+
}));
|
|
5350
|
+
const blockWide = ignoreList.filter((e) => e.pattern.startsWith("\0"));
|
|
5207
5351
|
const included = includeList ? globList2(includeList) : sel.regex ? regexes(sel.regex) : () => true;
|
|
5208
|
-
const ignoredBy = (p) => ignoreList.find((e) => (runner === "jest" ? regexes([e.pattern]) : globs3([e.pattern]))(p));
|
|
5352
|
+
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs3([e.pattern]))(p));
|
|
5209
5353
|
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
5210
5354
|
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
5211
5355
|
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
@@ -5220,7 +5364,10 @@ function predicate(sel, runner) {
|
|
|
5220
5364
|
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
5221
5365
|
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
5222
5366
|
const hit = ignoredBy(r);
|
|
5223
|
-
if (hit)
|
|
5367
|
+
if (hit) {
|
|
5368
|
+
if (hit.pattern.startsWith("\0")) return `${hit.key} narrows the whole run (${JSON.stringify(hit.pattern.slice(1))})`;
|
|
5369
|
+
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(hit.pattern)})`;
|
|
5370
|
+
}
|
|
5224
5371
|
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
5225
5372
|
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
5226
5373
|
}
|
|
@@ -5370,12 +5517,14 @@ function significantLines(src, path) {
|
|
|
5370
5517
|
return out2;
|
|
5371
5518
|
}
|
|
5372
5519
|
var OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
|
|
5373
|
-
function runnerSamples(configPath, policy, ctx) {
|
|
5520
|
+
function runnerSamples(configPath, policy, ctx, runner) {
|
|
5521
|
+
const lang = runner === "pytest" ? "py" : "js";
|
|
5522
|
+
const fallback = runner === "pytest" ? PYTEST_CANONICAL_SAMPLES : CANONICAL_SAMPLES;
|
|
5374
5523
|
const files = trackedFiles(ctx);
|
|
5375
|
-
if (!files) return
|
|
5524
|
+
if (!files) return fallback;
|
|
5376
5525
|
const root = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/") + 1) : "";
|
|
5377
|
-
const own = files.filter((f) => f.startsWith(root) && langOf(f) ===
|
|
5378
|
-
return own.length ? own :
|
|
5526
|
+
const own = files.filter((f) => f.startsWith(root) && langOf(f) === lang && isProtected(f, policy, "tests") && (lang !== "js" || !OTHER_RUNNER_DIR.test(f))).map((f) => f.slice(root.length));
|
|
5527
|
+
return own.length ? own : fallback;
|
|
5379
5528
|
}
|
|
5380
5529
|
var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
|
|
5381
5530
|
function excludesOtherRunner(flag, cmd) {
|
|
@@ -5496,7 +5645,9 @@ var testDeletion = {
|
|
|
5496
5645
|
);
|
|
5497
5646
|
}
|
|
5498
5647
|
} else if (c.op !== "delete" && c.after != null && !isTest && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
|
|
5499
|
-
|
|
5648
|
+
const rn = runnerOf(c.path, c.after);
|
|
5649
|
+
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
5650
|
+
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
5500
5651
|
out2.push(
|
|
5501
5652
|
makeFinding(RULE7, policy, {
|
|
5502
5653
|
file: c.path,
|
|
@@ -5606,9 +5757,48 @@ var testDeletion = {
|
|
|
5606
5757
|
}
|
|
5607
5758
|
}
|
|
5608
5759
|
}
|
|
5760
|
+
out2.push(...pytestPrecedenceFindings(changes, policy, ctx));
|
|
5609
5761
|
return out2;
|
|
5610
5762
|
}
|
|
5611
5763
|
};
|
|
5764
|
+
function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
5765
|
+
const order = PYTEST_INI_ORDER;
|
|
5766
|
+
const touched = /* @__PURE__ */ new Map();
|
|
5767
|
+
for (const c of changes) {
|
|
5768
|
+
if (c.kind !== "file" || c.oldPath) continue;
|
|
5769
|
+
if (order.includes(c.path)) touched.set(c.path, c);
|
|
5770
|
+
}
|
|
5771
|
+
if (touched.size === 0) return [];
|
|
5772
|
+
const listing = trackedFiles(ctx);
|
|
5773
|
+
if (!listing) return [];
|
|
5774
|
+
const rootNames = new Set(listing.filter((f) => !f.includes("/")));
|
|
5775
|
+
const base = /* @__PURE__ */ new Map();
|
|
5776
|
+
const head = /* @__PURE__ */ new Map();
|
|
5777
|
+
for (const name of order) {
|
|
5778
|
+
const c = touched.get(name);
|
|
5779
|
+
if (c) {
|
|
5780
|
+
base.set(name, c.before);
|
|
5781
|
+
head.set(name, c.after);
|
|
5782
|
+
continue;
|
|
5783
|
+
}
|
|
5784
|
+
if (!rootNames.has(name)) continue;
|
|
5785
|
+
const src = trackedContent(name, ctx);
|
|
5786
|
+
base.set(name, src);
|
|
5787
|
+
head.set(name, src);
|
|
5788
|
+
}
|
|
5789
|
+
const was = effectivePytestFile(base);
|
|
5790
|
+
const now = effectivePytestFile(head);
|
|
5791
|
+
if (!was || !now || was.path === now.path) return [];
|
|
5792
|
+
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
5793
|
+
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
5794
|
+
(n) => makeFinding(RULE7, policy, {
|
|
5795
|
+
file: now.path,
|
|
5796
|
+
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
5797
|
+
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
5798
|
+
remediation: "pytest reads ONE config file. Adding a higher-precedence one, or deleting the one in force, changes which settings apply \u2014 keep the effective configuration selecting the whole suite."
|
|
5799
|
+
})
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5612
5802
|
|
|
5613
5803
|
// src/detectors/test-content-removal.ts
|
|
5614
5804
|
import ts5 from "typescript";
|
|
@@ -5894,8 +6084,8 @@ function hasBlocking(findings) {
|
|
|
5894
6084
|
}
|
|
5895
6085
|
|
|
5896
6086
|
// src/policy-load.ts
|
|
5897
|
-
import { readFileSync as
|
|
5898
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6087
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
6088
|
+
import { isAbsolute as isAbsolute4, join as join5, posix as posix2 } from "node:path";
|
|
5899
6089
|
import { parse as parse2 } from "yaml";
|
|
5900
6090
|
var PolicyError = class extends Error {
|
|
5901
6091
|
};
|
|
@@ -6019,9 +6209,9 @@ function parseOrThrow(src, where) {
|
|
|
6019
6209
|
return parsePolicy(raw, where);
|
|
6020
6210
|
}
|
|
6021
6211
|
function loadPolicy(cwd = process.cwd()) {
|
|
6022
|
-
const path =
|
|
6212
|
+
const path = join5(cwd, POLICY_FILE);
|
|
6023
6213
|
if (!existsSync3(path)) return defaultPolicy();
|
|
6024
|
-
return parseOrThrow(
|
|
6214
|
+
return parseOrThrow(readFileSync4(path, "utf8"), POLICY_FILE);
|
|
6025
6215
|
}
|
|
6026
6216
|
function loadPolicyAt(rev, cwd) {
|
|
6027
6217
|
const src = fileAt(rev, POLICY_FILE, { cwd });
|
|
@@ -6031,8 +6221,8 @@ function loadPolicyAt(rev, cwd) {
|
|
|
6031
6221
|
|
|
6032
6222
|
// src/signoff.ts
|
|
6033
6223
|
import { createHash } from "node:crypto";
|
|
6034
|
-
import { appendFileSync, mkdirSync, readFileSync as
|
|
6035
|
-
import { dirname as dirname2, join as
|
|
6224
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync5, existsSync as existsSync4 } from "node:fs";
|
|
6225
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
6036
6226
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6037
6227
|
function fingerprint(rule, file, evidence) {
|
|
6038
6228
|
return createHash("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
|
|
@@ -6043,13 +6233,13 @@ function ledgerPath(cwd, policy) {
|
|
|
6043
6233
|
if (!ledgerInsideRepo(rel)) {
|
|
6044
6234
|
throw new PolicyError(`signoff.ledger must be a relative path inside the repository, got ${JSON.stringify(rel)}`);
|
|
6045
6235
|
}
|
|
6046
|
-
return
|
|
6236
|
+
return join6(cwd, rel);
|
|
6047
6237
|
}
|
|
6048
6238
|
function readLedger(cwd, policy) {
|
|
6049
6239
|
const p = ledgerPath(cwd, policy);
|
|
6050
6240
|
if (!existsSync4(p)) return [];
|
|
6051
6241
|
const out2 = [];
|
|
6052
|
-
for (const line of
|
|
6242
|
+
for (const line of readFileSync5(p, "utf8").split("\n")) {
|
|
6053
6243
|
if (!line.trim()) continue;
|
|
6054
6244
|
try {
|
|
6055
6245
|
const e = JSON.parse(line);
|
|
@@ -6419,14 +6609,14 @@ function runCheck(opts) {
|
|
|
6419
6609
|
}
|
|
6420
6610
|
|
|
6421
6611
|
// src/cli/hook.ts
|
|
6422
|
-
import { readFileSync as
|
|
6612
|
+
import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
|
|
6423
6613
|
import { createHash as createHash4 } from "node:crypto";
|
|
6424
6614
|
|
|
6425
6615
|
// src/adapters/claude/changes.ts
|
|
6426
6616
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
6427
6617
|
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
6428
6618
|
import { tmpdir } from "node:os";
|
|
6429
|
-
import { isAbsolute as isAbsolute5, join as
|
|
6619
|
+
import { isAbsolute as isAbsolute5, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
|
|
6430
6620
|
function asStr(v) {
|
|
6431
6621
|
return typeof v === "string" ? v : "";
|
|
6432
6622
|
}
|
|
@@ -6451,11 +6641,11 @@ function synthFileChange(displayPath, before, after) {
|
|
|
6451
6641
|
if (before === null) op = "add";
|
|
6452
6642
|
else if (after === null) op = "delete";
|
|
6453
6643
|
else op = "modify";
|
|
6454
|
-
const dir = mkdtempSync(
|
|
6644
|
+
const dir = mkdtempSync(join7(tmpdir(), "hf-"));
|
|
6455
6645
|
let raw = "";
|
|
6456
6646
|
try {
|
|
6457
|
-
const a =
|
|
6458
|
-
const b =
|
|
6647
|
+
const a = join7(dir, "a");
|
|
6648
|
+
const b = join7(dir, "b");
|
|
6459
6649
|
writeFileSync(a, before ?? "");
|
|
6460
6650
|
writeFileSync(b, after ?? "");
|
|
6461
6651
|
try {
|
|
@@ -6540,14 +6730,14 @@ function formatDenial(blocks) {
|
|
|
6540
6730
|
}
|
|
6541
6731
|
|
|
6542
6732
|
// src/session.ts
|
|
6543
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as
|
|
6544
|
-
import { dirname as dirname3, join as
|
|
6733
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6734
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
6545
6735
|
var UNSAFE = /[^A-Za-z0-9_-]/g;
|
|
6546
6736
|
var SHA = /^[0-9a-f]{7,40}$/;
|
|
6547
6737
|
function baselinePath(cwd, sessionId) {
|
|
6548
6738
|
const gd = gitDir(cwd);
|
|
6549
6739
|
if (!gd) return null;
|
|
6550
|
-
return
|
|
6740
|
+
return join8(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
|
|
6551
6741
|
}
|
|
6552
6742
|
function turnBaseline(cwd, sessionId) {
|
|
6553
6743
|
if (!sessionId) return null;
|
|
@@ -6555,7 +6745,7 @@ function turnBaseline(cwd, sessionId) {
|
|
|
6555
6745
|
const p = baselinePath(cwd, sessionId);
|
|
6556
6746
|
if (!p) return null;
|
|
6557
6747
|
if (existsSync5(p)) {
|
|
6558
|
-
const v =
|
|
6748
|
+
const v = readFileSync6(p, "utf8").trim();
|
|
6559
6749
|
if (SHA.test(v)) return v;
|
|
6560
6750
|
}
|
|
6561
6751
|
const head = headSha(cwd);
|
|
@@ -6581,8 +6771,8 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
6581
6771
|
|
|
6582
6772
|
// src/effect.ts
|
|
6583
6773
|
import { createHash as createHash2 } from "node:crypto";
|
|
6584
|
-
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as
|
|
6585
|
-
import { isAbsolute as isAbsolute6, join as
|
|
6774
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6775
|
+
import { isAbsolute as isAbsolute6, join as join9 } from "node:path";
|
|
6586
6776
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
6587
6777
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
6588
6778
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
@@ -6594,7 +6784,7 @@ function gitDir2(cwd) {
|
|
|
6594
6784
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6595
6785
|
}).trim();
|
|
6596
6786
|
if (!d) return null;
|
|
6597
|
-
return isAbsolute6(d) ? d :
|
|
6787
|
+
return isAbsolute6(d) ? d : join9(cwd, d);
|
|
6598
6788
|
} catch {
|
|
6599
6789
|
return null;
|
|
6600
6790
|
}
|
|
@@ -6603,8 +6793,8 @@ function statePath(cwd, sessionId, kind) {
|
|
|
6603
6793
|
if (!sessionId) return null;
|
|
6604
6794
|
const gd = gitDir2(cwd);
|
|
6605
6795
|
if (!gd) return null;
|
|
6606
|
-
const dir =
|
|
6607
|
-
return
|
|
6796
|
+
const dir = join9(gd, "tamperward");
|
|
6797
|
+
return join9(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
|
|
6608
6798
|
}
|
|
6609
6799
|
function ptreePath(cwd, sessionId) {
|
|
6610
6800
|
return statePath(cwd, sessionId, "ptree");
|
|
@@ -6633,7 +6823,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6633
6823
|
} catch {
|
|
6634
6824
|
}
|
|
6635
6825
|
const walk2 = (rel) => {
|
|
6636
|
-
const abs2 = rel ?
|
|
6826
|
+
const abs2 = rel ? join9(cwd, rel) : cwd;
|
|
6637
6827
|
let names;
|
|
6638
6828
|
try {
|
|
6639
6829
|
names = readdirSync(abs2);
|
|
@@ -6646,7 +6836,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6646
6836
|
if (pruned.has(r)) continue;
|
|
6647
6837
|
let st;
|
|
6648
6838
|
try {
|
|
6649
|
-
st = lstatSync2(
|
|
6839
|
+
st = lstatSync2(join9(cwd, r));
|
|
6650
6840
|
} catch {
|
|
6651
6841
|
continue;
|
|
6652
6842
|
}
|
|
@@ -6675,7 +6865,7 @@ function driftBetween(expected, current) {
|
|
|
6675
6865
|
function loadTree(p) {
|
|
6676
6866
|
if (!p || !existsSync6(p)) return null;
|
|
6677
6867
|
try {
|
|
6678
|
-
return JSON.parse(
|
|
6868
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
6679
6869
|
} catch {
|
|
6680
6870
|
return null;
|
|
6681
6871
|
}
|
|
@@ -6683,7 +6873,7 @@ function loadTree(p) {
|
|
|
6683
6873
|
function saveTree(p, tree) {
|
|
6684
6874
|
if (!p) return;
|
|
6685
6875
|
try {
|
|
6686
|
-
mkdirSync3(
|
|
6876
|
+
mkdirSync3(join9(p, ".."), { recursive: true });
|
|
6687
6877
|
const tmp = `${p}.${process.pid}.tmp`;
|
|
6688
6878
|
writeFileSync3(tmp, JSON.stringify(tree));
|
|
6689
6879
|
renameSync(tmp, p);
|
|
@@ -6715,7 +6905,7 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
6715
6905
|
|
|
6716
6906
|
// src/cli/watch.ts
|
|
6717
6907
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
|
|
6718
|
-
import { join as
|
|
6908
|
+
import { join as join10 } from "node:path";
|
|
6719
6909
|
import { createHash as createHash3 } from "node:crypto";
|
|
6720
6910
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
6721
6911
|
function snap(abs2) {
|
|
@@ -6727,7 +6917,7 @@ function snap(abs2) {
|
|
|
6727
6917
|
}
|
|
6728
6918
|
function defaultEventLog(cwd) {
|
|
6729
6919
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
6730
|
-
return
|
|
6920
|
+
return join10(gitDir(cwd) ?? join10(cwd, ".git"), "tamperward", "fsevents.jsonl");
|
|
6731
6921
|
}
|
|
6732
6922
|
function watchTree(dir, cb) {
|
|
6733
6923
|
if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
|
|
@@ -6744,12 +6934,12 @@ function watchTree(dir, cb) {
|
|
|
6744
6934
|
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
6745
6935
|
let w;
|
|
6746
6936
|
try {
|
|
6747
|
-
w = watch(rel ?
|
|
6937
|
+
w = watch(rel ? join10(dir, rel) : dir, (kind, fname) => {
|
|
6748
6938
|
if (!fname) return;
|
|
6749
6939
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
6750
6940
|
cb(kind, child);
|
|
6751
6941
|
try {
|
|
6752
|
-
if (statSync(
|
|
6942
|
+
if (statSync(join10(dir, child)).isDirectory()) addDir(child);
|
|
6753
6943
|
} catch {
|
|
6754
6944
|
}
|
|
6755
6945
|
});
|
|
@@ -6762,7 +6952,7 @@ function watchTree(dir, cb) {
|
|
|
6762
6952
|
addDir(rel);
|
|
6763
6953
|
let names = [];
|
|
6764
6954
|
try {
|
|
6765
|
-
names = readdirSync2(rel ?
|
|
6955
|
+
names = readdirSync2(rel ? join10(dir, rel) : dir);
|
|
6766
6956
|
} catch {
|
|
6767
6957
|
return;
|
|
6768
6958
|
}
|
|
@@ -6770,7 +6960,7 @@ function watchTree(dir, cb) {
|
|
|
6770
6960
|
const child = rel ? `${rel}/${name}` : name;
|
|
6771
6961
|
if (SKIP2.test(child + "/")) continue;
|
|
6772
6962
|
try {
|
|
6773
|
-
if (statSync(
|
|
6963
|
+
if (statSync(join10(dir, child)).isDirectory()) walk2(child);
|
|
6774
6964
|
} catch {
|
|
6775
6965
|
}
|
|
6776
6966
|
}
|
|
@@ -6782,13 +6972,13 @@ function watchTree(dir, cb) {
|
|
|
6782
6972
|
}
|
|
6783
6973
|
function startWatcher(dir, log, policy) {
|
|
6784
6974
|
try {
|
|
6785
|
-
mkdirSync4(
|
|
6975
|
+
mkdirSync4(join10(log, ".."), { recursive: true });
|
|
6786
6976
|
} catch {
|
|
6787
6977
|
}
|
|
6788
6978
|
const last = /* @__PURE__ */ new Map();
|
|
6789
6979
|
return watchTree(dir, (kind, rel) => {
|
|
6790
6980
|
if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
|
|
6791
|
-
const s = snap(
|
|
6981
|
+
const s = snap(join10(dir, rel));
|
|
6792
6982
|
const key2 = `${s.hash}:${s.mode}`;
|
|
6793
6983
|
if (last.get(rel) === key2) return;
|
|
6794
6984
|
last.set(rel, key2);
|
|
@@ -6815,7 +7005,7 @@ function runWatch(args) {
|
|
|
6815
7005
|
}
|
|
6816
7006
|
|
|
6817
7007
|
// src/detectors/fs-events.ts
|
|
6818
|
-
import { readFileSync as
|
|
7008
|
+
import { readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
|
|
6819
7009
|
var RULE11 = "transient-protected-mutation";
|
|
6820
7010
|
function readEvents(log, offset) {
|
|
6821
7011
|
let size = 0;
|
|
@@ -6825,7 +7015,7 @@ function readEvents(log, offset) {
|
|
|
6825
7015
|
return { events: [], newOffset: offset };
|
|
6826
7016
|
}
|
|
6827
7017
|
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
6828
|
-
const raw =
|
|
7018
|
+
const raw = readFileSync9(log, "utf8").slice(offset);
|
|
6829
7019
|
const events = [];
|
|
6830
7020
|
for (const line of raw.split("\n")) {
|
|
6831
7021
|
if (!line.trim()) continue;
|
|
@@ -6878,7 +7068,7 @@ var HookInputError = class extends Error {
|
|
|
6878
7068
|
};
|
|
6879
7069
|
function readStdin() {
|
|
6880
7070
|
try {
|
|
6881
|
-
return
|
|
7071
|
+
return readFileSync10(0, "utf8");
|
|
6882
7072
|
} catch (e) {
|
|
6883
7073
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
6884
7074
|
}
|
|
@@ -7134,7 +7324,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7134
7324
|
if (!existsSync7(log)) return none;
|
|
7135
7325
|
let offset = 0;
|
|
7136
7326
|
if (cp && existsSync7(cp)) {
|
|
7137
|
-
const n = Number(
|
|
7327
|
+
const n = Number(readFileSync10(cp, "utf8"));
|
|
7138
7328
|
offset = Number.isFinite(n) ? n : 0;
|
|
7139
7329
|
}
|
|
7140
7330
|
const { events, newOffset } = readEvents(log, offset);
|
|
@@ -7287,8 +7477,8 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7287
7477
|
// src/cli/init.ts
|
|
7288
7478
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7289
7479
|
import { createHash as createHash5 } from "node:crypto";
|
|
7290
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as
|
|
7291
|
-
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute7, join as
|
|
7480
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
7481
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute7, join as join11, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7292
7482
|
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
7293
7483
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7294
7484
|
function gitConfig(cwd, key2) {
|
|
@@ -7464,7 +7654,7 @@ function hooksShapeError(hooks) {
|
|
|
7464
7654
|
return null;
|
|
7465
7655
|
}
|
|
7466
7656
|
function planPolicy(cwd) {
|
|
7467
|
-
const path =
|
|
7657
|
+
const path = join11(cwd, POLICY_FILE);
|
|
7468
7658
|
if (existsSync8(path)) {
|
|
7469
7659
|
try {
|
|
7470
7660
|
loadPolicy(cwd);
|
|
@@ -7483,12 +7673,12 @@ function planPolicy(cwd) {
|
|
|
7483
7673
|
}
|
|
7484
7674
|
function planClaudeHooks(cwd) {
|
|
7485
7675
|
const rel = ".claude/settings.json";
|
|
7486
|
-
const path =
|
|
7676
|
+
const path = join11(cwd, rel);
|
|
7487
7677
|
let settings = {};
|
|
7488
7678
|
if (existsSync8(path)) {
|
|
7489
7679
|
let parsed;
|
|
7490
7680
|
try {
|
|
7491
|
-
parsed = JSON.parse(
|
|
7681
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7492
7682
|
} catch {
|
|
7493
7683
|
return {
|
|
7494
7684
|
item: "agent",
|
|
@@ -7584,7 +7774,7 @@ function hooksDir(cwd) {
|
|
|
7584
7774
|
if (out2) return resolve3(cwd, out2);
|
|
7585
7775
|
} catch {
|
|
7586
7776
|
}
|
|
7587
|
-
return existsSync8(
|
|
7777
|
+
return existsSync8(join11(cwd, ".git")) ? join11(cwd, ".git", "hooks") : null;
|
|
7588
7778
|
}
|
|
7589
7779
|
function display(cwd, path) {
|
|
7590
7780
|
const rel = relative3(cwd, path);
|
|
@@ -7611,30 +7801,30 @@ function planPreCommit(cwd) {
|
|
|
7611
7801
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
7612
7802
|
let path;
|
|
7613
7803
|
let note;
|
|
7614
|
-
if (existsSync8(
|
|
7615
|
-
path =
|
|
7804
|
+
if (existsSync8(join11(cwd, ".husky"))) {
|
|
7805
|
+
path = join11(cwd, ".husky", "pre-commit");
|
|
7616
7806
|
note = "husky";
|
|
7617
7807
|
} else {
|
|
7618
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(
|
|
7808
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join11(cwd, c))));
|
|
7619
7809
|
if (managed) {
|
|
7620
7810
|
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}`);
|
|
7621
7811
|
}
|
|
7622
7812
|
const dir = hooksDir(cwd);
|
|
7623
7813
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
7624
7814
|
if (basename2(dir) === "_" && basename2(dirname4(dir)) === ".husky") {
|
|
7625
|
-
path =
|
|
7815
|
+
path = join11(dirname4(dir), "pre-commit");
|
|
7626
7816
|
note = "husky";
|
|
7627
7817
|
} else {
|
|
7628
|
-
path =
|
|
7818
|
+
path = join11(dir, "pre-commit");
|
|
7629
7819
|
const rel2 = display(cwd, dir);
|
|
7630
|
-
note = rel2 ===
|
|
7820
|
+
note = rel2 === join11(".git", "hooks") ? "plain git hook (local-only: .git/hooks is not committed \u2014 consider husky to share it)" : `git hooks directory ${rel2} (core.hooksPath)`;
|
|
7631
7821
|
}
|
|
7632
7822
|
}
|
|
7633
7823
|
const rel = display(cwd, path);
|
|
7634
7824
|
if (existsSync8(path) && !statSync3(path).isFile()) {
|
|
7635
7825
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
7636
7826
|
}
|
|
7637
|
-
const existing = existsSync8(path) ?
|
|
7827
|
+
const existing = existsSync8(path) ? readFileSync11(path, "utf8") : null;
|
|
7638
7828
|
if (existing === null) {
|
|
7639
7829
|
return {
|
|
7640
7830
|
item: "pre-commit",
|
|
@@ -7723,10 +7913,10 @@ ${block.join("\n")}
|
|
|
7723
7913
|
function planCodeowners(cwd) {
|
|
7724
7914
|
const rel = ".github/CODEOWNERS";
|
|
7725
7915
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
7726
|
-
(r) => existsSync8(
|
|
7916
|
+
(r) => existsSync8(join11(cwd, r))
|
|
7727
7917
|
);
|
|
7728
|
-
const path =
|
|
7729
|
-
const existing = existingRel ?
|
|
7918
|
+
const path = join11(cwd, existingRel ?? rel);
|
|
7919
|
+
const existing = existingRel ? readFileSync11(path, "utf8") : null;
|
|
7730
7920
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
7731
7921
|
if (missing.length === 0) {
|
|
7732
7922
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -7759,7 +7949,7 @@ ${CODEOWNERS_MARK}
|
|
|
7759
7949
|
}
|
|
7760
7950
|
function planWorkflow(cwd, force) {
|
|
7761
7951
|
const rel = ".github/workflows/tamperward.yml";
|
|
7762
|
-
const path =
|
|
7952
|
+
const path = join11(cwd, rel);
|
|
7763
7953
|
const write = () => {
|
|
7764
7954
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7765
7955
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
@@ -7775,7 +7965,7 @@ function planWorkflow(cwd, force) {
|
|
|
7775
7965
|
}
|
|
7776
7966
|
let src;
|
|
7777
7967
|
try {
|
|
7778
|
-
src =
|
|
7968
|
+
src = readFileSync11(path, "utf8");
|
|
7779
7969
|
} catch {
|
|
7780
7970
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
7781
7971
|
}
|
|
@@ -7874,15 +8064,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
7874
8064
|
// src/cli/verify.ts
|
|
7875
8065
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
7876
8066
|
import { createHash as createHash8 } from "node:crypto";
|
|
7877
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
8067
|
+
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, readlinkSync as readlinkSync3, rmSync as rmSync3, statSync as statSync5, symlinkSync, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "node:fs";
|
|
7878
8068
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
7879
|
-
import { dirname as dirname5, join as
|
|
8069
|
+
import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
|
|
7880
8070
|
|
|
7881
8071
|
// src/git/trusted.ts
|
|
7882
8072
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7883
8073
|
import { createHash as createHash6 } from "node:crypto";
|
|
7884
|
-
import { existsSync as existsSync9, readFileSync as
|
|
7885
|
-
import { isAbsolute as isAbsolute8, join as
|
|
8074
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
8075
|
+
import { isAbsolute as isAbsolute8, join as join12 } from "node:path";
|
|
7886
8076
|
function trustedGitEnv() {
|
|
7887
8077
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
7888
8078
|
}
|
|
@@ -7895,7 +8085,7 @@ function gitDirOf(cwd) {
|
|
|
7895
8085
|
env: trustedGitEnv()
|
|
7896
8086
|
}).trim();
|
|
7897
8087
|
if (!d) return null;
|
|
7898
|
-
return isAbsolute8(d) ? d :
|
|
8088
|
+
return isAbsolute8(d) ? d : join12(cwd, d);
|
|
7899
8089
|
} catch {
|
|
7900
8090
|
return null;
|
|
7901
8091
|
}
|
|
@@ -7920,9 +8110,9 @@ function objectRewriteState(cwd) {
|
|
|
7920
8110
|
parts.push(`${rel}:<no-git-dir>`);
|
|
7921
8111
|
continue;
|
|
7922
8112
|
}
|
|
7923
|
-
const p =
|
|
8113
|
+
const p = join12(dir, rel);
|
|
7924
8114
|
try {
|
|
7925
|
-
parts.push(`${rel}:` + (existsSync9(p) ?
|
|
8115
|
+
parts.push(`${rel}:` + (existsSync9(p) ? readFileSync12(p, "utf8").trim() : ""));
|
|
7926
8116
|
} catch {
|
|
7927
8117
|
parts.push(`${rel}:<unreadable>`);
|
|
7928
8118
|
}
|
|
@@ -7933,8 +8123,8 @@ function objectRewriteState(cwd) {
|
|
|
7933
8123
|
// src/fingerprint.ts
|
|
7934
8124
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7935
8125
|
import { createHash as createHash7 } from "node:crypto";
|
|
7936
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
7937
|
-
import { join as
|
|
8126
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
8127
|
+
import { join as join13 } from "node:path";
|
|
7938
8128
|
function treeFingerprint(cwd) {
|
|
7939
8129
|
const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
7940
8130
|
cwd,
|
|
@@ -7952,7 +8142,7 @@ function treeFingerprint(cwd) {
|
|
|
7952
8142
|
return h.digest("hex");
|
|
7953
8143
|
}
|
|
7954
8144
|
function depsFingerprint(cwd) {
|
|
7955
|
-
const root =
|
|
8145
|
+
const root = join13(cwd, "node_modules");
|
|
7956
8146
|
const h = createHash7("sha256");
|
|
7957
8147
|
const walk2 = (dir) => {
|
|
7958
8148
|
let entries;
|
|
@@ -7962,12 +8152,12 @@ function depsFingerprint(cwd) {
|
|
|
7962
8152
|
return;
|
|
7963
8153
|
}
|
|
7964
8154
|
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7965
|
-
const p =
|
|
8155
|
+
const p = join13(dir, e.name);
|
|
7966
8156
|
if (e.isDirectory()) walk2(p);
|
|
7967
8157
|
else if (e.isFile()) {
|
|
7968
8158
|
h.update(p);
|
|
7969
8159
|
try {
|
|
7970
|
-
h.update(
|
|
8160
|
+
h.update(readFileSync13(p));
|
|
7971
8161
|
h.update(String(statSync4(p).mode));
|
|
7972
8162
|
} catch {
|
|
7973
8163
|
h.update("<unreadable>");
|
|
@@ -8082,7 +8272,7 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
8082
8272
|
function materialize(cwd, dest) {
|
|
8083
8273
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8084
8274
|
for (const rel of listed) {
|
|
8085
|
-
const src =
|
|
8275
|
+
const src = join14(cwd, rel);
|
|
8086
8276
|
let st;
|
|
8087
8277
|
try {
|
|
8088
8278
|
st = statSync5(src);
|
|
@@ -8090,18 +8280,18 @@ function materialize(cwd, dest) {
|
|
|
8090
8280
|
continue;
|
|
8091
8281
|
}
|
|
8092
8282
|
if (!st.isFile()) continue;
|
|
8093
|
-
const out2 =
|
|
8283
|
+
const out2 = join14(dest, rel);
|
|
8094
8284
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8095
8285
|
dropSymlink(out2);
|
|
8096
8286
|
if (lstatSync3(src).isSymbolicLink()) {
|
|
8097
|
-
writeFileSync6(out2,
|
|
8287
|
+
writeFileSync6(out2, readFileSync14(src));
|
|
8098
8288
|
} else {
|
|
8099
8289
|
cpSync(src, out2, { dereference: true });
|
|
8100
8290
|
}
|
|
8101
8291
|
chmodSync2(out2, st.mode);
|
|
8102
8292
|
}
|
|
8103
8293
|
const nm = resolve4(cwd, "node_modules");
|
|
8104
|
-
if (existsSync10(nm)) symlinkSync(nm,
|
|
8294
|
+
if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
|
|
8105
8295
|
}
|
|
8106
8296
|
function dropSymlink(p) {
|
|
8107
8297
|
try {
|
|
@@ -8134,11 +8324,11 @@ function overlayDigest(dest, paths) {
|
|
|
8134
8324
|
for (const rel of [...paths].sort()) {
|
|
8135
8325
|
h.update(rel);
|
|
8136
8326
|
h.update("\0");
|
|
8137
|
-
const p =
|
|
8327
|
+
const p = join14(dest, rel);
|
|
8138
8328
|
try {
|
|
8139
8329
|
const st = lstatSync3(p);
|
|
8140
8330
|
h.update(String(st.mode));
|
|
8141
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
8331
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
|
|
8142
8332
|
} catch {
|
|
8143
8333
|
h.update("<gone>");
|
|
8144
8334
|
}
|
|
@@ -8161,7 +8351,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8161
8351
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
8162
8352
|
}
|
|
8163
8353
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8164
|
-
const out2 =
|
|
8354
|
+
const out2 = join14(dest, e.path);
|
|
8165
8355
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8166
8356
|
rmSync3(out2, { force: true });
|
|
8167
8357
|
if (e.mode === "120000") {
|
|
@@ -8176,7 +8366,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8176
8366
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8177
8367
|
for (const rel of inCopy) {
|
|
8178
8368
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
8179
|
-
rmSync3(
|
|
8369
|
+
rmSync3(join14(dest, rel), { force: true });
|
|
8180
8370
|
removed++;
|
|
8181
8371
|
}
|
|
8182
8372
|
return { restored, removed };
|
|
@@ -8218,8 +8408,8 @@ function suiteEnv(scratch) {
|
|
|
8218
8408
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
8219
8409
|
env[k] = v;
|
|
8220
8410
|
}
|
|
8221
|
-
const userRc =
|
|
8222
|
-
const globalRc =
|
|
8411
|
+
const userRc = join14(scratch, "npmrc-user");
|
|
8412
|
+
const globalRc = join14(scratch, "npmrc-global");
|
|
8223
8413
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
8224
8414
|
env.npm_config_userconfig = userRc;
|
|
8225
8415
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -8229,8 +8419,8 @@ function suiteEnv(scratch) {
|
|
|
8229
8419
|
}
|
|
8230
8420
|
function runSuite(dir, cmd, budgetSecs) {
|
|
8231
8421
|
const t0 = Date.now();
|
|
8232
|
-
const outDir = mkdtempSync2(
|
|
8233
|
-
const outFile =
|
|
8422
|
+
const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
|
|
8423
|
+
const outFile = join14(outDir, "result.json");
|
|
8234
8424
|
try {
|
|
8235
8425
|
spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
8236
8426
|
cwd: dir,
|
|
@@ -8243,7 +8433,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
8243
8433
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8244
8434
|
let r;
|
|
8245
8435
|
try {
|
|
8246
|
-
r = JSON.parse(
|
|
8436
|
+
r = JSON.parse(readFileSync14(outFile, "utf8"));
|
|
8247
8437
|
} catch {
|
|
8248
8438
|
return { exit: null, secs };
|
|
8249
8439
|
}
|
|
@@ -8294,8 +8484,8 @@ function runVerify(opts) {
|
|
|
8294
8484
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
8295
8485
|
return 2;
|
|
8296
8486
|
}
|
|
8297
|
-
const visRoot = mkdtempSync2(
|
|
8298
|
-
const visDir =
|
|
8487
|
+
const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
|
|
8488
|
+
const visDir = join14(visRoot, "t");
|
|
8299
8489
|
const cleanup = (dirs) => {
|
|
8300
8490
|
if (opts.keep) return;
|
|
8301
8491
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
@@ -8323,8 +8513,8 @@ function runVerify(opts) {
|
|
|
8323
8513
|
out2("pristine run executes through it too. Failing closed, not open.");
|
|
8324
8514
|
return 2;
|
|
8325
8515
|
}
|
|
8326
|
-
const priRoot = mkdtempSync2(
|
|
8327
|
-
const priDir =
|
|
8516
|
+
const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
|
|
8517
|
+
const priDir = join14(priRoot, "t");
|
|
8328
8518
|
let restored = [];
|
|
8329
8519
|
let removedAdded = 0;
|
|
8330
8520
|
try {
|
|
@@ -8436,7 +8626,7 @@ function parseVerify(args) {
|
|
|
8436
8626
|
|
|
8437
8627
|
// src/cli/run.ts
|
|
8438
8628
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8439
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
8629
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync as realpathSync3 } from "node:fs";
|
|
8440
8630
|
import { resolve as resolve5 } from "node:path";
|
|
8441
8631
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8442
8632
|
var err = (s) => process.stderr.write(s + "\n");
|
|
@@ -8445,7 +8635,7 @@ function git3(args, cwd) {
|
|
|
8445
8635
|
}
|
|
8446
8636
|
function nowTicks() {
|
|
8447
8637
|
try {
|
|
8448
|
-
return Math.floor(parseFloat(
|
|
8638
|
+
return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
8449
8639
|
} catch {
|
|
8450
8640
|
return Number.POSITIVE_INFINITY;
|
|
8451
8641
|
}
|
|
@@ -8475,7 +8665,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8475
8665
|
}
|
|
8476
8666
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
8477
8667
|
try {
|
|
8478
|
-
const stat =
|
|
8668
|
+
const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
|
|
8479
8669
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
8480
8670
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
8481
8671
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.3",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|