tamperward 2.10.1 → 2.10.2
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 +292 -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,104 @@ 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 === "--collect-only" || o === "--co") ignore.push({ pattern: "**", key: o });
|
|
5141
|
+
}
|
|
5142
|
+
sel.ignore = ignore;
|
|
5143
|
+
return sel;
|
|
5144
|
+
}
|
|
5009
5145
|
var isWorkspaceFile = (path) => /^vitest\.workspace\./.test(path.split("/").pop() ?? path);
|
|
5010
5146
|
var empty = () => ({ include: null, regex: null, ignore: null, roots: null, base: null, projects: null, opaque: false, present: false });
|
|
5011
5147
|
var stripRootDir = (g) => g.replace(/^<rootDir>\/?/, "");
|
|
@@ -5154,6 +5290,7 @@ function workspaceArray(sf) {
|
|
|
5154
5290
|
}
|
|
5155
5291
|
function parseSelection(src, runner, path = "") {
|
|
5156
5292
|
let sel = empty();
|
|
5293
|
+
if (runner === "pytest") return pytestSelection(src);
|
|
5157
5294
|
try {
|
|
5158
5295
|
const sf = ts3.createSourceFile("cfg.ts", asExpression2(src), ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TS);
|
|
5159
5296
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
@@ -5202,10 +5339,14 @@ function predicate(sel, runner) {
|
|
|
5202
5339
|
why: (p) => ps.length ? `no project selects it (${ps[0].why(p)})` : "the projects list is empty"
|
|
5203
5340
|
};
|
|
5204
5341
|
}
|
|
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 :
|
|
5342
|
+
const includeList = sel.include ?? (runner === "jest" ? sel.regex ? null : JEST_DEFAULT_MATCH : runner === "pytest" ? PYTEST_DEFAULT_INCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5343
|
+
const ignoreList = sel.ignore ?? (runner === "jest" ? JEST_DEFAULT_IGNORE : runner === "pytest" ? PYTEST_DEFAULT_IGNORE : VITEST_DEFAULT_EXCLUDE).map((pattern) => ({
|
|
5344
|
+
pattern,
|
|
5345
|
+
key: runner === "jest" ? "testPathIgnorePatterns" : runner === "pytest" ? "norecursedirs" : "test.exclude"
|
|
5346
|
+
}));
|
|
5347
|
+
const blockWide = ignoreList.filter((e) => e.pattern.startsWith("\0"));
|
|
5207
5348
|
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));
|
|
5349
|
+
const ignoredBy = (p) => blockWide[0] ?? ignoreList.filter((e) => !e.pattern.startsWith("\0")).find((e) => (runner === "jest" ? regexes([e.pattern]) : globs3([e.pattern]))(p));
|
|
5209
5350
|
const roots = sel.roots?.map((r) => r.replace(/^\.\//, "").replace(/\/+$/, "")).map((r) => r === "." ? "" : r) ?? null;
|
|
5210
5351
|
const rebase = (p) => sel.base === null ? p : underDir(p, sel.base) ? p.slice(sel.base.length + 1) : null;
|
|
5211
5352
|
const inRoots = (p) => roots === null || roots.some((r) => underDir(p, r));
|
|
@@ -5220,7 +5361,10 @@ function predicate(sel, runner) {
|
|
|
5220
5361
|
if (r === null) return `${baseKey} no longer covers it (${JSON.stringify(sel.base)})`;
|
|
5221
5362
|
if (!inRoots(r)) return `roots no longer covers it (${JSON.stringify(sel.roots)})`;
|
|
5222
5363
|
const hit = ignoredBy(r);
|
|
5223
|
-
if (hit)
|
|
5364
|
+
if (hit) {
|
|
5365
|
+
const pat = hit.pattern.startsWith("\0") ? hit.pattern.slice(1) : hit.pattern;
|
|
5366
|
+
return `${hit.key === "exclude" ? "test.exclude" : hit.key} now matches it (${JSON.stringify(pat)})`;
|
|
5367
|
+
}
|
|
5224
5368
|
const key2 = runner === "jest" ? sel.include ? "testMatch" : sel.regex ? "testRegex" : "testMatch (default)" : sel.include ? "test.include" : "test.include (default)";
|
|
5225
5369
|
return `${key2} no longer matches it (${JSON.stringify(sel.include ?? sel.regex ?? includeList ?? [])})`;
|
|
5226
5370
|
}
|
|
@@ -5370,12 +5514,14 @@ function significantLines(src, path) {
|
|
|
5370
5514
|
return out2;
|
|
5371
5515
|
}
|
|
5372
5516
|
var OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
|
|
5373
|
-
function runnerSamples(configPath, policy, ctx) {
|
|
5517
|
+
function runnerSamples(configPath, policy, ctx, runner) {
|
|
5518
|
+
const lang = runner === "pytest" ? "py" : "js";
|
|
5519
|
+
const fallback = runner === "pytest" ? PYTEST_CANONICAL_SAMPLES : CANONICAL_SAMPLES;
|
|
5374
5520
|
const files = trackedFiles(ctx);
|
|
5375
|
-
if (!files) return
|
|
5521
|
+
if (!files) return fallback;
|
|
5376
5522
|
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 :
|
|
5523
|
+
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));
|
|
5524
|
+
return own.length ? own : fallback;
|
|
5379
5525
|
}
|
|
5380
5526
|
var WHOLE_SUITE_SCRIPT = /^test(?::ci)?$/;
|
|
5381
5527
|
function excludesOtherRunner(flag, cmd) {
|
|
@@ -5496,7 +5642,9 @@ var testDeletion = {
|
|
|
5496
5642
|
);
|
|
5497
5643
|
}
|
|
5498
5644
|
} else if (c.op !== "delete" && c.after != null && !isTest && isProtected(c.path, policy, "config") && runnerOf(c.path, c.after)) {
|
|
5499
|
-
|
|
5645
|
+
const rn = runnerOf(c.path, c.after);
|
|
5646
|
+
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
5647
|
+
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
5500
5648
|
out2.push(
|
|
5501
5649
|
makeFinding(RULE7, policy, {
|
|
5502
5650
|
file: c.path,
|
|
@@ -5606,9 +5754,48 @@ var testDeletion = {
|
|
|
5606
5754
|
}
|
|
5607
5755
|
}
|
|
5608
5756
|
}
|
|
5757
|
+
out2.push(...pytestPrecedenceFindings(changes, policy, ctx));
|
|
5609
5758
|
return out2;
|
|
5610
5759
|
}
|
|
5611
5760
|
};
|
|
5761
|
+
function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
5762
|
+
const order = PYTEST_INI_ORDER;
|
|
5763
|
+
const touched = /* @__PURE__ */ new Map();
|
|
5764
|
+
for (const c of changes) {
|
|
5765
|
+
if (c.kind !== "file" || c.oldPath) continue;
|
|
5766
|
+
if (order.includes(c.path)) touched.set(c.path, c);
|
|
5767
|
+
}
|
|
5768
|
+
if (touched.size === 0) return [];
|
|
5769
|
+
const listing = trackedFiles(ctx);
|
|
5770
|
+
if (!listing) return [];
|
|
5771
|
+
const rootNames = new Set(listing.filter((f) => !f.includes("/")));
|
|
5772
|
+
const base = /* @__PURE__ */ new Map();
|
|
5773
|
+
const head = /* @__PURE__ */ new Map();
|
|
5774
|
+
for (const name of order) {
|
|
5775
|
+
const c = touched.get(name);
|
|
5776
|
+
if (c) {
|
|
5777
|
+
base.set(name, c.before);
|
|
5778
|
+
head.set(name, c.after);
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
if (!rootNames.has(name)) continue;
|
|
5782
|
+
const src = trackedContent(name, ctx);
|
|
5783
|
+
base.set(name, src);
|
|
5784
|
+
head.set(name, src);
|
|
5785
|
+
}
|
|
5786
|
+
const was = effectivePytestFile(base);
|
|
5787
|
+
const now = effectivePytestFile(head);
|
|
5788
|
+
if (!was || !now || was.path === now.path) return [];
|
|
5789
|
+
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
5790
|
+
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
5791
|
+
(n) => makeFinding(RULE7, policy, {
|
|
5792
|
+
file: now.path,
|
|
5793
|
+
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
5794
|
+
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
5795
|
+
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."
|
|
5796
|
+
})
|
|
5797
|
+
);
|
|
5798
|
+
}
|
|
5612
5799
|
|
|
5613
5800
|
// src/detectors/test-content-removal.ts
|
|
5614
5801
|
import ts5 from "typescript";
|
|
@@ -5894,8 +6081,8 @@ function hasBlocking(findings) {
|
|
|
5894
6081
|
}
|
|
5895
6082
|
|
|
5896
6083
|
// src/policy-load.ts
|
|
5897
|
-
import { readFileSync as
|
|
5898
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6084
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "node:fs";
|
|
6085
|
+
import { isAbsolute as isAbsolute4, join as join5, posix as posix2 } from "node:path";
|
|
5899
6086
|
import { parse as parse2 } from "yaml";
|
|
5900
6087
|
var PolicyError = class extends Error {
|
|
5901
6088
|
};
|
|
@@ -6019,9 +6206,9 @@ function parseOrThrow(src, where) {
|
|
|
6019
6206
|
return parsePolicy(raw, where);
|
|
6020
6207
|
}
|
|
6021
6208
|
function loadPolicy(cwd = process.cwd()) {
|
|
6022
|
-
const path =
|
|
6209
|
+
const path = join5(cwd, POLICY_FILE);
|
|
6023
6210
|
if (!existsSync3(path)) return defaultPolicy();
|
|
6024
|
-
return parseOrThrow(
|
|
6211
|
+
return parseOrThrow(readFileSync4(path, "utf8"), POLICY_FILE);
|
|
6025
6212
|
}
|
|
6026
6213
|
function loadPolicyAt(rev, cwd) {
|
|
6027
6214
|
const src = fileAt(rev, POLICY_FILE, { cwd });
|
|
@@ -6031,8 +6218,8 @@ function loadPolicyAt(rev, cwd) {
|
|
|
6031
6218
|
|
|
6032
6219
|
// src/signoff.ts
|
|
6033
6220
|
import { createHash } from "node:crypto";
|
|
6034
|
-
import { appendFileSync, mkdirSync, readFileSync as
|
|
6035
|
-
import { dirname as dirname2, join as
|
|
6221
|
+
import { appendFileSync, mkdirSync, readFileSync as readFileSync5, existsSync as existsSync4 } from "node:fs";
|
|
6222
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
6036
6223
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
6037
6224
|
function fingerprint(rule, file, evidence) {
|
|
6038
6225
|
return createHash("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
|
|
@@ -6043,13 +6230,13 @@ function ledgerPath(cwd, policy) {
|
|
|
6043
6230
|
if (!ledgerInsideRepo(rel)) {
|
|
6044
6231
|
throw new PolicyError(`signoff.ledger must be a relative path inside the repository, got ${JSON.stringify(rel)}`);
|
|
6045
6232
|
}
|
|
6046
|
-
return
|
|
6233
|
+
return join6(cwd, rel);
|
|
6047
6234
|
}
|
|
6048
6235
|
function readLedger(cwd, policy) {
|
|
6049
6236
|
const p = ledgerPath(cwd, policy);
|
|
6050
6237
|
if (!existsSync4(p)) return [];
|
|
6051
6238
|
const out2 = [];
|
|
6052
|
-
for (const line of
|
|
6239
|
+
for (const line of readFileSync5(p, "utf8").split("\n")) {
|
|
6053
6240
|
if (!line.trim()) continue;
|
|
6054
6241
|
try {
|
|
6055
6242
|
const e = JSON.parse(line);
|
|
@@ -6419,14 +6606,14 @@ function runCheck(opts) {
|
|
|
6419
6606
|
}
|
|
6420
6607
|
|
|
6421
6608
|
// src/cli/hook.ts
|
|
6422
|
-
import { readFileSync as
|
|
6609
|
+
import { readFileSync as readFileSync10, appendFileSync as appendFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7 } from "node:fs";
|
|
6423
6610
|
import { createHash as createHash4 } from "node:crypto";
|
|
6424
6611
|
|
|
6425
6612
|
// src/adapters/claude/changes.ts
|
|
6426
6613
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
6427
6614
|
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
6428
6615
|
import { tmpdir } from "node:os";
|
|
6429
|
-
import { isAbsolute as isAbsolute5, join as
|
|
6616
|
+
import { isAbsolute as isAbsolute5, join as join7, relative as relative2, resolve as resolve2 } from "node:path";
|
|
6430
6617
|
function asStr(v) {
|
|
6431
6618
|
return typeof v === "string" ? v : "";
|
|
6432
6619
|
}
|
|
@@ -6451,11 +6638,11 @@ function synthFileChange(displayPath, before, after) {
|
|
|
6451
6638
|
if (before === null) op = "add";
|
|
6452
6639
|
else if (after === null) op = "delete";
|
|
6453
6640
|
else op = "modify";
|
|
6454
|
-
const dir = mkdtempSync(
|
|
6641
|
+
const dir = mkdtempSync(join7(tmpdir(), "hf-"));
|
|
6455
6642
|
let raw = "";
|
|
6456
6643
|
try {
|
|
6457
|
-
const a =
|
|
6458
|
-
const b =
|
|
6644
|
+
const a = join7(dir, "a");
|
|
6645
|
+
const b = join7(dir, "b");
|
|
6459
6646
|
writeFileSync(a, before ?? "");
|
|
6460
6647
|
writeFileSync(b, after ?? "");
|
|
6461
6648
|
try {
|
|
@@ -6540,14 +6727,14 @@ function formatDenial(blocks) {
|
|
|
6540
6727
|
}
|
|
6541
6728
|
|
|
6542
6729
|
// src/session.ts
|
|
6543
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as
|
|
6544
|
-
import { dirname as dirname3, join as
|
|
6730
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6731
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
6545
6732
|
var UNSAFE = /[^A-Za-z0-9_-]/g;
|
|
6546
6733
|
var SHA = /^[0-9a-f]{7,40}$/;
|
|
6547
6734
|
function baselinePath(cwd, sessionId) {
|
|
6548
6735
|
const gd = gitDir(cwd);
|
|
6549
6736
|
if (!gd) return null;
|
|
6550
|
-
return
|
|
6737
|
+
return join8(gd, "tamperward", `session-${sessionId.replace(UNSAFE, "")}`);
|
|
6551
6738
|
}
|
|
6552
6739
|
function turnBaseline(cwd, sessionId) {
|
|
6553
6740
|
if (!sessionId) return null;
|
|
@@ -6555,7 +6742,7 @@ function turnBaseline(cwd, sessionId) {
|
|
|
6555
6742
|
const p = baselinePath(cwd, sessionId);
|
|
6556
6743
|
if (!p) return null;
|
|
6557
6744
|
if (existsSync5(p)) {
|
|
6558
|
-
const v =
|
|
6745
|
+
const v = readFileSync6(p, "utf8").trim();
|
|
6559
6746
|
if (SHA.test(v)) return v;
|
|
6560
6747
|
}
|
|
6561
6748
|
const head = headSha(cwd);
|
|
@@ -6581,8 +6768,8 @@ function advanceTurnBaseline(cwd, sessionId) {
|
|
|
6581
6768
|
|
|
6582
6769
|
// src/effect.ts
|
|
6583
6770
|
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
|
|
6771
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6772
|
+
import { isAbsolute as isAbsolute6, join as join9 } from "node:path";
|
|
6586
6773
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
6587
6774
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
6588
6775
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
@@ -6594,7 +6781,7 @@ function gitDir2(cwd) {
|
|
|
6594
6781
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6595
6782
|
}).trim();
|
|
6596
6783
|
if (!d) return null;
|
|
6597
|
-
return isAbsolute6(d) ? d :
|
|
6784
|
+
return isAbsolute6(d) ? d : join9(cwd, d);
|
|
6598
6785
|
} catch {
|
|
6599
6786
|
return null;
|
|
6600
6787
|
}
|
|
@@ -6603,8 +6790,8 @@ function statePath(cwd, sessionId, kind) {
|
|
|
6603
6790
|
if (!sessionId) return null;
|
|
6604
6791
|
const gd = gitDir2(cwd);
|
|
6605
6792
|
if (!gd) return null;
|
|
6606
|
-
const dir =
|
|
6607
|
-
return
|
|
6793
|
+
const dir = join9(gd, "tamperward");
|
|
6794
|
+
return join9(dir, `${kind}-${sessionId.replace(UNSAFE2, "")}.json`);
|
|
6608
6795
|
}
|
|
6609
6796
|
function ptreePath(cwd, sessionId) {
|
|
6610
6797
|
return statePath(cwd, sessionId, "ptree");
|
|
@@ -6633,7 +6820,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6633
6820
|
} catch {
|
|
6634
6821
|
}
|
|
6635
6822
|
const walk2 = (rel) => {
|
|
6636
|
-
const abs2 = rel ?
|
|
6823
|
+
const abs2 = rel ? join9(cwd, rel) : cwd;
|
|
6637
6824
|
let names;
|
|
6638
6825
|
try {
|
|
6639
6826
|
names = readdirSync(abs2);
|
|
@@ -6646,7 +6833,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
6646
6833
|
if (pruned.has(r)) continue;
|
|
6647
6834
|
let st;
|
|
6648
6835
|
try {
|
|
6649
|
-
st = lstatSync2(
|
|
6836
|
+
st = lstatSync2(join9(cwd, r));
|
|
6650
6837
|
} catch {
|
|
6651
6838
|
continue;
|
|
6652
6839
|
}
|
|
@@ -6675,7 +6862,7 @@ function driftBetween(expected, current) {
|
|
|
6675
6862
|
function loadTree(p) {
|
|
6676
6863
|
if (!p || !existsSync6(p)) return null;
|
|
6677
6864
|
try {
|
|
6678
|
-
return JSON.parse(
|
|
6865
|
+
return JSON.parse(readFileSync7(p, "utf8"));
|
|
6679
6866
|
} catch {
|
|
6680
6867
|
return null;
|
|
6681
6868
|
}
|
|
@@ -6683,7 +6870,7 @@ function loadTree(p) {
|
|
|
6683
6870
|
function saveTree(p, tree) {
|
|
6684
6871
|
if (!p) return;
|
|
6685
6872
|
try {
|
|
6686
|
-
mkdirSync3(
|
|
6873
|
+
mkdirSync3(join9(p, ".."), { recursive: true });
|
|
6687
6874
|
const tmp = `${p}.${process.pid}.tmp`;
|
|
6688
6875
|
writeFileSync3(tmp, JSON.stringify(tree));
|
|
6689
6876
|
renameSync(tmp, p);
|
|
@@ -6715,7 +6902,7 @@ function dropTurnTree(cwd, sessionId) {
|
|
|
6715
6902
|
|
|
6716
6903
|
// src/cli/watch.ts
|
|
6717
6904
|
import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync, watch } from "node:fs";
|
|
6718
|
-
import { join as
|
|
6905
|
+
import { join as join10 } from "node:path";
|
|
6719
6906
|
import { createHash as createHash3 } from "node:crypto";
|
|
6720
6907
|
var SKIP2 = /(^|\/)(\.git|node_modules)(\/|$)/;
|
|
6721
6908
|
function snap(abs2) {
|
|
@@ -6727,7 +6914,7 @@ function snap(abs2) {
|
|
|
6727
6914
|
}
|
|
6728
6915
|
function defaultEventLog(cwd) {
|
|
6729
6916
|
if (process.env.TAMPERWARD_FSEVENTS) return process.env.TAMPERWARD_FSEVENTS;
|
|
6730
|
-
return
|
|
6917
|
+
return join10(gitDir(cwd) ?? join10(cwd, ".git"), "tamperward", "fsevents.jsonl");
|
|
6731
6918
|
}
|
|
6732
6919
|
function watchTree(dir, cb) {
|
|
6733
6920
|
if (process.env.TAMPERWARD_WATCH_NO_RECURSIVE !== "1") {
|
|
@@ -6744,12 +6931,12 @@ function watchTree(dir, cb) {
|
|
|
6744
6931
|
if (watchers.has(rel) || SKIP2.test(rel + "/")) return;
|
|
6745
6932
|
let w;
|
|
6746
6933
|
try {
|
|
6747
|
-
w = watch(rel ?
|
|
6934
|
+
w = watch(rel ? join10(dir, rel) : dir, (kind, fname) => {
|
|
6748
6935
|
if (!fname) return;
|
|
6749
6936
|
const child = rel ? `${rel}/${String(fname)}` : String(fname);
|
|
6750
6937
|
cb(kind, child);
|
|
6751
6938
|
try {
|
|
6752
|
-
if (statSync(
|
|
6939
|
+
if (statSync(join10(dir, child)).isDirectory()) addDir(child);
|
|
6753
6940
|
} catch {
|
|
6754
6941
|
}
|
|
6755
6942
|
});
|
|
@@ -6762,7 +6949,7 @@ function watchTree(dir, cb) {
|
|
|
6762
6949
|
addDir(rel);
|
|
6763
6950
|
let names = [];
|
|
6764
6951
|
try {
|
|
6765
|
-
names = readdirSync2(rel ?
|
|
6952
|
+
names = readdirSync2(rel ? join10(dir, rel) : dir);
|
|
6766
6953
|
} catch {
|
|
6767
6954
|
return;
|
|
6768
6955
|
}
|
|
@@ -6770,7 +6957,7 @@ function watchTree(dir, cb) {
|
|
|
6770
6957
|
const child = rel ? `${rel}/${name}` : name;
|
|
6771
6958
|
if (SKIP2.test(child + "/")) continue;
|
|
6772
6959
|
try {
|
|
6773
|
-
if (statSync(
|
|
6960
|
+
if (statSync(join10(dir, child)).isDirectory()) walk2(child);
|
|
6774
6961
|
} catch {
|
|
6775
6962
|
}
|
|
6776
6963
|
}
|
|
@@ -6782,13 +6969,13 @@ function watchTree(dir, cb) {
|
|
|
6782
6969
|
}
|
|
6783
6970
|
function startWatcher(dir, log, policy) {
|
|
6784
6971
|
try {
|
|
6785
|
-
mkdirSync4(
|
|
6972
|
+
mkdirSync4(join10(log, ".."), { recursive: true });
|
|
6786
6973
|
} catch {
|
|
6787
6974
|
}
|
|
6788
6975
|
const last = /* @__PURE__ */ new Map();
|
|
6789
6976
|
return watchTree(dir, (kind, rel) => {
|
|
6790
6977
|
if (SKIP2.test(rel) || !isProtected(rel, policy)) return;
|
|
6791
|
-
const s = snap(
|
|
6978
|
+
const s = snap(join10(dir, rel));
|
|
6792
6979
|
const key2 = `${s.hash}:${s.mode}`;
|
|
6793
6980
|
if (last.get(rel) === key2) return;
|
|
6794
6981
|
last.set(rel, key2);
|
|
@@ -6815,7 +7002,7 @@ function runWatch(args) {
|
|
|
6815
7002
|
}
|
|
6816
7003
|
|
|
6817
7004
|
// src/detectors/fs-events.ts
|
|
6818
|
-
import { readFileSync as
|
|
7005
|
+
import { readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
|
|
6819
7006
|
var RULE11 = "transient-protected-mutation";
|
|
6820
7007
|
function readEvents(log, offset) {
|
|
6821
7008
|
let size = 0;
|
|
@@ -6825,7 +7012,7 @@ function readEvents(log, offset) {
|
|
|
6825
7012
|
return { events: [], newOffset: offset };
|
|
6826
7013
|
}
|
|
6827
7014
|
if (size <= offset) return { events: [], newOffset: offset >= 0 ? Math.min(offset, size) : 0 };
|
|
6828
|
-
const raw =
|
|
7015
|
+
const raw = readFileSync9(log, "utf8").slice(offset);
|
|
6829
7016
|
const events = [];
|
|
6830
7017
|
for (const line of raw.split("\n")) {
|
|
6831
7018
|
if (!line.trim()) continue;
|
|
@@ -6878,7 +7065,7 @@ var HookInputError = class extends Error {
|
|
|
6878
7065
|
};
|
|
6879
7066
|
function readStdin() {
|
|
6880
7067
|
try {
|
|
6881
|
-
return
|
|
7068
|
+
return readFileSync10(0, "utf8");
|
|
6882
7069
|
} catch (e) {
|
|
6883
7070
|
throw new HookInputError(`cannot read the hook payload on stdin: ${e instanceof Error ? e.message : String(e)}`);
|
|
6884
7071
|
}
|
|
@@ -7134,7 +7321,7 @@ function turnTransientBlocks(cwd, sessionId, policy, changes) {
|
|
|
7134
7321
|
if (!existsSync7(log)) return none;
|
|
7135
7322
|
let offset = 0;
|
|
7136
7323
|
if (cp && existsSync7(cp)) {
|
|
7137
|
-
const n = Number(
|
|
7324
|
+
const n = Number(readFileSync10(cp, "utf8"));
|
|
7138
7325
|
offset = Number.isFinite(n) ? n : 0;
|
|
7139
7326
|
}
|
|
7140
7327
|
const { events, newOffset } = readEvents(log, offset);
|
|
@@ -7287,8 +7474,8 @@ Honored at LOCAL pre-commit only. The agent-layer hook ignores this file; CI req
|
|
|
7287
7474
|
// src/cli/init.ts
|
|
7288
7475
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7289
7476
|
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
|
|
7477
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5, chmodSync, statSync as statSync3 } from "node:fs";
|
|
7478
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute7, join as join11, relative as relative3, resolve as resolve3 } from "node:path";
|
|
7292
7479
|
var errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
|
|
7293
7480
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7294
7481
|
function gitConfig(cwd, key2) {
|
|
@@ -7464,7 +7651,7 @@ function hooksShapeError(hooks) {
|
|
|
7464
7651
|
return null;
|
|
7465
7652
|
}
|
|
7466
7653
|
function planPolicy(cwd) {
|
|
7467
|
-
const path =
|
|
7654
|
+
const path = join11(cwd, POLICY_FILE);
|
|
7468
7655
|
if (existsSync8(path)) {
|
|
7469
7656
|
try {
|
|
7470
7657
|
loadPolicy(cwd);
|
|
@@ -7483,12 +7670,12 @@ function planPolicy(cwd) {
|
|
|
7483
7670
|
}
|
|
7484
7671
|
function planClaudeHooks(cwd) {
|
|
7485
7672
|
const rel = ".claude/settings.json";
|
|
7486
|
-
const path =
|
|
7673
|
+
const path = join11(cwd, rel);
|
|
7487
7674
|
let settings = {};
|
|
7488
7675
|
if (existsSync8(path)) {
|
|
7489
7676
|
let parsed;
|
|
7490
7677
|
try {
|
|
7491
|
-
parsed = JSON.parse(
|
|
7678
|
+
parsed = JSON.parse(readFileSync11(path, "utf8"));
|
|
7492
7679
|
} catch {
|
|
7493
7680
|
return {
|
|
7494
7681
|
item: "agent",
|
|
@@ -7584,7 +7771,7 @@ function hooksDir(cwd) {
|
|
|
7584
7771
|
if (out2) return resolve3(cwd, out2);
|
|
7585
7772
|
} catch {
|
|
7586
7773
|
}
|
|
7587
|
-
return existsSync8(
|
|
7774
|
+
return existsSync8(join11(cwd, ".git")) ? join11(cwd, ".git", "hooks") : null;
|
|
7588
7775
|
}
|
|
7589
7776
|
function display(cwd, path) {
|
|
7590
7777
|
const rel = relative3(cwd, path);
|
|
@@ -7611,30 +7798,30 @@ function planPreCommit(cwd) {
|
|
|
7611
7798
|
const skip = (detail) => ({ item: "pre-commit", path: "(none)", status: "skip", detail });
|
|
7612
7799
|
let path;
|
|
7613
7800
|
let note;
|
|
7614
|
-
if (existsSync8(
|
|
7615
|
-
path =
|
|
7801
|
+
if (existsSync8(join11(cwd, ".husky"))) {
|
|
7802
|
+
path = join11(cwd, ".husky", "pre-commit");
|
|
7616
7803
|
note = "husky";
|
|
7617
7804
|
} else {
|
|
7618
|
-
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(
|
|
7805
|
+
const managed = MANAGED_HOOKS.find((m) => m.configs.some((c) => existsSync8(join11(cwd, c))));
|
|
7619
7806
|
if (managed) {
|
|
7620
7807
|
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
7808
|
}
|
|
7622
7809
|
const dir = hooksDir(cwd);
|
|
7623
7810
|
if (!dir) return skip("not a git repo and no .husky/ \u2014 nothing to wire");
|
|
7624
7811
|
if (basename2(dir) === "_" && basename2(dirname4(dir)) === ".husky") {
|
|
7625
|
-
path =
|
|
7812
|
+
path = join11(dirname4(dir), "pre-commit");
|
|
7626
7813
|
note = "husky";
|
|
7627
7814
|
} else {
|
|
7628
|
-
path =
|
|
7815
|
+
path = join11(dir, "pre-commit");
|
|
7629
7816
|
const rel2 = display(cwd, dir);
|
|
7630
|
-
note = rel2 ===
|
|
7817
|
+
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
7818
|
}
|
|
7632
7819
|
}
|
|
7633
7820
|
const rel = display(cwd, path);
|
|
7634
7821
|
if (existsSync8(path) && !statSync3(path).isFile()) {
|
|
7635
7822
|
return { item: "pre-commit", path: rel, status: "error", detail: "exists but is not a regular file \u2014 refusing to write" };
|
|
7636
7823
|
}
|
|
7637
|
-
const existing = existsSync8(path) ?
|
|
7824
|
+
const existing = existsSync8(path) ? readFileSync11(path, "utf8") : null;
|
|
7638
7825
|
if (existing === null) {
|
|
7639
7826
|
return {
|
|
7640
7827
|
item: "pre-commit",
|
|
@@ -7723,10 +7910,10 @@ ${block.join("\n")}
|
|
|
7723
7910
|
function planCodeowners(cwd) {
|
|
7724
7911
|
const rel = ".github/CODEOWNERS";
|
|
7725
7912
|
const existingRel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find(
|
|
7726
|
-
(r) => existsSync8(
|
|
7913
|
+
(r) => existsSync8(join11(cwd, r))
|
|
7727
7914
|
);
|
|
7728
|
-
const path =
|
|
7729
|
-
const existing = existingRel ?
|
|
7915
|
+
const path = join11(cwd, existingRel ?? rel);
|
|
7916
|
+
const existing = existingRel ? readFileSync11(path, "utf8") : null;
|
|
7730
7917
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
7731
7918
|
if (missing.length === 0) {
|
|
7732
7919
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
@@ -7759,7 +7946,7 @@ ${CODEOWNERS_MARK}
|
|
|
7759
7946
|
}
|
|
7760
7947
|
function planWorkflow(cwd, force) {
|
|
7761
7948
|
const rel = ".github/workflows/tamperward.yml";
|
|
7762
|
-
const path =
|
|
7949
|
+
const path = join11(cwd, rel);
|
|
7763
7950
|
const write = () => {
|
|
7764
7951
|
mkdirSync5(dirname4(path), { recursive: true });
|
|
7765
7952
|
writeFileSync5(path, workflowFile(WORKFLOW_CONTENT));
|
|
@@ -7775,7 +7962,7 @@ function planWorkflow(cwd, force) {
|
|
|
7775
7962
|
}
|
|
7776
7963
|
let src;
|
|
7777
7964
|
try {
|
|
7778
|
-
src =
|
|
7965
|
+
src = readFileSync11(path, "utf8");
|
|
7779
7966
|
} catch {
|
|
7780
7967
|
return { item: "ci", path: rel, status: "error", detail: "exists but cannot be read \u2014 refusing to overwrite" };
|
|
7781
7968
|
}
|
|
@@ -7874,15 +8061,15 @@ tamperward init: ${changed} change(s) applied. Commit them so the gate travels w
|
|
|
7874
8061
|
// src/cli/verify.ts
|
|
7875
8062
|
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
7876
8063
|
import { createHash as createHash8 } from "node:crypto";
|
|
7877
|
-
import { chmodSync as chmodSync2, cpSync, lstatSync as lstatSync3, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as
|
|
8064
|
+
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
8065
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
7879
|
-
import { dirname as dirname5, join as
|
|
8066
|
+
import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
|
|
7880
8067
|
|
|
7881
8068
|
// src/git/trusted.ts
|
|
7882
8069
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7883
8070
|
import { createHash as createHash6 } from "node:crypto";
|
|
7884
|
-
import { existsSync as existsSync9, readFileSync as
|
|
7885
|
-
import { isAbsolute as isAbsolute8, join as
|
|
8071
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
8072
|
+
import { isAbsolute as isAbsolute8, join as join12 } from "node:path";
|
|
7886
8073
|
function trustedGitEnv() {
|
|
7887
8074
|
return { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" };
|
|
7888
8075
|
}
|
|
@@ -7895,7 +8082,7 @@ function gitDirOf(cwd) {
|
|
|
7895
8082
|
env: trustedGitEnv()
|
|
7896
8083
|
}).trim();
|
|
7897
8084
|
if (!d) return null;
|
|
7898
|
-
return isAbsolute8(d) ? d :
|
|
8085
|
+
return isAbsolute8(d) ? d : join12(cwd, d);
|
|
7899
8086
|
} catch {
|
|
7900
8087
|
return null;
|
|
7901
8088
|
}
|
|
@@ -7920,9 +8107,9 @@ function objectRewriteState(cwd) {
|
|
|
7920
8107
|
parts.push(`${rel}:<no-git-dir>`);
|
|
7921
8108
|
continue;
|
|
7922
8109
|
}
|
|
7923
|
-
const p =
|
|
8110
|
+
const p = join12(dir, rel);
|
|
7924
8111
|
try {
|
|
7925
|
-
parts.push(`${rel}:` + (existsSync9(p) ?
|
|
8112
|
+
parts.push(`${rel}:` + (existsSync9(p) ? readFileSync12(p, "utf8").trim() : ""));
|
|
7926
8113
|
} catch {
|
|
7927
8114
|
parts.push(`${rel}:<unreadable>`);
|
|
7928
8115
|
}
|
|
@@ -7933,8 +8120,8 @@ function objectRewriteState(cwd) {
|
|
|
7933
8120
|
// src/fingerprint.ts
|
|
7934
8121
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7935
8122
|
import { createHash as createHash7 } from "node:crypto";
|
|
7936
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
7937
|
-
import { join as
|
|
8123
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync13, readlinkSync as readlinkSync2, statSync as statSync4 } from "node:fs";
|
|
8124
|
+
import { join as join13 } from "node:path";
|
|
7938
8125
|
function treeFingerprint(cwd) {
|
|
7939
8126
|
const files = execFileSync7("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
7940
8127
|
cwd,
|
|
@@ -7952,7 +8139,7 @@ function treeFingerprint(cwd) {
|
|
|
7952
8139
|
return h.digest("hex");
|
|
7953
8140
|
}
|
|
7954
8141
|
function depsFingerprint(cwd) {
|
|
7955
|
-
const root =
|
|
8142
|
+
const root = join13(cwd, "node_modules");
|
|
7956
8143
|
const h = createHash7("sha256");
|
|
7957
8144
|
const walk2 = (dir) => {
|
|
7958
8145
|
let entries;
|
|
@@ -7962,12 +8149,12 @@ function depsFingerprint(cwd) {
|
|
|
7962
8149
|
return;
|
|
7963
8150
|
}
|
|
7964
8151
|
for (const e of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7965
|
-
const p =
|
|
8152
|
+
const p = join13(dir, e.name);
|
|
7966
8153
|
if (e.isDirectory()) walk2(p);
|
|
7967
8154
|
else if (e.isFile()) {
|
|
7968
8155
|
h.update(p);
|
|
7969
8156
|
try {
|
|
7970
|
-
h.update(
|
|
8157
|
+
h.update(readFileSync13(p));
|
|
7971
8158
|
h.update(String(statSync4(p).mode));
|
|
7972
8159
|
} catch {
|
|
7973
8160
|
h.update("<unreadable>");
|
|
@@ -8082,7 +8269,7 @@ function baseIsAncestorOfHead(base, cwd) {
|
|
|
8082
8269
|
function materialize(cwd, dest) {
|
|
8083
8270
|
const listed = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8084
8271
|
for (const rel of listed) {
|
|
8085
|
-
const src =
|
|
8272
|
+
const src = join14(cwd, rel);
|
|
8086
8273
|
let st;
|
|
8087
8274
|
try {
|
|
8088
8275
|
st = statSync5(src);
|
|
@@ -8090,18 +8277,18 @@ function materialize(cwd, dest) {
|
|
|
8090
8277
|
continue;
|
|
8091
8278
|
}
|
|
8092
8279
|
if (!st.isFile()) continue;
|
|
8093
|
-
const out2 =
|
|
8280
|
+
const out2 = join14(dest, rel);
|
|
8094
8281
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8095
8282
|
dropSymlink(out2);
|
|
8096
8283
|
if (lstatSync3(src).isSymbolicLink()) {
|
|
8097
|
-
writeFileSync6(out2,
|
|
8284
|
+
writeFileSync6(out2, readFileSync14(src));
|
|
8098
8285
|
} else {
|
|
8099
8286
|
cpSync(src, out2, { dereference: true });
|
|
8100
8287
|
}
|
|
8101
8288
|
chmodSync2(out2, st.mode);
|
|
8102
8289
|
}
|
|
8103
8290
|
const nm = resolve4(cwd, "node_modules");
|
|
8104
|
-
if (existsSync10(nm)) symlinkSync(nm,
|
|
8291
|
+
if (existsSync10(nm)) symlinkSync(nm, join14(dest, "node_modules"), "dir");
|
|
8105
8292
|
}
|
|
8106
8293
|
function dropSymlink(p) {
|
|
8107
8294
|
try {
|
|
@@ -8134,11 +8321,11 @@ function overlayDigest(dest, paths) {
|
|
|
8134
8321
|
for (const rel of [...paths].sort()) {
|
|
8135
8322
|
h.update(rel);
|
|
8136
8323
|
h.update("\0");
|
|
8137
|
-
const p =
|
|
8324
|
+
const p = join14(dest, rel);
|
|
8138
8325
|
try {
|
|
8139
8326
|
const st = lstatSync3(p);
|
|
8140
8327
|
h.update(String(st.mode));
|
|
8141
|
-
h.update(st.isSymbolicLink() ? readlinkSync3(p) :
|
|
8328
|
+
h.update(st.isSymbolicLink() ? readlinkSync3(p) : readFileSync14(p));
|
|
8142
8329
|
} catch {
|
|
8143
8330
|
h.update("<gone>");
|
|
8144
8331
|
}
|
|
@@ -8161,7 +8348,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8161
8348
|
throw new Error(`the base's ${e.path} is a submodule; the pristine copy cannot reproduce it`);
|
|
8162
8349
|
}
|
|
8163
8350
|
const content = execFileSync8("git", ["show", `${base}:${e.path}`], { cwd, maxBuffer: 1 << 28, env: trustedGitEnv() });
|
|
8164
|
-
const out2 =
|
|
8351
|
+
const out2 = join14(dest, e.path);
|
|
8165
8352
|
mkdirSync6(dirname5(out2), { recursive: true });
|
|
8166
8353
|
rmSync3(out2, { force: true });
|
|
8167
8354
|
if (e.mode === "120000") {
|
|
@@ -8176,7 +8363,7 @@ function overlayPristine(cwd, base, dest, policy, cmd) {
|
|
|
8176
8363
|
const inCopy = git2(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], cwd).split("\0").filter(Boolean);
|
|
8177
8364
|
for (const rel of inCopy) {
|
|
8178
8365
|
if (!isOverlay(rel) || baseProtected.has(rel)) continue;
|
|
8179
|
-
rmSync3(
|
|
8366
|
+
rmSync3(join14(dest, rel), { force: true });
|
|
8180
8367
|
removed++;
|
|
8181
8368
|
}
|
|
8182
8369
|
return { restored, removed };
|
|
@@ -8218,8 +8405,8 @@ function suiteEnv(scratch) {
|
|
|
8218
8405
|
if (DROPPED_ENV.has(k) || PINNED_NPM.test(k)) continue;
|
|
8219
8406
|
env[k] = v;
|
|
8220
8407
|
}
|
|
8221
|
-
const userRc =
|
|
8222
|
-
const globalRc =
|
|
8408
|
+
const userRc = join14(scratch, "npmrc-user");
|
|
8409
|
+
const globalRc = join14(scratch, "npmrc-global");
|
|
8223
8410
|
for (const f of [userRc, globalRc]) writeFileSync6(f, "", { mode: 292 });
|
|
8224
8411
|
env.npm_config_userconfig = userRc;
|
|
8225
8412
|
env.npm_config_globalconfig = globalRc;
|
|
@@ -8229,8 +8416,8 @@ function suiteEnv(scratch) {
|
|
|
8229
8416
|
}
|
|
8230
8417
|
function runSuite(dir, cmd, budgetSecs) {
|
|
8231
8418
|
const t0 = Date.now();
|
|
8232
|
-
const outDir = mkdtempSync2(
|
|
8233
|
-
const outFile =
|
|
8419
|
+
const outDir = mkdtempSync2(join14(tmpdir2(), "tw-verify-run-"));
|
|
8420
|
+
const outFile = join14(outDir, "result.json");
|
|
8234
8421
|
try {
|
|
8235
8422
|
spawnSync(process.execPath, ["-e", SUPERVISOR, cmd, String(budgetSecs * 1e3), outFile], {
|
|
8236
8423
|
cwd: dir,
|
|
@@ -8243,7 +8430,7 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
8243
8430
|
const secs = Math.round((Date.now() - t0) / 1e3);
|
|
8244
8431
|
let r;
|
|
8245
8432
|
try {
|
|
8246
|
-
r = JSON.parse(
|
|
8433
|
+
r = JSON.parse(readFileSync14(outFile, "utf8"));
|
|
8247
8434
|
} catch {
|
|
8248
8435
|
return { exit: null, secs };
|
|
8249
8436
|
}
|
|
@@ -8294,8 +8481,8 @@ function runVerify(opts) {
|
|
|
8294
8481
|
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
8295
8482
|
return 2;
|
|
8296
8483
|
}
|
|
8297
|
-
const visRoot = mkdtempSync2(
|
|
8298
|
-
const visDir =
|
|
8484
|
+
const visRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-vis-"));
|
|
8485
|
+
const visDir = join14(visRoot, "t");
|
|
8299
8486
|
const cleanup = (dirs) => {
|
|
8300
8487
|
if (opts.keep) return;
|
|
8301
8488
|
for (const d of dirs) rmSync3(d, { recursive: true, force: true });
|
|
@@ -8323,8 +8510,8 @@ function runVerify(opts) {
|
|
|
8323
8510
|
out2("pristine run executes through it too. Failing closed, not open.");
|
|
8324
8511
|
return 2;
|
|
8325
8512
|
}
|
|
8326
|
-
const priRoot = mkdtempSync2(
|
|
8327
|
-
const priDir =
|
|
8513
|
+
const priRoot = mkdtempSync2(join14(tmpdir2(), "tw-verify-pri-"));
|
|
8514
|
+
const priDir = join14(priRoot, "t");
|
|
8328
8515
|
let restored = [];
|
|
8329
8516
|
let removedAdded = 0;
|
|
8330
8517
|
try {
|
|
@@ -8436,7 +8623,7 @@ function parseVerify(args) {
|
|
|
8436
8623
|
|
|
8437
8624
|
// src/cli/run.ts
|
|
8438
8625
|
import { execFileSync as execFileSync9, spawnSync as spawnSync2 } from "node:child_process";
|
|
8439
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
8626
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync15, realpathSync as realpathSync3 } from "node:fs";
|
|
8440
8627
|
import { resolve as resolve5 } from "node:path";
|
|
8441
8628
|
var out = (s) => process.stdout.write(s + "\n");
|
|
8442
8629
|
var err = (s) => process.stderr.write(s + "\n");
|
|
@@ -8445,7 +8632,7 @@ function git3(args, cwd) {
|
|
|
8445
8632
|
}
|
|
8446
8633
|
function nowTicks() {
|
|
8447
8634
|
try {
|
|
8448
|
-
return Math.floor(parseFloat(
|
|
8635
|
+
return Math.floor(parseFloat(readFileSync15("/proc/uptime", "utf8").split(" ")[0]) * 100);
|
|
8449
8636
|
} catch {
|
|
8450
8637
|
return Number.POSITIVE_INFINITY;
|
|
8451
8638
|
}
|
|
@@ -8475,7 +8662,7 @@ function survivorsHoldingTree(cwd, spawnedAfterTicks) {
|
|
|
8475
8662
|
}
|
|
8476
8663
|
if (held !== real && !held.startsWith(real + "/")) continue;
|
|
8477
8664
|
try {
|
|
8478
|
-
const stat =
|
|
8665
|
+
const stat = readFileSync15(`/proc/${pid}/stat`, "utf8");
|
|
8479
8666
|
const startTicks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
|
|
8480
8667
|
if (Number.isFinite(startTicks) && startTicks >= spawnedAfterTicks) out2.push(n);
|
|
8481
8668
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
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",
|