tamperward 2.19.0 → 2.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -27
- package/dist/cli/index.js +679 -373
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -216,6 +216,10 @@ function defaultPolicy(version = 1) {
|
|
|
216
216
|
// broad any in annotation/generic position is common in legit code (measured ~84-100% FP as a
|
|
217
217
|
// block rule on real TS), so it WARNs until a semantic (error-silencing-aware) signal earns block.
|
|
218
218
|
"ts-any-launder": { severity: "warn" },
|
|
219
|
+
// net growth of ordinary `as T` / `<T>x` / `x!` assertions in non-test source (#383).
|
|
220
|
+
// Fires on a large share of legitimate TypeScript mainline diffs (measured in
|
|
221
|
+
// harness/fp-study/CAST-GROWTH-CORPUS.md), so it is a review prompt, not a gate.
|
|
222
|
+
"ts-cast-growth": { severity: "warn" },
|
|
219
223
|
"lint-suppression": { severity: "block" },
|
|
220
224
|
"coverage-lowering": { severity: "block" },
|
|
221
225
|
"ci-tampering": { severity: "block" },
|
|
@@ -244,6 +248,45 @@ function defaultPolicy(version = 1) {
|
|
|
244
248
|
return p;
|
|
245
249
|
}
|
|
246
250
|
|
|
251
|
+
// src/narrow.ts
|
|
252
|
+
function isRecord(v) {
|
|
253
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
254
|
+
}
|
|
255
|
+
function errnoCode(e) {
|
|
256
|
+
const code2 = isRecord(e) ? e.code : void 0;
|
|
257
|
+
return typeof code2 === "string" ? code2 : void 0;
|
|
258
|
+
}
|
|
259
|
+
function errorMessage(e) {
|
|
260
|
+
return e instanceof Error ? e.message : String(e);
|
|
261
|
+
}
|
|
262
|
+
function textOf(v) {
|
|
263
|
+
if (typeof v === "string") return v;
|
|
264
|
+
if (Buffer.isBuffer(v)) return v.toString("utf8");
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
function execFailure(e) {
|
|
268
|
+
const r = isRecord(e) ? e : {};
|
|
269
|
+
return {
|
|
270
|
+
stdout: textOf(r.stdout),
|
|
271
|
+
stderr: textOf(r.stderr),
|
|
272
|
+
status: typeof r.status === "number" ? r.status : null,
|
|
273
|
+
code: errnoCode(e),
|
|
274
|
+
message: errorMessage(e)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function finiteNumber(v) {
|
|
278
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
279
|
+
}
|
|
280
|
+
function stringOrUndefined(v) {
|
|
281
|
+
return typeof v === "string" ? v : void 0;
|
|
282
|
+
}
|
|
283
|
+
function nullableString(v) {
|
|
284
|
+
return v === null || typeof v === "string" ? v : void 0;
|
|
285
|
+
}
|
|
286
|
+
function nullableNumber(v) {
|
|
287
|
+
return v === null || typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
// src/disk.ts
|
|
248
291
|
var READ_CAP = 64 * 1024 * 1024;
|
|
249
292
|
var NO_META = { mode: 0, size: 0, mtimeMs: 0 };
|
|
@@ -251,7 +294,7 @@ function errText(e) {
|
|
|
251
294
|
return e instanceof Error ? e.message : String(e);
|
|
252
295
|
}
|
|
253
296
|
function errCode(e) {
|
|
254
|
-
return
|
|
297
|
+
return errnoCode(e) ?? "";
|
|
255
298
|
}
|
|
256
299
|
function irregularName(st) {
|
|
257
300
|
if (st.isFIFO()) return "fifo";
|
|
@@ -341,7 +384,7 @@ function inspectResolved(abs2) {
|
|
|
341
384
|
}
|
|
342
385
|
return inspectPath(real);
|
|
343
386
|
}
|
|
344
|
-
function
|
|
387
|
+
function textOf2(e) {
|
|
345
388
|
return e.content == null ? null : e.content.toString("utf8");
|
|
346
389
|
}
|
|
347
390
|
function describe(e, shown) {
|
|
@@ -675,9 +718,9 @@ function git(args, cwd) {
|
|
|
675
718
|
env: trustedGitEnv()
|
|
676
719
|
});
|
|
677
720
|
} catch (e) {
|
|
678
|
-
const
|
|
679
|
-
const detail =
|
|
680
|
-
throw new Error(detail ? `git ${args[0]}: ${detail}` :
|
|
721
|
+
const failure = execFailure(e);
|
|
722
|
+
const detail = failure.stderr.trim();
|
|
723
|
+
throw new Error(detail ? `git ${args[0]}: ${detail}` : failure.message || `git ${args[0]} failed`);
|
|
681
724
|
}
|
|
682
725
|
}
|
|
683
726
|
function blobAt(rev, path, cwd) {
|
|
@@ -688,7 +731,7 @@ function blobAt(rev, path, cwd) {
|
|
|
688
731
|
}
|
|
689
732
|
}
|
|
690
733
|
function fromDisk(path, cwd) {
|
|
691
|
-
return
|
|
734
|
+
return textOf2(inspectRel(cwd ?? process.cwd(), path));
|
|
692
735
|
}
|
|
693
736
|
function enrich(c, beforeReader, afterReader) {
|
|
694
737
|
if (c.kind !== "file") return c;
|
|
@@ -807,7 +850,7 @@ function ignoredAdds(opts = {}, keep, exclude) {
|
|
|
807
850
|
}
|
|
808
851
|
function diffWorktreeWithUntracked(opts = {}, keepIgnored) {
|
|
809
852
|
const tracked = diffWorktree(opts);
|
|
810
|
-
const seen = new Set(tracked.
|
|
853
|
+
const seen = new Set(tracked.flatMap((c) => c.kind === "file" ? [c.path] : []));
|
|
811
854
|
const untracked = untrackedAdds(opts, void 0, seen);
|
|
812
855
|
for (const c of untracked) if (c.kind === "file") seen.add(c.path);
|
|
813
856
|
const ignored = keepIgnored ? ignoredAdds(opts, keepIgnored, seen) : [];
|
|
@@ -832,7 +875,7 @@ function asStr(v) {
|
|
|
832
875
|
return typeof v === "string" ? v : "";
|
|
833
876
|
}
|
|
834
877
|
function readDisk(path) {
|
|
835
|
-
return
|
|
878
|
+
return textOf2(inspectResolved(path));
|
|
836
879
|
}
|
|
837
880
|
function abs(path, cwd) {
|
|
838
881
|
return resolve(cwd, path);
|
|
@@ -865,11 +908,11 @@ function synthFileChange(displayPath, before, after) {
|
|
|
865
908
|
maxBuffer: 256 * 1024 * 1024
|
|
866
909
|
});
|
|
867
910
|
} catch (e) {
|
|
868
|
-
const
|
|
869
|
-
if (
|
|
870
|
-
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${
|
|
911
|
+
const failure = execFailure(e);
|
|
912
|
+
if (failure.status !== 1) {
|
|
913
|
+
throw new Error(`cannot diff the incoming edit to ${displayPath}: ${failure.code ?? failure.message}`);
|
|
871
914
|
}
|
|
872
|
-
raw =
|
|
915
|
+
raw = failure.stdout;
|
|
873
916
|
}
|
|
874
917
|
} finally {
|
|
875
918
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -905,8 +948,8 @@ function changesFromClaudeHook(input, cwd) {
|
|
|
905
948
|
let after = before;
|
|
906
949
|
const edits = Array.isArray(ti.edits) ? ti.edits : [];
|
|
907
950
|
for (const raw of edits) {
|
|
908
|
-
const ed = raw;
|
|
909
|
-
after = applyEdit(after, ed.old_string
|
|
951
|
+
const ed = isRecord(raw) ? raw : {};
|
|
952
|
+
after = applyEdit(after, asStr(ed.old_string), asStr(ed.new_string));
|
|
910
953
|
}
|
|
911
954
|
return synthFileChange(relForDisplay(abs(fp, cwd), cwd), before, after);
|
|
912
955
|
}
|
|
@@ -1110,7 +1153,7 @@ var noVerify = {
|
|
|
1110
1153
|
};
|
|
1111
1154
|
|
|
1112
1155
|
// src/detectors/ts-any-cast.ts
|
|
1113
|
-
import
|
|
1156
|
+
import ts2 from "typescript";
|
|
1114
1157
|
|
|
1115
1158
|
// src/diff/select.ts
|
|
1116
1159
|
function addedLines(c) {
|
|
@@ -1190,39 +1233,188 @@ function insideStringLiteral(line, idx, lang) {
|
|
|
1190
1233
|
return quote !== null;
|
|
1191
1234
|
}
|
|
1192
1235
|
|
|
1236
|
+
// src/detectors/ts-cast-growth.ts
|
|
1237
|
+
import ts from "typescript";
|
|
1238
|
+
var RULE2 = "ts-cast-growth";
|
|
1239
|
+
var DECLARATION = /\.d\.(?:ts|mts|cts)$/;
|
|
1240
|
+
var OUT_OF_BUDGET_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|generated|__generated__)\/|\.(?:generated|gen)\.[cm]?[jt]sx?$/;
|
|
1241
|
+
var GENERATED_HEADER = /@generated\b|\bAUTO-?GENERATED\b|\bDO NOT EDIT\b/i;
|
|
1242
|
+
var HEADER_LINES = 20;
|
|
1243
|
+
function scriptKind(path) {
|
|
1244
|
+
const lower = path.toLowerCase();
|
|
1245
|
+
if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
1246
|
+
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) return ts.ScriptKind.TS;
|
|
1247
|
+
if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX;
|
|
1248
|
+
return ts.ScriptKind.JS;
|
|
1249
|
+
}
|
|
1250
|
+
function outOfBudget(path, before) {
|
|
1251
|
+
if (DECLARATION.test(path) || OUT_OF_BUDGET_PATH.test(path)) return true;
|
|
1252
|
+
return before != null && GENERATED_HEADER.test(before.split("\n", HEADER_LINES).join("\n"));
|
|
1253
|
+
}
|
|
1254
|
+
function unparenthesized(e) {
|
|
1255
|
+
let x = e;
|
|
1256
|
+
while (ts.isParenthesizedExpression(x)) x = x.expression;
|
|
1257
|
+
return x;
|
|
1258
|
+
}
|
|
1259
|
+
function assertedType(node) {
|
|
1260
|
+
let t = node.type;
|
|
1261
|
+
while (ts.isParenthesizedTypeNode(t)) t = t.type;
|
|
1262
|
+
return t;
|
|
1263
|
+
}
|
|
1264
|
+
function isDoubleCast(node) {
|
|
1265
|
+
const inner = unparenthesized(node.expression);
|
|
1266
|
+
return (ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner)) && assertedType(inner).kind === ts.SyntaxKind.UnknownKeyword;
|
|
1267
|
+
}
|
|
1268
|
+
function surfaceOf(path, src) {
|
|
1269
|
+
let sf;
|
|
1270
|
+
try {
|
|
1271
|
+
sf = ts.createSourceFile(path, src, ts.ScriptTarget.Latest, true, scriptKind(path));
|
|
1272
|
+
} catch {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
|
|
1276
|
+
if (diagnostics.length > 0) return null;
|
|
1277
|
+
const surface = { type: 0, nonNull: 0, assertions: [] };
|
|
1278
|
+
const lines = src.split("\n");
|
|
1279
|
+
const record = (kind, node) => {
|
|
1280
|
+
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
1281
|
+
surface.assertions.push({ kind, line, text: node.getText(sf).replace(/\s+/g, " "), lineText: (lines[line - 1] ?? "").trim() });
|
|
1282
|
+
if (kind === "type") surface.type++;
|
|
1283
|
+
else surface.nonNull++;
|
|
1284
|
+
};
|
|
1285
|
+
const isRow4OrHonest = (node) => {
|
|
1286
|
+
const type = assertedType(node);
|
|
1287
|
+
if (type.kind === ts.SyntaxKind.AnyKeyword || type.kind === ts.SyntaxKind.UnknownKeyword) return true;
|
|
1288
|
+
return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
|
|
1289
|
+
};
|
|
1290
|
+
const visit = (node) => {
|
|
1291
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
|
|
1292
|
+
if (!isDoubleCast(node) && !isRow4OrHonest(node)) record("type", node);
|
|
1293
|
+
} else if (ts.isNonNullExpression(node)) {
|
|
1294
|
+
record("non-null", node);
|
|
1295
|
+
}
|
|
1296
|
+
ts.forEachChild(node, visit);
|
|
1297
|
+
};
|
|
1298
|
+
visit(sf);
|
|
1299
|
+
return surface;
|
|
1300
|
+
}
|
|
1301
|
+
function unmatchedAfter(before, after) {
|
|
1302
|
+
const n = before.length;
|
|
1303
|
+
const m = after.length;
|
|
1304
|
+
const same = (i2, j2) => before[i2].lineText === after[j2].lineText && before[i2].text === after[j2].text;
|
|
1305
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1306
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
1307
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
1308
|
+
lcs[i2][j2] = same(i2, j2) ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const out2 = [];
|
|
1312
|
+
let i = 0;
|
|
1313
|
+
let j = 0;
|
|
1314
|
+
while (j < m) {
|
|
1315
|
+
if (i < n && same(i, j)) {
|
|
1316
|
+
i++;
|
|
1317
|
+
j++;
|
|
1318
|
+
} else if (i < n && lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
1319
|
+
i++;
|
|
1320
|
+
} else {
|
|
1321
|
+
out2.push(after[j]);
|
|
1322
|
+
j++;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return out2;
|
|
1326
|
+
}
|
|
1327
|
+
var tsCastGrowth = {
|
|
1328
|
+
id: RULE2,
|
|
1329
|
+
surface: ["file"],
|
|
1330
|
+
certainty: "mechanical",
|
|
1331
|
+
run(changes, policy) {
|
|
1332
|
+
const out2 = [];
|
|
1333
|
+
for (const c of changes) {
|
|
1334
|
+
if (c.kind !== "file" || c.op === "delete" || c.after == null) continue;
|
|
1335
|
+
if (!isCodeFile(c.path) || outOfBudget(c.path, c.before)) continue;
|
|
1336
|
+
if (protectedCategory(c.path, policy) === "tests") continue;
|
|
1337
|
+
const after = surfaceOf(c.path, c.after);
|
|
1338
|
+
const before = c.before == null ? { type: 0, nonNull: 0, assertions: [] } : surfaceOf(c.path, c.before);
|
|
1339
|
+
if (!after || !before) continue;
|
|
1340
|
+
const dType = after.type - before.type;
|
|
1341
|
+
const dNonNull = after.nonNull - before.nonNull;
|
|
1342
|
+
if (dType <= 0 && dNonNull <= 0) continue;
|
|
1343
|
+
const grownKinds = /* @__PURE__ */ new Set([...dType > 0 ? ["type"] : [], ...dNonNull > 0 ? ["non-null"] : []]);
|
|
1344
|
+
const addedLineNumbers2 = new Set(addedLines(c).flatMap((l) => l.newLine == null ? [] : [l.newLine]));
|
|
1345
|
+
const onAddedLine = after.assertions.find((a) => grownKinds.has(a.kind) && addedLineNumbers2.has(a.line));
|
|
1346
|
+
const first = onAddedLine ?? unmatchedAfter(before.assertions, after.assertions).find((a) => grownKinds.has(a.kind)) ?? after.assertions.find((a) => grownKinds.has(a.kind));
|
|
1347
|
+
const parts = [];
|
|
1348
|
+
if (dType > 0) parts.push(`+${dType} type assertion${dType === 1 ? "" : "s"} (\`as T\` / \`<T>x\`)`);
|
|
1349
|
+
if (dNonNull > 0) parts.push(`+${dNonNull} non-null assertion${dNonNull === 1 ? "" : "s"} (\`x!\`)`);
|
|
1350
|
+
out2.push(
|
|
1351
|
+
makeFinding(RULE2, policy, {
|
|
1352
|
+
file: c.path,
|
|
1353
|
+
...first ? { line: first.line } : {},
|
|
1354
|
+
message: `The unsafe assertion surface grew: ${parts.join(", ")} net of any removed in this change (now ${after.type} type / ${after.nonNull} non-null in this file).`,
|
|
1355
|
+
evidence: first?.text ?? parts.join(", "),
|
|
1356
|
+
remediation: "Prove the value instead of asserting it: parse or validate at the boundary, use a type guard or discriminated union, or handle the nullable case. If the assertion is genuinely unavoidable, keep it at the narrowest boundary and say why in a comment.",
|
|
1357
|
+
defaultSeverity: "warn"
|
|
1358
|
+
})
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
return out2;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1193
1365
|
// src/detectors/ts-any-cast.ts
|
|
1194
1366
|
var BLOCK_RULE = "ts-any-cast";
|
|
1195
1367
|
var WARN_RULE = "ts-any-launder";
|
|
1196
|
-
var DOUBLE_CAST = /\bas\s
|
|
1368
|
+
var DOUBLE_CAST = /\bas\s+\(*\s*unknown\s*\)*\s+as\b/g;
|
|
1197
1369
|
var SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
|
|
1198
1370
|
var JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
|
|
1199
1371
|
var JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
|
|
1200
1372
|
var countMatches = (s, re) => (s.match(re) || []).length;
|
|
1201
1373
|
function countAny(src) {
|
|
1202
|
-
const r = { cast: 0, broad: 0 };
|
|
1374
|
+
const r = { cast: 0, broad: 0, double: 0 };
|
|
1203
1375
|
try {
|
|
1204
|
-
const sf =
|
|
1376
|
+
const sf = ts2.createSourceFile("f.ts", src, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
1205
1377
|
const visit = (node) => {
|
|
1206
|
-
if (node
|
|
1378
|
+
if ((ts2.isAsExpression(node) || ts2.isTypeAssertionExpression(node)) && isDoubleCast(node)) r.double++;
|
|
1379
|
+
if (node.kind === ts2.SyntaxKind.AnyKeyword) {
|
|
1207
1380
|
let target = node;
|
|
1208
1381
|
let p = target.parent;
|
|
1209
|
-
while (p &&
|
|
1382
|
+
while (p && ts2.isParenthesizedTypeNode(p)) {
|
|
1210
1383
|
target = p;
|
|
1211
1384
|
p = p.parent;
|
|
1212
1385
|
}
|
|
1213
|
-
const isCast = p &&
|
|
1386
|
+
const isCast = p && ts2.isAsExpression(p) && p.type === target || p && ts2.isTypeAssertionExpression(p) && p.type === target;
|
|
1214
1387
|
if (isCast) r.cast++;
|
|
1215
1388
|
else r.broad++;
|
|
1216
1389
|
}
|
|
1217
|
-
|
|
1390
|
+
ts2.forEachChild(node, visit);
|
|
1218
1391
|
};
|
|
1219
1392
|
visit(sf);
|
|
1220
1393
|
} catch {
|
|
1221
1394
|
}
|
|
1222
1395
|
return r;
|
|
1223
1396
|
}
|
|
1224
|
-
var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s
|
|
1397
|
+
var NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|@ts-(?:ignore|expect-error|nocheck)\b/;
|
|
1225
1398
|
var BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
|
|
1399
|
+
function lineHasDoubleCast(line) {
|
|
1400
|
+
let sf;
|
|
1401
|
+
try {
|
|
1402
|
+
sf = ts2.createSourceFile("line.ts", line, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
|
|
1403
|
+
} catch {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
let found = false;
|
|
1407
|
+
const visit = (n) => {
|
|
1408
|
+
if (found) return;
|
|
1409
|
+
if ((ts2.isAsExpression(n) || ts2.isTypeAssertionExpression(n)) && isDoubleCast(n)) {
|
|
1410
|
+
found = true;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
ts2.forEachChild(n, visit);
|
|
1414
|
+
};
|
|
1415
|
+
visit(sf);
|
|
1416
|
+
return found;
|
|
1417
|
+
}
|
|
1226
1418
|
var BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
|
|
1227
1419
|
var WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
|
|
1228
1420
|
var tsAnyCast = {
|
|
@@ -1240,7 +1432,7 @@ var tsAnyCast = {
|
|
|
1240
1432
|
const b = countAny(before);
|
|
1241
1433
|
const dCast = a.cast - b.cast;
|
|
1242
1434
|
const dBroad = a.broad - b.broad;
|
|
1243
|
-
const dDouble = countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST);
|
|
1435
|
+
const dDouble = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
|
|
1244
1436
|
const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
|
|
1245
1437
|
const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
|
|
1246
1438
|
const blockReasons = [];
|
|
@@ -1274,7 +1466,7 @@ var tsAnyCast = {
|
|
|
1274
1466
|
}
|
|
1275
1467
|
const jsdocCast = JS_FILE.test(c.path) ? new RegExp(JSDOC_ANY_CAST.source) : null;
|
|
1276
1468
|
for (const l of addedLines(c)) {
|
|
1277
|
-
if (NARROW_LINE.test(l.content) || jsdocCast && jsdocCast.test(l.content)) {
|
|
1469
|
+
if (NARROW_LINE.test(l.content) || lineHasDoubleCast(l.content) || jsdocCast && jsdocCast.test(l.content)) {
|
|
1278
1470
|
out2.push(
|
|
1279
1471
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
1280
1472
|
file: c.path,
|
|
@@ -1304,7 +1496,7 @@ var tsAnyCast = {
|
|
|
1304
1496
|
};
|
|
1305
1497
|
|
|
1306
1498
|
// src/detectors/lint-suppression.ts
|
|
1307
|
-
var
|
|
1499
|
+
var RULE3 = "lint-suppression";
|
|
1308
1500
|
var PATTERNS = {
|
|
1309
1501
|
js: [
|
|
1310
1502
|
{ re: /eslint-disable(?:-next-line|-line)?\b/, why: "eslint-disable suppresses lint rules" },
|
|
@@ -1352,7 +1544,7 @@ var PATTERNS = {
|
|
|
1352
1544
|
]
|
|
1353
1545
|
};
|
|
1354
1546
|
var lintSuppression = {
|
|
1355
|
-
id:
|
|
1547
|
+
id: RULE3,
|
|
1356
1548
|
surface: ["file"],
|
|
1357
1549
|
certainty: "mechanical",
|
|
1358
1550
|
run(changes, policy) {
|
|
@@ -1368,7 +1560,7 @@ var lintSuppression = {
|
|
|
1368
1560
|
const m = p.re.exec(l.content);
|
|
1369
1561
|
if (!m || insideStringLiteral(l.content, m.index, lang)) continue;
|
|
1370
1562
|
out2.push(
|
|
1371
|
-
makeFinding(
|
|
1563
|
+
makeFinding(RULE3, policy, {
|
|
1372
1564
|
file: c.path,
|
|
1373
1565
|
line: l.newLine ?? void 0,
|
|
1374
1566
|
message: `Lint suppression added: ${p.why}.`,
|
|
@@ -1385,8 +1577,8 @@ var lintSuppression = {
|
|
|
1385
1577
|
};
|
|
1386
1578
|
|
|
1387
1579
|
// src/detectors/test-skip.ts
|
|
1388
|
-
import
|
|
1389
|
-
var
|
|
1580
|
+
import ts3 from "typescript";
|
|
1581
|
+
var RULE4 = "test-skip";
|
|
1390
1582
|
var acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
|
|
1391
1583
|
var JS_RUNNER = "\\b(?:it|test|describe|suite)";
|
|
1392
1584
|
var JS_MOD = "concurrent|sequential|shuffle";
|
|
@@ -1489,11 +1681,11 @@ var JS_TEST_MODULES = /* @__PURE__ */ new Set([
|
|
|
1489
1681
|
"bun:test",
|
|
1490
1682
|
"@playwright/test"
|
|
1491
1683
|
]);
|
|
1492
|
-
function
|
|
1493
|
-
if (/\.tsx$/i.test(path)) return
|
|
1494
|
-
if (/\.jsx$/i.test(path)) return
|
|
1495
|
-
if (/\.(?:js|mjs|cjs)$/i.test(path)) return
|
|
1496
|
-
return
|
|
1684
|
+
function scriptKind2(path) {
|
|
1685
|
+
if (/\.tsx$/i.test(path)) return ts3.ScriptKind.TSX;
|
|
1686
|
+
if (/\.jsx$/i.test(path)) return ts3.ScriptKind.JSX;
|
|
1687
|
+
if (/\.(?:js|mjs|cjs)$/i.test(path)) return ts3.ScriptKind.JS;
|
|
1688
|
+
return ts3.ScriptKind.TS;
|
|
1497
1689
|
}
|
|
1498
1690
|
function addedLineNumbers(c) {
|
|
1499
1691
|
const out2 = /* @__PURE__ */ new Set();
|
|
@@ -1505,32 +1697,32 @@ function addedLineNumbers(c) {
|
|
|
1505
1697
|
return out2;
|
|
1506
1698
|
}
|
|
1507
1699
|
function moduleName(expr) {
|
|
1508
|
-
return
|
|
1700
|
+
return ts3.isStringLiteralLike(expr) ? expr.text : null;
|
|
1509
1701
|
}
|
|
1510
1702
|
function requiredModule(expr) {
|
|
1511
|
-
if (!
|
|
1703
|
+
if (!ts3.isCallExpression(expr) || !ts3.isIdentifier(expr.expression) || expr.expression.text !== "require" || expr.arguments.length !== 1) return null;
|
|
1512
1704
|
return moduleName(expr.arguments[0]);
|
|
1513
1705
|
}
|
|
1514
1706
|
function astContext(path, source) {
|
|
1515
1707
|
const rootName = path.startsWith("/") ? path : "/tamperward/" + path;
|
|
1516
|
-
const sf =
|
|
1708
|
+
const sf = ts3.createSourceFile(
|
|
1517
1709
|
rootName,
|
|
1518
1710
|
source,
|
|
1519
|
-
|
|
1711
|
+
ts3.ScriptTarget.Latest,
|
|
1520
1712
|
true,
|
|
1521
|
-
|
|
1713
|
+
scriptKind2(path)
|
|
1522
1714
|
);
|
|
1523
|
-
const diagnostics = sf.parseDiagnostics
|
|
1715
|
+
const diagnostics = "parseDiagnostics" in sf && Array.isArray(sf.parseDiagnostics) ? sf.parseDiagnostics : [];
|
|
1524
1716
|
if (diagnostics.length > 0) return null;
|
|
1525
1717
|
const options = {
|
|
1526
1718
|
allowJs: true,
|
|
1527
1719
|
checkJs: false,
|
|
1528
1720
|
noLib: true,
|
|
1529
1721
|
noResolve: true,
|
|
1530
|
-
target:
|
|
1531
|
-
module:
|
|
1722
|
+
target: ts3.ScriptTarget.Latest,
|
|
1723
|
+
module: ts3.ModuleKind.ESNext
|
|
1532
1724
|
};
|
|
1533
|
-
const host =
|
|
1725
|
+
const host = ts3.createCompilerHost(options, true);
|
|
1534
1726
|
host.getSourceFile = (fileName) => fileName === rootName ? sf : void 0;
|
|
1535
1727
|
host.fileExists = (fileName) => fileName === rootName;
|
|
1536
1728
|
host.readFile = (fileName) => fileName === rootName ? source : void 0;
|
|
@@ -1541,7 +1733,7 @@ function astContext(path, source) {
|
|
|
1541
1733
|
host.getCanonicalFileName = (fileName) => fileName;
|
|
1542
1734
|
host.useCaseSensitiveFileNames = () => true;
|
|
1543
1735
|
host.getNewLine = () => "\n";
|
|
1544
|
-
const program =
|
|
1736
|
+
const program = ts3.createProgram([rootName], options, host);
|
|
1545
1737
|
return { sf, checker: program.getTypeChecker() };
|
|
1546
1738
|
}
|
|
1547
1739
|
function symbolAt(ctx, node) {
|
|
@@ -1555,13 +1747,13 @@ function importAliases(ctx) {
|
|
|
1555
1747
|
if (sym) out2.set(sym, { causes });
|
|
1556
1748
|
};
|
|
1557
1749
|
for (const stmt of ctx.sf.statements) {
|
|
1558
|
-
if (
|
|
1750
|
+
if (ts3.isImportDeclaration(stmt) && ts3.isStringLiteralLike(stmt.moduleSpecifier)) {
|
|
1559
1751
|
if (!JS_TEST_MODULES.has(stmt.moduleSpecifier.text)) continue;
|
|
1560
1752
|
const clause = stmt.importClause;
|
|
1561
1753
|
if (!clause) continue;
|
|
1562
1754
|
if (clause.name && stmt.moduleSpecifier.text.startsWith("node:test")) add(clause.name);
|
|
1563
1755
|
const bindings = clause.namedBindings;
|
|
1564
|
-
if (bindings &&
|
|
1756
|
+
if (bindings && ts3.isNamedImports(bindings)) {
|
|
1565
1757
|
for (const el of bindings.elements) {
|
|
1566
1758
|
const imported = el.propertyName?.text ?? el.name.text;
|
|
1567
1759
|
if (JS_RUNNERS.has(imported)) add(el.name);
|
|
@@ -1569,17 +1761,17 @@ function importAliases(ctx) {
|
|
|
1569
1761
|
}
|
|
1570
1762
|
continue;
|
|
1571
1763
|
}
|
|
1572
|
-
if (!
|
|
1764
|
+
if (!ts3.isVariableStatement(stmt)) continue;
|
|
1573
1765
|
for (const decl of stmt.declarationList.declarations) {
|
|
1574
|
-
if (
|
|
1766
|
+
if (ts3.isObjectBindingPattern(decl.name) && decl.initializer) {
|
|
1575
1767
|
const mod = requiredModule(decl.initializer);
|
|
1576
1768
|
if (!mod || !JS_TEST_MODULES.has(mod)) continue;
|
|
1577
1769
|
for (const el of decl.name.elements) {
|
|
1578
|
-
if (!
|
|
1579
|
-
const imported = el.propertyName &&
|
|
1770
|
+
if (!ts3.isIdentifier(el.name)) continue;
|
|
1771
|
+
const imported = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
1580
1772
|
if (JS_RUNNERS.has(imported)) add(el.name);
|
|
1581
1773
|
}
|
|
1582
|
-
} else if (
|
|
1774
|
+
} else if (ts3.isIdentifier(decl.name) && decl.initializer && ts3.isIdentifier(decl.initializer)) {
|
|
1583
1775
|
simpleAliases.push({ local: decl.name, source: decl.initializer });
|
|
1584
1776
|
}
|
|
1585
1777
|
}
|
|
@@ -1605,11 +1797,11 @@ function importAliases(ctx) {
|
|
|
1605
1797
|
function topLevelStaticStrings(ctx) {
|
|
1606
1798
|
const pending = /* @__PURE__ */ new Map();
|
|
1607
1799
|
for (const stmt of ctx.sf.statements) {
|
|
1608
|
-
if (!
|
|
1609
|
-
const isConst = (stmt.declarationList.flags &
|
|
1800
|
+
if (!ts3.isVariableStatement(stmt)) continue;
|
|
1801
|
+
const isConst = (stmt.declarationList.flags & ts3.NodeFlags.Const) !== 0;
|
|
1610
1802
|
if (!isConst) continue;
|
|
1611
1803
|
for (const decl of stmt.declarationList.declarations) {
|
|
1612
|
-
if (!
|
|
1804
|
+
if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1613
1805
|
const sym = symbolAt(ctx, decl.name);
|
|
1614
1806
|
if (sym) pending.set(sym, { decl: decl.name, expr: decl.initializer });
|
|
1615
1807
|
}
|
|
@@ -1617,15 +1809,15 @@ function topLevelStaticStrings(ctx) {
|
|
|
1617
1809
|
const resolved = /* @__PURE__ */ new Map();
|
|
1618
1810
|
const resolving = /* @__PURE__ */ new Set();
|
|
1619
1811
|
const valueOf = (expr) => {
|
|
1620
|
-
if (
|
|
1621
|
-
if (
|
|
1622
|
-
if (
|
|
1812
|
+
if (ts3.isStringLiteralLike(expr)) return { value: expr.text, causes: [] };
|
|
1813
|
+
if (ts3.isParenthesizedExpression(expr)) return valueOf(expr.expression);
|
|
1814
|
+
if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
|
|
1623
1815
|
const left = valueOf(expr.left);
|
|
1624
1816
|
const right = valueOf(expr.right);
|
|
1625
1817
|
if (!left || !right) return null;
|
|
1626
1818
|
return { value: left.value + right.value, causes: [...left.causes, ...right.causes] };
|
|
1627
1819
|
}
|
|
1628
|
-
if (
|
|
1820
|
+
if (ts3.isIdentifier(expr)) {
|
|
1629
1821
|
const sym = symbolAt(ctx, expr);
|
|
1630
1822
|
if (!sym) return null;
|
|
1631
1823
|
const cached = resolved.get(sym);
|
|
@@ -1652,15 +1844,15 @@ function topLevelStaticStrings(ctx) {
|
|
|
1652
1844
|
return resolved;
|
|
1653
1845
|
}
|
|
1654
1846
|
function staticPropertyName(expr, ctx, strings) {
|
|
1655
|
-
if (
|
|
1656
|
-
if (
|
|
1657
|
-
if (
|
|
1847
|
+
if (ts3.isStringLiteralLike(expr)) return { name: expr.text, causes: [] };
|
|
1848
|
+
if (ts3.isParenthesizedExpression(expr)) return staticPropertyName(expr.expression, ctx, strings);
|
|
1849
|
+
if (ts3.isBinaryExpression(expr) && expr.operatorToken.kind === ts3.SyntaxKind.PlusToken) {
|
|
1658
1850
|
const left = staticPropertyName(expr.left, ctx, strings);
|
|
1659
1851
|
const right = staticPropertyName(expr.right, ctx, strings);
|
|
1660
1852
|
if (!left || !right) return null;
|
|
1661
1853
|
return { name: left.name + right.name, causes: [...left.causes, ...right.causes] };
|
|
1662
1854
|
}
|
|
1663
|
-
if (
|
|
1855
|
+
if (ts3.isIdentifier(expr)) {
|
|
1664
1856
|
const sym = symbolAt(ctx, expr);
|
|
1665
1857
|
if (!sym) return null;
|
|
1666
1858
|
const value = strings.get(sym);
|
|
@@ -1673,8 +1865,8 @@ function runnerChain(expr, ctx, runners, strings) {
|
|
|
1673
1865
|
const causes = [];
|
|
1674
1866
|
let cur = expr;
|
|
1675
1867
|
let terminalNode = expr;
|
|
1676
|
-
while (
|
|
1677
|
-
if (
|
|
1868
|
+
while (ts3.isPropertyAccessExpression(cur) || ts3.isElementAccessExpression(cur)) {
|
|
1869
|
+
if (ts3.isPropertyAccessExpression(cur)) {
|
|
1678
1870
|
props.unshift(cur.name.text);
|
|
1679
1871
|
if (props.length === 1) terminalNode = cur.name;
|
|
1680
1872
|
cur = cur.expression;
|
|
@@ -1688,7 +1880,7 @@ function runnerChain(expr, ctx, runners, strings) {
|
|
|
1688
1880
|
if (props.length === 1) terminalNode = cur.argumentExpression;
|
|
1689
1881
|
cur = cur.expression;
|
|
1690
1882
|
}
|
|
1691
|
-
if (!
|
|
1883
|
+
if (!ts3.isIdentifier(cur)) return null;
|
|
1692
1884
|
const sym = symbolAt(ctx, cur);
|
|
1693
1885
|
if (sym) {
|
|
1694
1886
|
const binding = runners.get(sym);
|
|
@@ -1700,17 +1892,17 @@ function runnerChain(expr, ctx, runners, strings) {
|
|
|
1700
1892
|
return { root: cur.text, props, terminalNode, causes };
|
|
1701
1893
|
}
|
|
1702
1894
|
function optionDisables(value) {
|
|
1703
|
-
return !(value.kind ===
|
|
1895
|
+
return !(value.kind === ts3.SyntaxKind.FalseKeyword || ts3.isNumericLiteral(value) && Number(value.text) === 0);
|
|
1704
1896
|
}
|
|
1705
1897
|
function shorthandOptionDisables(prop, ctx) {
|
|
1706
1898
|
const sym = ctx.checker.getShorthandAssignmentValueSymbol(prop) ?? symbolAt(ctx, prop.name);
|
|
1707
|
-
const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) =>
|
|
1708
|
-
if (!decl || !
|
|
1899
|
+
const decl = sym?.valueDeclaration ?? sym?.declarations?.find((d) => ts3.isVariableDeclaration(d));
|
|
1900
|
+
if (!decl || !ts3.isVariableDeclaration(decl) || !decl.initializer) return true;
|
|
1709
1901
|
return optionDisables(decl.initializer);
|
|
1710
1902
|
}
|
|
1711
1903
|
function structuralNodeKey(node, sf) {
|
|
1712
1904
|
const children = node.getChildren(sf);
|
|
1713
|
-
const text = node.text;
|
|
1905
|
+
const text = "text" in node ? node.text : void 0;
|
|
1714
1906
|
if (children.length === 0) {
|
|
1715
1907
|
return typeof text === "string" ? `${node.kind}:${JSON.stringify(text)}` : String(node.kind);
|
|
1716
1908
|
}
|
|
@@ -1722,7 +1914,7 @@ function semanticSkipHits(ctx) {
|
|
|
1722
1914
|
const hits = [];
|
|
1723
1915
|
const callOrdinals = /* @__PURE__ */ new Map();
|
|
1724
1916
|
const visit = (node) => {
|
|
1725
|
-
if (
|
|
1917
|
+
if (ts3.isCallExpression(node)) {
|
|
1726
1918
|
const callText = node.getText(ctx.sf).trim();
|
|
1727
1919
|
const callIdentity = structuralNodeKey(node, ctx.sf);
|
|
1728
1920
|
const ordinal = (callOrdinals.get(callIdentity) ?? 0) + 1;
|
|
@@ -1747,19 +1939,19 @@ function semanticSkipHits(ctx) {
|
|
|
1747
1939
|
push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
|
|
1748
1940
|
} else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
|
|
1749
1941
|
push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
|
|
1750
|
-
} else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 &&
|
|
1942
|
+
} else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts3.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
|
|
1751
1943
|
push(terminalNode, [], "an empty .each table (no case ever runs)");
|
|
1752
1944
|
}
|
|
1753
1945
|
if (props.length === 0) {
|
|
1754
1946
|
for (const arg of node.arguments) {
|
|
1755
|
-
if (!
|
|
1947
|
+
if (!ts3.isObjectLiteralExpression(arg)) continue;
|
|
1756
1948
|
for (const prop of arg.properties) {
|
|
1757
|
-
if (
|
|
1949
|
+
if (ts3.isPropertyAssignment(prop)) {
|
|
1758
1950
|
let name = null;
|
|
1759
1951
|
let propertyCauses = [];
|
|
1760
|
-
if (
|
|
1952
|
+
if (ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name)) {
|
|
1761
1953
|
name = prop.name.text;
|
|
1762
|
-
} else if (
|
|
1954
|
+
} else if (ts3.isComputedPropertyName(prop.name)) {
|
|
1763
1955
|
const resolved = staticPropertyName(prop.name.expression, ctx, strings);
|
|
1764
1956
|
name = resolved?.name ?? null;
|
|
1765
1957
|
propertyCauses = resolved?.causes ?? [];
|
|
@@ -1768,7 +1960,7 @@ function semanticSkipHits(ctx) {
|
|
|
1768
1960
|
if (!optionDisables(prop.initializer)) continue;
|
|
1769
1961
|
const why = name === "only" ? "an { only: ... } option focusing the test" : name === "todo" ? "a { todo: ... } option (the test no longer fails the run)" : "a { skip: ... } option disabling the test";
|
|
1770
1962
|
push(prop.name, propertyCauses, why);
|
|
1771
|
-
} else if (
|
|
1963
|
+
} else if (ts3.isShorthandPropertyAssignment(prop) && ["skip", "todo", "only"].includes(prop.name.text)) {
|
|
1772
1964
|
if (!shorthandOptionDisables(prop, ctx)) continue;
|
|
1773
1965
|
push(prop.name, [], `a { ${prop.name.text} } option that conditionally narrows the test run`);
|
|
1774
1966
|
}
|
|
@@ -1777,7 +1969,7 @@ function semanticSkipHits(ctx) {
|
|
|
1777
1969
|
}
|
|
1778
1970
|
}
|
|
1779
1971
|
}
|
|
1780
|
-
|
|
1972
|
+
ts3.forEachChild(node, visit);
|
|
1781
1973
|
};
|
|
1782
1974
|
visit(ctx.sf);
|
|
1783
1975
|
return hits;
|
|
@@ -1816,7 +2008,7 @@ function astSkipHits(c) {
|
|
|
1816
2008
|
return { hits, authoritative: true };
|
|
1817
2009
|
}
|
|
1818
2010
|
var testSkip = {
|
|
1819
|
-
id:
|
|
2011
|
+
id: RULE4,
|
|
1820
2012
|
surface: ["file"],
|
|
1821
2013
|
certainty: "mechanical",
|
|
1822
2014
|
run(changes, policy) {
|
|
@@ -1834,7 +2026,7 @@ var testSkip = {
|
|
|
1834
2026
|
for (const hit of analysis.hits) {
|
|
1835
2027
|
astHitLines.add(hit.line);
|
|
1836
2028
|
out2.push(
|
|
1837
|
-
makeFinding(
|
|
2029
|
+
makeFinding(RULE4, policy, {
|
|
1838
2030
|
file: c.path,
|
|
1839
2031
|
line: hit.line,
|
|
1840
2032
|
message: `Test skipped or narrowed: ${hit.why}.`,
|
|
@@ -1852,7 +2044,7 @@ var testSkip = {
|
|
|
1852
2044
|
if (comment && !p.comment) continue;
|
|
1853
2045
|
if (matchesOutsideString(p, l.content, lang)) {
|
|
1854
2046
|
out2.push(
|
|
1855
|
-
makeFinding(
|
|
2047
|
+
makeFinding(RULE4, policy, {
|
|
1856
2048
|
file: c.path,
|
|
1857
2049
|
line: l.newLine ?? void 0,
|
|
1858
2050
|
message: `Test skipped or narrowed: ${p.why}.`,
|
|
@@ -1871,7 +2063,7 @@ var testSkip = {
|
|
|
1871
2063
|
|
|
1872
2064
|
// src/detectors/coverage-lowering.ts
|
|
1873
2065
|
import picomatch2 from "picomatch";
|
|
1874
|
-
import
|
|
2066
|
+
import ts4 from "typescript";
|
|
1875
2067
|
|
|
1876
2068
|
// src/detectors/repo.ts
|
|
1877
2069
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
@@ -1994,34 +2186,34 @@ function containsProtected(token, policy, category, ctx, except) {
|
|
|
1994
2186
|
}
|
|
1995
2187
|
|
|
1996
2188
|
// src/detectors/coverage-lowering.ts
|
|
1997
|
-
var
|
|
2189
|
+
var RULE5 = "coverage-lowering";
|
|
1998
2190
|
var METRICS = ["branches", "functions", "lines", "statements"];
|
|
1999
|
-
var isMetric = (k) => METRICS.
|
|
2191
|
+
var isMetric = (k) => METRICS.some((m) => m === k);
|
|
2000
2192
|
var norm = (p) => p.replace(/^\.\//, "");
|
|
2001
2193
|
function keyName(name) {
|
|
2002
|
-
if (
|
|
2194
|
+
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
2003
2195
|
return null;
|
|
2004
2196
|
}
|
|
2005
2197
|
function numericOf(e) {
|
|
2006
|
-
if (
|
|
2007
|
-
if (
|
|
2008
|
-
if (
|
|
2198
|
+
if (ts4.isNumericLiteral(e)) return Number(e.text);
|
|
2199
|
+
if (ts4.isStringLiteral(e) && /^-?\d+(?:\.\d+)?$/.test(e.text.trim())) return Number(e.text);
|
|
2200
|
+
if (ts4.isPrefixUnaryExpression(e) && e.operator === ts4.SyntaxKind.MinusToken && ts4.isNumericLiteral(e.operand)) {
|
|
2009
2201
|
return -Number(e.operand.text);
|
|
2010
2202
|
}
|
|
2011
2203
|
return void 0;
|
|
2012
2204
|
}
|
|
2013
2205
|
function metricsOf(expr, resolve10) {
|
|
2014
2206
|
const obj = resolve10(expr);
|
|
2015
|
-
if (!
|
|
2207
|
+
if (!ts4.isObjectLiteralExpression(obj)) return void 0;
|
|
2016
2208
|
const out2 = { values: {}, opaque: false };
|
|
2017
2209
|
let any = false;
|
|
2018
2210
|
for (const p of obj.properties) {
|
|
2019
|
-
if (
|
|
2211
|
+
if (ts4.isSpreadAssignment(p)) {
|
|
2020
2212
|
out2.opaque = true;
|
|
2021
2213
|
any = true;
|
|
2022
2214
|
continue;
|
|
2023
2215
|
}
|
|
2024
|
-
if (!
|
|
2216
|
+
if (!ts4.isPropertyAssignment(p)) continue;
|
|
2025
2217
|
const k = keyName(p.name);
|
|
2026
2218
|
if (k === null || !isMetric(k)) continue;
|
|
2027
2219
|
any = true;
|
|
@@ -2033,34 +2225,34 @@ function metricsOf(expr, resolve10) {
|
|
|
2033
2225
|
}
|
|
2034
2226
|
function underKey(node, key2) {
|
|
2035
2227
|
for (let p = node.parent; p; p = p.parent) {
|
|
2036
|
-
if (
|
|
2228
|
+
if (ts4.isPropertyAssignment(p) && keyName(p.name) === key2) return true;
|
|
2037
2229
|
}
|
|
2038
2230
|
return false;
|
|
2039
2231
|
}
|
|
2040
2232
|
function ownerKey(node) {
|
|
2041
2233
|
const obj = node.parent;
|
|
2042
|
-
if (!obj || !
|
|
2234
|
+
if (!obj || !ts4.isObjectLiteralExpression(obj)) return null;
|
|
2043
2235
|
let q = obj.parent;
|
|
2044
|
-
while (q && (
|
|
2045
|
-
return q &&
|
|
2236
|
+
while (q && (ts4.isParenthesizedExpression(q) || ts4.isAsExpression(q) || q.kind === ts4.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
2237
|
+
return q && ts4.isPropertyAssignment(q) ? keyName(q.name) : null;
|
|
2046
2238
|
}
|
|
2047
2239
|
function asExpression(src) {
|
|
2048
2240
|
const t = src.trim();
|
|
2049
2241
|
return t.startsWith("{") ? `(${t})` : src;
|
|
2050
2242
|
}
|
|
2051
2243
|
function sourceOf(src) {
|
|
2052
|
-
return
|
|
2244
|
+
return ts4.createSourceFile("cfg.ts", asExpression(src), ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TS);
|
|
2053
2245
|
}
|
|
2054
2246
|
function resolverFor(sf) {
|
|
2055
2247
|
const consts = /* @__PURE__ */ new Map();
|
|
2056
2248
|
for (const st of sf.statements) {
|
|
2057
|
-
if (!
|
|
2249
|
+
if (!ts4.isVariableStatement(st)) continue;
|
|
2058
2250
|
for (const d of st.declarationList.declarations) {
|
|
2059
|
-
if (
|
|
2251
|
+
if (ts4.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
|
|
2060
2252
|
}
|
|
2061
2253
|
}
|
|
2062
2254
|
return (e) => {
|
|
2063
|
-
if (
|
|
2255
|
+
if (ts4.isIdentifier(e)) {
|
|
2064
2256
|
const bound = consts.get(e.text);
|
|
2065
2257
|
return bound ?? e;
|
|
2066
2258
|
}
|
|
@@ -2073,25 +2265,25 @@ function parseThresholds(src) {
|
|
|
2073
2265
|
const sf = sourceOf(src);
|
|
2074
2266
|
const resolve10 = resolverFor(sf);
|
|
2075
2267
|
const visit = (node) => {
|
|
2076
|
-
if (
|
|
2268
|
+
if (ts4.isPropertyAssignment(node)) {
|
|
2077
2269
|
const key2 = keyName(node.name);
|
|
2078
2270
|
if (key2 === "coverageThreshold" || key2 === "thresholds" && underKey(node, "coverage")) {
|
|
2079
2271
|
res.present = true;
|
|
2080
2272
|
const init = resolve10(node.initializer);
|
|
2081
|
-
if (!
|
|
2273
|
+
if (!ts4.isObjectLiteralExpression(init)) {
|
|
2082
2274
|
res.opaque = true;
|
|
2083
2275
|
} else {
|
|
2084
2276
|
const flat = metricsOf(init, resolve10);
|
|
2085
2277
|
if (flat) res.global = merge(res.global, flat);
|
|
2086
2278
|
for (const p of init.properties) {
|
|
2087
|
-
if (
|
|
2279
|
+
if (ts4.isSpreadAssignment(p)) {
|
|
2088
2280
|
res.opaque = true;
|
|
2089
2281
|
continue;
|
|
2090
2282
|
}
|
|
2091
|
-
if (!
|
|
2283
|
+
if (!ts4.isPropertyAssignment(p)) continue;
|
|
2092
2284
|
const k = keyName(p.name);
|
|
2093
2285
|
if (k === null || isMetric(k)) continue;
|
|
2094
|
-
if (key2 === "thresholds" && k === "100" && p.initializer.kind ===
|
|
2286
|
+
if (key2 === "thresholds" && k === "100" && p.initializer.kind === ts4.SyntaxKind.TrueKeyword) {
|
|
2095
2287
|
res.global = merge(res.global, { values: { branches: 100, functions: 100, lines: 100, statements: 100 }, opaque: false });
|
|
2096
2288
|
continue;
|
|
2097
2289
|
}
|
|
@@ -2103,7 +2295,7 @@ function parseThresholds(src) {
|
|
|
2103
2295
|
}
|
|
2104
2296
|
}
|
|
2105
2297
|
}
|
|
2106
|
-
|
|
2298
|
+
ts4.forEachChild(node, visit);
|
|
2107
2299
|
};
|
|
2108
2300
|
visit(sf);
|
|
2109
2301
|
} catch {
|
|
@@ -2114,12 +2306,12 @@ function merge(a, b) {
|
|
|
2114
2306
|
return { values: { ...a?.values ?? {}, ...b.values }, opaque: (a?.opaque ?? false) || b.opaque };
|
|
2115
2307
|
}
|
|
2116
2308
|
function literals(e) {
|
|
2117
|
-
if (
|
|
2118
|
-
if (!
|
|
2309
|
+
if (ts4.isStringLiteral(e)) return { items: [e.text], opaque: false };
|
|
2310
|
+
if (!ts4.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
2119
2311
|
const items = [];
|
|
2120
2312
|
let opaque = false;
|
|
2121
2313
|
for (const el of e.elements) {
|
|
2122
|
-
if (
|
|
2314
|
+
if (ts4.isStringLiteral(el) || ts4.isNoSubstitutionTemplateLiteral(el)) items.push(el.text);
|
|
2123
2315
|
else opaque = true;
|
|
2124
2316
|
}
|
|
2125
2317
|
return { items, opaque };
|
|
@@ -2130,7 +2322,7 @@ function parseLists(src) {
|
|
|
2130
2322
|
const sf = sourceOf(src);
|
|
2131
2323
|
const resolve10 = resolverFor(sf);
|
|
2132
2324
|
const visit = (node) => {
|
|
2133
|
-
if (
|
|
2325
|
+
if (ts4.isPropertyAssignment(node)) {
|
|
2134
2326
|
const k = keyName(node.name);
|
|
2135
2327
|
const vitestCoverage = ownerKey(node) === "coverage";
|
|
2136
2328
|
if (k === "collectCoverageFrom" || k === "include" && vitestCoverage) {
|
|
@@ -2144,7 +2336,7 @@ function parseLists(src) {
|
|
|
2144
2336
|
if (opaque) res.opaqueIgnore = true;
|
|
2145
2337
|
}
|
|
2146
2338
|
}
|
|
2147
|
-
|
|
2339
|
+
ts4.forEachChild(node, visit);
|
|
2148
2340
|
};
|
|
2149
2341
|
visit(sf);
|
|
2150
2342
|
} catch {
|
|
@@ -2155,15 +2347,15 @@ function parseSwitches(src) {
|
|
|
2155
2347
|
const res = {};
|
|
2156
2348
|
try {
|
|
2157
2349
|
const sf = sourceOf(src);
|
|
2158
|
-
const bool = (e) => e.kind ===
|
|
2350
|
+
const bool = (e) => e.kind === ts4.SyntaxKind.TrueKeyword ? true : e.kind === ts4.SyntaxKind.FalseKeyword ? false : void 0;
|
|
2159
2351
|
const visit = (node) => {
|
|
2160
|
-
if (
|
|
2352
|
+
if (ts4.isPropertyAssignment(node)) {
|
|
2161
2353
|
const k = keyName(node.name);
|
|
2162
2354
|
const owner = ownerKey(node);
|
|
2163
2355
|
if (k === "enabled" && owner === "coverage") res.enabled = bool(node.initializer);
|
|
2164
2356
|
else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool(node.initializer);
|
|
2165
2357
|
}
|
|
2166
|
-
|
|
2358
|
+
ts4.forEachChild(node, visit);
|
|
2167
2359
|
};
|
|
2168
2360
|
visit(sf);
|
|
2169
2361
|
} catch {
|
|
@@ -2176,11 +2368,11 @@ function sectionText(src, key2) {
|
|
|
2176
2368
|
let out2 = "";
|
|
2177
2369
|
const visit = (node) => {
|
|
2178
2370
|
if (out2) return;
|
|
2179
|
-
if (
|
|
2371
|
+
if (ts4.isPropertyAssignment(node) && keyName(node.name) === key2 && ts4.isObjectLiteralExpression(node.initializer)) {
|
|
2180
2372
|
out2 = node.initializer.getText(sf);
|
|
2181
2373
|
return;
|
|
2182
2374
|
}
|
|
2183
|
-
|
|
2375
|
+
ts4.forEachChild(node, visit);
|
|
2184
2376
|
};
|
|
2185
2377
|
visit(sf);
|
|
2186
2378
|
return out2;
|
|
@@ -2517,7 +2709,7 @@ function gateMovedElsewhere(c, changes, policy) {
|
|
|
2517
2709
|
});
|
|
2518
2710
|
}
|
|
2519
2711
|
var coverageLowering = {
|
|
2520
|
-
id:
|
|
2712
|
+
id: RULE5,
|
|
2521
2713
|
surface: ["file"],
|
|
2522
2714
|
certainty: "mechanical",
|
|
2523
2715
|
run(changes, policy, _view, ctx) {
|
|
@@ -2539,7 +2731,7 @@ var coverageLowering = {
|
|
|
2539
2731
|
reasons.push(...simpleNarrowings(c.path, c.before ?? "", c.after));
|
|
2540
2732
|
for (const reason of reasons) {
|
|
2541
2733
|
out2.push(
|
|
2542
|
-
makeFinding(
|
|
2734
|
+
makeFinding(RULE5, policy, {
|
|
2543
2735
|
file: c.path,
|
|
2544
2736
|
message: `Coverage gate weakened: ${reason}.`,
|
|
2545
2737
|
evidence: reason,
|
|
@@ -2552,7 +2744,7 @@ var coverageLowering = {
|
|
|
2552
2744
|
const gated = isJsConfig(c.path) ? parseThresholds(before).present : simpleWeakenings(c.path, before, "").length > 0 || SIMPLE_FILES.some((s) => s.file.test(c.path) && s.keys.some((k) => numbersOf(before, k.key).length));
|
|
2553
2745
|
if (gated && !gateMovedElsewhere(c, changes, policy)) {
|
|
2554
2746
|
out2.push(
|
|
2555
|
-
makeFinding(
|
|
2747
|
+
makeFinding(RULE5, policy, {
|
|
2556
2748
|
file: c.path,
|
|
2557
2749
|
message: "Coverage gate weakened: the coverage config was deleted.",
|
|
2558
2750
|
evidence: c.path,
|
|
@@ -2566,7 +2758,7 @@ var coverageLowering = {
|
|
|
2566
2758
|
const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
|
|
2567
2759
|
if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
|
|
2568
2760
|
out2.push(
|
|
2569
|
-
makeFinding(
|
|
2761
|
+
makeFinding(RULE5, policy, {
|
|
2570
2762
|
file: c.path,
|
|
2571
2763
|
message: "The --coverage flag was removed from a test script.",
|
|
2572
2764
|
evidence: removedCoverage.content.trim(),
|
|
@@ -2580,7 +2772,7 @@ var coverageLowering = {
|
|
|
2580
2772
|
if (removed.some((r) => s.re.test(r.content))) continue;
|
|
2581
2773
|
if (/passWithNoTests/.test(s.re.source) && dirHasSpec(c.path, policy, ctx) === false) continue;
|
|
2582
2774
|
out2.push(
|
|
2583
|
-
makeFinding(
|
|
2775
|
+
makeFinding(RULE5, policy, {
|
|
2584
2776
|
file: c.path,
|
|
2585
2777
|
line: l.newLine ?? void 0,
|
|
2586
2778
|
message: s.message,
|
|
@@ -2603,7 +2795,7 @@ var coverageLowering = {
|
|
|
2603
2795
|
const ov = oldNums.get(m[1]);
|
|
2604
2796
|
if (ov !== void 0 && Number(m[2]) < ov) {
|
|
2605
2797
|
out2.push(
|
|
2606
|
-
makeFinding(
|
|
2798
|
+
makeFinding(RULE5, policy, {
|
|
2607
2799
|
file: c.path,
|
|
2608
2800
|
line: l.newLine ?? void 0,
|
|
2609
2801
|
message: `Coverage threshold for ${m[1]} lowered ${ov} \u2192 ${m[2]}.`,
|
|
@@ -2626,7 +2818,7 @@ var coverageLowering = {
|
|
|
2626
2818
|
};
|
|
2627
2819
|
|
|
2628
2820
|
// src/detectors/ci-tampering.ts
|
|
2629
|
-
var
|
|
2821
|
+
var RULE6 = "ci-tampering";
|
|
2630
2822
|
var USES = /^\s*-\s*uses:/;
|
|
2631
2823
|
var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
2632
2824
|
var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
@@ -2890,7 +3082,7 @@ function isTruthy(raw) {
|
|
|
2890
3082
|
return f !== void 0 && truthy(f);
|
|
2891
3083
|
}
|
|
2892
3084
|
var SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
|
|
2893
|
-
var indentOf = (l) => l.
|
|
3085
|
+
var indentOf = (l) => l.length - l.trimStart().length;
|
|
2894
3086
|
var STEP_START = /^\s*-\s+(?:name|uses|run|if|id|with|env|shell|continue-on-error|working-directory|timeout-minutes):/;
|
|
2895
3087
|
function stepOf(lines, i) {
|
|
2896
3088
|
const ind = indentOf(lines[i]);
|
|
@@ -3072,7 +3264,7 @@ function triggerNarrowings(before, after, opts) {
|
|
|
3072
3264
|
}
|
|
3073
3265
|
var REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
|
|
3074
3266
|
var ciTampering = {
|
|
3075
|
-
id:
|
|
3267
|
+
id: RULE6,
|
|
3076
3268
|
surface: ["file"],
|
|
3077
3269
|
certainty: "mechanical",
|
|
3078
3270
|
run(changes, policy, _view, ctx) {
|
|
@@ -3084,7 +3276,7 @@ var ciTampering = {
|
|
|
3084
3276
|
if (c.kind !== "file") continue;
|
|
3085
3277
|
if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
|
|
3086
3278
|
out2.push(
|
|
3087
|
-
makeFinding(
|
|
3279
|
+
makeFinding(RULE6, policy, {
|
|
3088
3280
|
file: c.path,
|
|
3089
3281
|
message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
|
|
3090
3282
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -3108,7 +3300,7 @@ var ciTampering = {
|
|
|
3108
3300
|
}
|
|
3109
3301
|
if (coe && isTruthy(coe[1])) {
|
|
3110
3302
|
out2.push(
|
|
3111
|
-
makeFinding(
|
|
3303
|
+
makeFinding(RULE6, policy, {
|
|
3112
3304
|
file: c.path,
|
|
3113
3305
|
line: l.newLine ?? void 0,
|
|
3114
3306
|
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
@@ -3118,7 +3310,7 @@ var ciTampering = {
|
|
|
3118
3310
|
);
|
|
3119
3311
|
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
3120
3312
|
out2.push(
|
|
3121
|
-
makeFinding(
|
|
3313
|
+
makeFinding(RULE6, policy, {
|
|
3122
3314
|
file: c.path,
|
|
3123
3315
|
line: l.newLine ?? void 0,
|
|
3124
3316
|
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
@@ -3138,7 +3330,7 @@ var ciTampering = {
|
|
|
3138
3330
|
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
3139
3331
|
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
3140
3332
|
out2.push(
|
|
3141
|
-
makeFinding(
|
|
3333
|
+
makeFinding(RULE6, policy, {
|
|
3142
3334
|
file: c.path,
|
|
3143
3335
|
line: idx + 1,
|
|
3144
3336
|
message: `A check's run block was neutralised: ${n.what}.`,
|
|
@@ -3154,7 +3346,7 @@ var ciTampering = {
|
|
|
3154
3346
|
const step = stepOf(afterLines, l.newLine - 1);
|
|
3155
3347
|
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
3156
3348
|
out2.push(
|
|
3157
|
-
makeFinding(
|
|
3349
|
+
makeFinding(RULE6, policy, {
|
|
3158
3350
|
file: c.path,
|
|
3159
3351
|
line: l.newLine,
|
|
3160
3352
|
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
@@ -3163,11 +3355,11 @@ var ciTampering = {
|
|
|
3163
3355
|
})
|
|
3164
3356
|
);
|
|
3165
3357
|
}
|
|
3166
|
-
if (c.before != null) {
|
|
3358
|
+
if (c.before != null && c.after != null) {
|
|
3167
3359
|
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
3168
3360
|
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
3169
3361
|
out2.push(
|
|
3170
|
-
makeFinding(
|
|
3362
|
+
makeFinding(RULE6, policy, {
|
|
3171
3363
|
file: c.path,
|
|
3172
3364
|
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
3173
3365
|
evidence: reason,
|
|
@@ -3196,7 +3388,7 @@ var ciTampering = {
|
|
|
3196
3388
|
if (s.state === "kept") continue;
|
|
3197
3389
|
if (s.state === "neutralised") {
|
|
3198
3390
|
out2.push(
|
|
3199
|
-
makeFinding(
|
|
3391
|
+
makeFinding(RULE6, policy, {
|
|
3200
3392
|
file: c.path,
|
|
3201
3393
|
message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
|
|
3202
3394
|
evidence: `${l.content.trim()} \u2192 ${s.by}`,
|
|
@@ -3206,7 +3398,7 @@ var ciTampering = {
|
|
|
3206
3398
|
continue;
|
|
3207
3399
|
}
|
|
3208
3400
|
out2.push(
|
|
3209
|
-
makeFinding(
|
|
3401
|
+
makeFinding(RULE6, policy, {
|
|
3210
3402
|
file: c.path,
|
|
3211
3403
|
message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
|
|
3212
3404
|
evidence: l.content.trim(),
|
|
@@ -3235,7 +3427,7 @@ function shippedVersion() {
|
|
|
3235
3427
|
for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
|
|
3236
3428
|
try {
|
|
3237
3429
|
const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf8"));
|
|
3238
|
-
if (pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
|
|
3430
|
+
if (isRecord(pkg) && pkg.name === "tamperward" && typeof pkg.version === "string" && PLAIN_SEMVER.test(pkg.version)) {
|
|
3239
3431
|
return pkg.version;
|
|
3240
3432
|
}
|
|
3241
3433
|
} catch {
|
|
@@ -3319,11 +3511,25 @@ function resolvesToClaudeSettings(path, env = process.env) {
|
|
|
3319
3511
|
|
|
3320
3512
|
// src/detectors/policy-diff.ts
|
|
3321
3513
|
import { parse } from "yaml";
|
|
3514
|
+
function verifyBlock(v) {
|
|
3515
|
+
if (!isRecord(v)) return void 0;
|
|
3516
|
+
return {
|
|
3517
|
+
...typeof v.command === "string" ? { command: v.command } : {},
|
|
3518
|
+
...typeof v.budget === "number" ? { budget: v.budget } : {},
|
|
3519
|
+
...Array.isArray(v.inputs) ? { inputs: v.inputs.filter((g) => typeof g === "string") } : {},
|
|
3520
|
+
...typeof v.backend === "string" ? { backend: v.backend } : {},
|
|
3521
|
+
...typeof v.image === "string" ? { image: v.image } : {}
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
function ruleOverrides(v) {
|
|
3525
|
+
if (!isRecord(v)) return {};
|
|
3526
|
+
return Object.fromEntries(Object.entries(v).map(([name, cfg]) => [name, isRecord(cfg) ? cfg : {}]));
|
|
3527
|
+
}
|
|
3322
3528
|
var globs2 = (list2) => Array.isArray(list2) ? list2.filter((g) => typeof g === "string").map(normalizeGlob) : [];
|
|
3323
3529
|
function safeParse(src) {
|
|
3324
3530
|
try {
|
|
3325
3531
|
const v = parse(src);
|
|
3326
|
-
if (
|
|
3532
|
+
if (isRecord(v)) return v;
|
|
3327
3533
|
return null;
|
|
3328
3534
|
} catch {
|
|
3329
3535
|
return null;
|
|
@@ -3334,18 +3540,24 @@ function effective(raw) {
|
|
|
3334
3540
|
const version = typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : 1;
|
|
3335
3541
|
const base = defaultPolicy(version);
|
|
3336
3542
|
const rules = { ...base.rules };
|
|
3337
|
-
for (const [name,
|
|
3338
|
-
|
|
3339
|
-
|
|
3543
|
+
for (const [name, over] of Object.entries(ruleOverrides(raw.rules))) {
|
|
3544
|
+
rules[name] = {
|
|
3545
|
+
...rules[name] ?? {},
|
|
3546
|
+
...over.severity !== void 0 ? { severity: over.severity } : {},
|
|
3547
|
+
...over.enabled !== void 0 ? { enabled: over.enabled } : {},
|
|
3548
|
+
...over.exclude !== void 0 ? { exclude: globs2(over.exclude) } : {}
|
|
3549
|
+
};
|
|
3340
3550
|
}
|
|
3341
|
-
const userProtected = raw.protected
|
|
3551
|
+
const userProtected = isRecord(raw.protected) ? Object.fromEntries(Object.entries(raw.protected).map(([cat, list2]) => [cat, globs2(list2)])) : void 0;
|
|
3552
|
+
const signoff = isRecord(raw.signoff) ? raw.signoff : {};
|
|
3553
|
+
const requiredFor = [signoff.required_for, signoff.requiredFor].find(Array.isArray);
|
|
3342
3554
|
return {
|
|
3343
3555
|
version,
|
|
3344
3556
|
rules,
|
|
3345
3557
|
ignore: raw.ignore ? globs2(raw.ignore) : base.ignore ?? [],
|
|
3346
3558
|
protected: mergeProtected(base.protected, userProtected),
|
|
3347
|
-
requiredFor:
|
|
3348
|
-
ledger: typeof
|
|
3559
|
+
requiredFor: requiredFor ? requiredFor.filter((s) => typeof s === "string") : base.signoff.requiredFor,
|
|
3560
|
+
ledger: typeof signoff.ledger === "string" ? signoff.ledger : base.signoff.ledger
|
|
3349
3561
|
};
|
|
3350
3562
|
}
|
|
3351
3563
|
function policyWeakening(before, after) {
|
|
@@ -3400,8 +3612,8 @@ function policyWeakening(before, after) {
|
|
|
3400
3612
|
if (ae.ledger !== be.ledger) {
|
|
3401
3613
|
reasons.push(`sign-off ledger moved (${be.ledger} \u2192 ${ae.ledger}) \u2014 the local layer would trust a different file`);
|
|
3402
3614
|
}
|
|
3403
|
-
const bv = b.verify;
|
|
3404
|
-
const av = a.verify;
|
|
3615
|
+
const bv = verifyBlock(b.verify);
|
|
3616
|
+
const av = verifyBlock(a.verify);
|
|
3405
3617
|
if (bv?.command) {
|
|
3406
3618
|
if (!av?.command) {
|
|
3407
3619
|
reasons.push("verify block removed \u2014 pristine-suite re-execution disabled");
|
|
@@ -4159,7 +4371,7 @@ function scriptWeakening(before, after, opts = {}) {
|
|
|
4159
4371
|
const evidence = (trace?.line ?? before.find((l) => checkIdentity(stripComment(l).trim().replace(/^!\s+/, "").replace(PREFIX, "")) === id) ?? id).trim();
|
|
4160
4372
|
out2.push({ reason: `the check invocation \`${id}\` was ${how}`, evidence });
|
|
4161
4373
|
}
|
|
4162
|
-
const floor = beforeInv.
|
|
4374
|
+
const floor = beforeInv.flatMap((i) => i.state === "live" && i.pin && PLAIN_SEMVER.test(i.pin) ? [i.pin] : []).sort((a, b) => compareVersions(a, b) ?? 0)[0];
|
|
4163
4375
|
if (floor) {
|
|
4164
4376
|
for (const inv of afterInv) {
|
|
4165
4377
|
if (inv.state !== "live" || !inv.pin) continue;
|
|
@@ -4555,7 +4767,10 @@ function destinations(cmd, args, isDir) {
|
|
|
4555
4767
|
positional.push(a);
|
|
4556
4768
|
}
|
|
4557
4769
|
const base = (p) => p.replace(/\/+$/, "").split("/").pop() ?? p;
|
|
4558
|
-
if (target !== null)
|
|
4770
|
+
if (target !== null) {
|
|
4771
|
+
const dir = target.replace(/\/+$/, "");
|
|
4772
|
+
return positional.map((p) => `${dir}/${base(p)}`);
|
|
4773
|
+
}
|
|
4559
4774
|
if (positional.length < 2) return [];
|
|
4560
4775
|
const dest = positional[positional.length - 1];
|
|
4561
4776
|
const srcs = positional.slice(0, -1);
|
|
@@ -4569,16 +4784,18 @@ function expandShellPath(t, env = process.env) {
|
|
|
4569
4784
|
}
|
|
4570
4785
|
function hookTests(policy, ctx) {
|
|
4571
4786
|
const cwd = ctx?.cwd;
|
|
4572
|
-
const
|
|
4573
|
-
const
|
|
4787
|
+
const diskCwd = cwd !== void 0 && existsSync2(cwd) ? cwd : void 0;
|
|
4788
|
+
const onDisk = diskCwd !== void 0;
|
|
4789
|
+
const root = diskCwd !== void 0 ? canonicalPath(diskCwd) : cwd;
|
|
4574
4790
|
const memo = /* @__PURE__ */ new Map();
|
|
4575
4791
|
const rel = (t) => {
|
|
4576
4792
|
const hit = memo.get(t);
|
|
4577
4793
|
if (hit !== void 0) return hit;
|
|
4578
4794
|
let p = expandShellPath(t.replace(/^["']|["']$/g, ""));
|
|
4579
4795
|
const pathy = isAbsolute4(p) || p.includes("/") && !/^[-:]/.test(p);
|
|
4580
|
-
|
|
4581
|
-
|
|
4796
|
+
const abs2 = isAbsolute4(p) ? p : diskCwd !== void 0 ? resolve2(diskCwd, p) : null;
|
|
4797
|
+
if (pathy && abs2 !== null) {
|
|
4798
|
+
p = canonicalPath(abs2);
|
|
4582
4799
|
if (root !== void 0) {
|
|
4583
4800
|
if (p === root) p = ".";
|
|
4584
4801
|
else if (p.startsWith(root + "/")) p = p.slice(root.length + 1);
|
|
@@ -4743,7 +4960,7 @@ function xargsWritesHook(toks, fed, policy, ctx) {
|
|
|
4743
4960
|
}
|
|
4744
4961
|
|
|
4745
4962
|
// src/detectors/hook-tampering.ts
|
|
4746
|
-
var
|
|
4963
|
+
var RULE7 = "hook-tampering";
|
|
4747
4964
|
var ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
4748
4965
|
var EVENTS = ["PreToolUse", "Stop"];
|
|
4749
4966
|
var isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -4959,7 +5176,7 @@ function coverage(cs) {
|
|
|
4959
5176
|
const live = cs.filter((c) => c.runs);
|
|
4960
5177
|
if (live.length === 0) return "none";
|
|
4961
5178
|
if (live.some((c) => c.tools === null)) return null;
|
|
4962
|
-
return new Set(live.flatMap((c) => [...c.tools]));
|
|
5179
|
+
return new Set(live.flatMap((c) => c.tools ? [...c.tools] : []));
|
|
4963
5180
|
}
|
|
4964
5181
|
function duplicateJsonKeys(src) {
|
|
4965
5182
|
const dups = [];
|
|
@@ -5072,7 +5289,7 @@ function settingsWeakening(before, after, local) {
|
|
|
5072
5289
|
const schema = schemaProblem(e.group, e.h);
|
|
5073
5290
|
if (schema) {
|
|
5074
5291
|
reasons.push(`a ${e.event} hook entry carries a value the runtime's schema rejects (${schema}) \u2014 the runtime continues without the broken file, the gate included`);
|
|
5075
|
-
} else if (e.event === "PreToolUse" || e.event === "Stop") {
|
|
5292
|
+
} else if ((e.event === "PreToolUse" || e.event === "Stop") && isObj(e.h)) {
|
|
5076
5293
|
reasons.push(`a ${e.event} hook entry beside the gate ${siblingProblem(e.h)}`);
|
|
5077
5294
|
}
|
|
5078
5295
|
}
|
|
@@ -5090,7 +5307,7 @@ function claudeSettingsFindings(c, policy) {
|
|
|
5090
5307
|
if (c.after == null) return null;
|
|
5091
5308
|
const after = parseSettings(c.after);
|
|
5092
5309
|
const before = c.before != null ? parseSettings(c.before) : null;
|
|
5093
|
-
const finding = (reason) => makeFinding(
|
|
5310
|
+
const finding = (reason) => makeFinding(RULE7, policy, {
|
|
5094
5311
|
file: c.path,
|
|
5095
5312
|
message: `The Claude Code hook wiring was weakened: ${reason}.`,
|
|
5096
5313
|
evidence: reason,
|
|
@@ -5111,10 +5328,10 @@ function claudeSettingsFindings(c, policy) {
|
|
|
5111
5328
|
function policyFindings(c, policy) {
|
|
5112
5329
|
if (c.after == null) return null;
|
|
5113
5330
|
const added = c.before == null;
|
|
5114
|
-
const reasons =
|
|
5331
|
+
const reasons = c.before == null ? policyAddWeakening(c.after) : policyWeakening(c.before, c.after);
|
|
5115
5332
|
if (reasons === null) return null;
|
|
5116
5333
|
return reasons.map(
|
|
5117
|
-
(reason) => makeFinding(
|
|
5334
|
+
(reason) => makeFinding(RULE7, policy, {
|
|
5118
5335
|
file: c.path,
|
|
5119
5336
|
message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
|
|
5120
5337
|
evidence: reason,
|
|
@@ -5177,7 +5394,7 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
5177
5394
|
reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
|
|
5178
5395
|
}
|
|
5179
5396
|
return reasons.map(
|
|
5180
|
-
(reason) => makeFinding(
|
|
5397
|
+
(reason) => makeFinding(RULE7, policy, {
|
|
5181
5398
|
file: c.path,
|
|
5182
5399
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
5183
5400
|
evidence: reason,
|
|
@@ -5260,7 +5477,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
5260
5477
|
const root = path.slice(0, path.length - m[0].length + (m[0].startsWith("/") ? 1 : 0));
|
|
5261
5478
|
const entry = inspectRel(join6(ctx.cwd, root), ".husky/pre-commit");
|
|
5262
5479
|
if (entry.kind !== "file") return false;
|
|
5263
|
-
const gate =
|
|
5480
|
+
const gate = textOf2(entry);
|
|
5264
5481
|
if (gate == null) return false;
|
|
5265
5482
|
return invocations(gate.split("\n"), { errexit: true }).some(
|
|
5266
5483
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
@@ -5269,7 +5486,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
5269
5486
|
var isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
5270
5487
|
var isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
5271
5488
|
var hookTampering = {
|
|
5272
|
-
id:
|
|
5489
|
+
id: RULE7,
|
|
5273
5490
|
surface: ["file", "command"],
|
|
5274
5491
|
certainty: "mechanical",
|
|
5275
5492
|
run(changes, policy, _view, ctx) {
|
|
@@ -5282,7 +5499,7 @@ var hookTampering = {
|
|
|
5282
5499
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
5283
5500
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
5284
5501
|
out2.push(
|
|
5285
|
-
makeFinding(
|
|
5502
|
+
makeFinding(RULE7, policy, {
|
|
5286
5503
|
file: c.path,
|
|
5287
5504
|
message: `The hook installation was weakened: ${reason}.`,
|
|
5288
5505
|
evidence: reason,
|
|
@@ -5295,7 +5512,7 @@ var hookTampering = {
|
|
|
5295
5512
|
}
|
|
5296
5513
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
5297
5514
|
out2.push(
|
|
5298
|
-
makeFinding(
|
|
5515
|
+
makeFinding(RULE7, policy, {
|
|
5299
5516
|
file: c.path,
|
|
5300
5517
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
5301
5518
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -5305,7 +5522,7 @@ var hookTampering = {
|
|
|
5305
5522
|
}
|
|
5306
5523
|
if (c.op === "delete") {
|
|
5307
5524
|
out2.push(
|
|
5308
|
-
makeFinding(
|
|
5525
|
+
makeFinding(RULE7, policy, {
|
|
5309
5526
|
file: c.path,
|
|
5310
5527
|
message: "A protected hook/policy file was deleted.",
|
|
5311
5528
|
evidence: c.path,
|
|
@@ -5314,7 +5531,7 @@ var hookTampering = {
|
|
|
5314
5531
|
);
|
|
5315
5532
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
5316
5533
|
out2.push(
|
|
5317
|
-
makeFinding(
|
|
5534
|
+
makeFinding(RULE7, policy, {
|
|
5318
5535
|
file: c.path,
|
|
5319
5536
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
5320
5537
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -5328,7 +5545,7 @@ var hookTampering = {
|
|
|
5328
5545
|
if (c.before != null && c.after != null) {
|
|
5329
5546
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
5330
5547
|
out2.push(
|
|
5331
|
-
makeFinding(
|
|
5548
|
+
makeFinding(RULE7, policy, {
|
|
5332
5549
|
file: c.path,
|
|
5333
5550
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
5334
5551
|
evidence: reason,
|
|
@@ -5344,12 +5561,12 @@ var hookTampering = {
|
|
|
5344
5561
|
const husky = isHuskyScript(c.path);
|
|
5345
5562
|
const opts = husky ? { errexit: true } : {};
|
|
5346
5563
|
const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
|
|
5347
|
-
if (beforePin !== null) {
|
|
5564
|
+
if (beforePin !== null && c.after != null) {
|
|
5348
5565
|
const afterPin = initScriptPin(c.after);
|
|
5349
5566
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
5350
5567
|
if (!kept) {
|
|
5351
5568
|
out2.push(
|
|
5352
|
-
makeFinding(
|
|
5569
|
+
makeFinding(RULE7, policy, {
|
|
5353
5570
|
file: c.path,
|
|
5354
5571
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
5355
5572
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -5372,7 +5589,7 @@ var hookTampering = {
|
|
|
5372
5589
|
if (shebang || !live) {
|
|
5373
5590
|
const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
|
|
5374
5591
|
out2.push(
|
|
5375
|
-
makeFinding(
|
|
5592
|
+
makeFinding(RULE7, policy, {
|
|
5376
5593
|
file: c.path,
|
|
5377
5594
|
message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
|
|
5378
5595
|
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
|
|
@@ -5381,7 +5598,7 @@ var hookTampering = {
|
|
|
5381
5598
|
);
|
|
5382
5599
|
} else if (earlyExit) {
|
|
5383
5600
|
out2.push(
|
|
5384
|
-
makeFinding(
|
|
5601
|
+
makeFinding(RULE7, policy, {
|
|
5385
5602
|
file: c.path,
|
|
5386
5603
|
line: earlyExit.newLine ?? void 0,
|
|
5387
5604
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -5403,7 +5620,7 @@ var hookTampering = {
|
|
|
5403
5620
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
5404
5621
|
].filter((d) => d !== null);
|
|
5405
5622
|
out2.push(
|
|
5406
|
-
makeFinding(
|
|
5623
|
+
makeFinding(RULE7, policy, {
|
|
5407
5624
|
file: c.path,
|
|
5408
5625
|
message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
|
|
5409
5626
|
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
|
|
@@ -5415,7 +5632,7 @@ var hookTampering = {
|
|
|
5415
5632
|
}
|
|
5416
5633
|
if (shebangChanged && c.after != null) {
|
|
5417
5634
|
out2.push(
|
|
5418
|
-
makeFinding(
|
|
5635
|
+
makeFinding(RULE7, policy, {
|
|
5419
5636
|
file: c.path,
|
|
5420
5637
|
line: 1,
|
|
5421
5638
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -5426,7 +5643,7 @@ var hookTampering = {
|
|
|
5426
5643
|
}
|
|
5427
5644
|
if (earlyExit) {
|
|
5428
5645
|
out2.push(
|
|
5429
|
-
makeFinding(
|
|
5646
|
+
makeFinding(RULE7, policy, {
|
|
5430
5647
|
file: c.path,
|
|
5431
5648
|
line: earlyExit.newLine ?? void 0,
|
|
5432
5649
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -5438,7 +5655,7 @@ var hookTampering = {
|
|
|
5438
5655
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
5439
5656
|
if (deadGuard) {
|
|
5440
5657
|
out2.push(
|
|
5441
|
-
makeFinding(
|
|
5658
|
+
makeFinding(RULE7, policy, {
|
|
5442
5659
|
file: c.path,
|
|
5443
5660
|
line: deadGuard.newLine ?? void 0,
|
|
5444
5661
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -5450,7 +5667,7 @@ var hookTampering = {
|
|
|
5450
5667
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
5451
5668
|
if (lost.length) {
|
|
5452
5669
|
out2.push(
|
|
5453
|
-
makeFinding(
|
|
5670
|
+
makeFinding(RULE7, policy, {
|
|
5454
5671
|
file: c.path,
|
|
5455
5672
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
5456
5673
|
evidence: lost[0].evidence,
|
|
@@ -5469,7 +5686,7 @@ var hookTampering = {
|
|
|
5469
5686
|
const m = l.content.match(INLINE);
|
|
5470
5687
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
5471
5688
|
out2.push(
|
|
5472
|
-
makeFinding(
|
|
5689
|
+
makeFinding(RULE7, policy, {
|
|
5473
5690
|
file: c.path,
|
|
5474
5691
|
line: l.newLine ?? void 0,
|
|
5475
5692
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -5479,7 +5696,7 @@ var hookTampering = {
|
|
|
5479
5696
|
);
|
|
5480
5697
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
5481
5698
|
out2.push(
|
|
5482
|
-
makeFinding(
|
|
5699
|
+
makeFinding(RULE7, policy, {
|
|
5483
5700
|
file: c.path,
|
|
5484
5701
|
line: l.newLine ?? void 0,
|
|
5485
5702
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -5497,7 +5714,7 @@ var hookTampering = {
|
|
|
5497
5714
|
const hiding = updateIndexHiding(toks, policy);
|
|
5498
5715
|
if (hiding) {
|
|
5499
5716
|
out2.push(
|
|
5500
|
-
makeFinding(
|
|
5717
|
+
makeFinding(RULE7, policy, {
|
|
5501
5718
|
file: repoRelative(hiding.file, ctx),
|
|
5502
5719
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
5503
5720
|
evidence: seg,
|
|
@@ -5513,7 +5730,7 @@ var hookTampering = {
|
|
|
5513
5730
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
5514
5731
|
const outside = file !== void 0 && (isAbsolute5(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
|
|
5515
5732
|
out2.push(
|
|
5516
|
-
makeFinding(
|
|
5733
|
+
makeFinding(RULE7, policy, {
|
|
5517
5734
|
...file !== void 0 ? { file } : {},
|
|
5518
5735
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
5519
5736
|
evidence: seg,
|
|
@@ -5530,11 +5747,11 @@ var hookTampering = {
|
|
|
5530
5747
|
};
|
|
5531
5748
|
|
|
5532
5749
|
// src/detectors/test-deletion.ts
|
|
5533
|
-
import
|
|
5750
|
+
import ts6 from "typescript";
|
|
5534
5751
|
|
|
5535
5752
|
// src/detectors/suite-config.ts
|
|
5536
5753
|
import picomatch3 from "picomatch";
|
|
5537
|
-
import
|
|
5754
|
+
import ts5 from "typescript";
|
|
5538
5755
|
var JEST_DEFAULT_MATCH = ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"];
|
|
5539
5756
|
var JEST_DEFAULT_IGNORE = ["/node_modules/"];
|
|
5540
5757
|
var VITEST_DEFAULT_INCLUDE = ["**/*.{test,spec}.?(c|m)[jt]s?(x)"];
|
|
@@ -5565,18 +5782,18 @@ var PYTEST_CANONICAL_SAMPLES = [
|
|
|
5565
5782
|
"tests/conftest.py"
|
|
5566
5783
|
];
|
|
5567
5784
|
function keyName2(name) {
|
|
5568
|
-
if (
|
|
5785
|
+
if (ts5.isIdentifier(name) || ts5.isStringLiteral(name)) return name.text;
|
|
5569
5786
|
return null;
|
|
5570
5787
|
}
|
|
5571
|
-
var isStr = (e) =>
|
|
5788
|
+
var isStr = (e) => ts5.isStringLiteral(e) || ts5.isNoSubstitutionTemplateLiteral(e);
|
|
5572
5789
|
function literals2(e) {
|
|
5573
5790
|
if (isStr(e)) return { items: [e.text], opaque: false };
|
|
5574
|
-
if (!
|
|
5791
|
+
if (!ts5.isArrayLiteralExpression(e)) return { items: [], opaque: true };
|
|
5575
5792
|
const items = [];
|
|
5576
5793
|
let opaque = false;
|
|
5577
5794
|
for (const el of e.elements) {
|
|
5578
5795
|
if (isStr(el)) items.push(el.text);
|
|
5579
|
-
else if (
|
|
5796
|
+
else if (ts5.isSpreadElement(el) && /^(?:configDefaults\.|default)(?:exclude|include)$/i.test(el.expression.getText())) {
|
|
5580
5797
|
items.push(.../exclude$/i.test(el.expression.getText()) ? VITEST_DEFAULT_EXCLUDE : VITEST_DEFAULT_INCLUDE);
|
|
5581
5798
|
} else opaque = true;
|
|
5582
5799
|
}
|
|
@@ -5584,10 +5801,10 @@ function literals2(e) {
|
|
|
5584
5801
|
}
|
|
5585
5802
|
function ownerKey2(node) {
|
|
5586
5803
|
const obj = node.parent;
|
|
5587
|
-
if (!obj || !
|
|
5804
|
+
if (!obj || !ts5.isObjectLiteralExpression(obj)) return null;
|
|
5588
5805
|
let q = obj.parent;
|
|
5589
|
-
while (q && (
|
|
5590
|
-
return q &&
|
|
5806
|
+
while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
5807
|
+
return q && ts5.isPropertyAssignment(q) ? keyName2(q.name) : null;
|
|
5591
5808
|
}
|
|
5592
5809
|
function asExpression2(src) {
|
|
5593
5810
|
const t = src.trim();
|
|
@@ -5622,13 +5839,12 @@ function effectivePytestFile(files) {
|
|
|
5622
5839
|
function effectivePytestConfig(path, ctx) {
|
|
5623
5840
|
if (path.includes("/")) return false;
|
|
5624
5841
|
const base = path;
|
|
5625
|
-
const
|
|
5626
|
-
const rank = ORDER.indexOf(base);
|
|
5842
|
+
const rank = PYTEST_INI_ORDER.findIndex((name) => name === base);
|
|
5627
5843
|
if (rank < 0) return false;
|
|
5628
5844
|
const files = ctx?.trackedFiles;
|
|
5629
5845
|
if (!files) return true;
|
|
5630
5846
|
const rootNames = new Set(files.filter((f) => !f.includes("/")));
|
|
5631
|
-
return !
|
|
5847
|
+
return !PYTEST_INI_ORDER.slice(0, rank).some((n) => rootNames.has(n));
|
|
5632
5848
|
}
|
|
5633
5849
|
function pytestBlock(src) {
|
|
5634
5850
|
const lines = src.split(/\r?\n/);
|
|
@@ -5728,15 +5944,15 @@ function collect(root, runner) {
|
|
|
5728
5944
|
projectList = e;
|
|
5729
5945
|
};
|
|
5730
5946
|
const visit = (node) => {
|
|
5731
|
-
if (
|
|
5947
|
+
if (ts5.isSpreadAssignment(node) && spreadsIntoConfig(node, runner)) {
|
|
5732
5948
|
sel.present = true;
|
|
5733
5949
|
sel.opaque = true;
|
|
5734
5950
|
}
|
|
5735
|
-
if (
|
|
5951
|
+
if (ts5.isCallExpression(node) && /^(?:\w+\.)?mergeConfig$/.test(node.expression.getText()) && node.arguments[0] && !ts5.isObjectLiteralExpression(unwrapConfigCall(node.arguments[0]))) {
|
|
5736
5952
|
sel.present = true;
|
|
5737
5953
|
sel.opaque = true;
|
|
5738
5954
|
}
|
|
5739
|
-
if (
|
|
5955
|
+
if (ts5.isPropertyAssignment(node)) {
|
|
5740
5956
|
const k = keyName2(node.name);
|
|
5741
5957
|
if (runner === "jest") {
|
|
5742
5958
|
if (k === "testMatch") sel.include = take(k, node.initializer);
|
|
@@ -5766,7 +5982,7 @@ function collect(root, runner) {
|
|
|
5766
5982
|
}
|
|
5767
5983
|
}
|
|
5768
5984
|
}
|
|
5769
|
-
|
|
5985
|
+
ts5.forEachChild(node, visit);
|
|
5770
5986
|
};
|
|
5771
5987
|
visit(root);
|
|
5772
5988
|
if (projectList) sel.projects = readProjects(projectList, sel, runner);
|
|
@@ -5775,30 +5991,30 @@ function collect(root, runner) {
|
|
|
5775
5991
|
}
|
|
5776
5992
|
function unwrapConfigCall(e) {
|
|
5777
5993
|
let x = e;
|
|
5778
|
-
while (
|
|
5994
|
+
while (ts5.isParenthesizedExpression(x) || ts5.isAsExpression(x) || ts5.isSatisfiesExpression(x)) {
|
|
5779
5995
|
x = x.expression;
|
|
5780
5996
|
}
|
|
5781
|
-
if (
|
|
5997
|
+
if (ts5.isCallExpression(x) && /^(?:\w+\.)?define(?:Config|Project)$/.test(x.expression.getText()) && x.arguments[0]) return unwrapConfigCall(x.arguments[0]);
|
|
5782
5998
|
return x;
|
|
5783
5999
|
}
|
|
5784
6000
|
var JEST_SELECTION_KEYS = /^(?:testMatch|testRegex|testPathIgnorePatterns|modulePathIgnorePatterns|roots|rootDir|projects)$/;
|
|
5785
6001
|
function spreadsIntoConfig(node, runner) {
|
|
5786
6002
|
const obj = node.parent;
|
|
5787
|
-
if (!
|
|
5788
|
-
const keys = obj.properties.map((p) =>
|
|
6003
|
+
if (!ts5.isObjectLiteralExpression(obj)) return false;
|
|
6004
|
+
const keys = obj.properties.map((p) => ts5.isPropertyAssignment(p) ? keyName2(p.name) : null);
|
|
5789
6005
|
if (runner === "vitest") {
|
|
5790
6006
|
if (keys.includes("test")) return true;
|
|
5791
6007
|
let q = obj.parent;
|
|
5792
|
-
while (q && (
|
|
5793
|
-
return !!q &&
|
|
6008
|
+
while (q && (ts5.isParenthesizedExpression(q) || ts5.isAsExpression(q) || q.kind === ts5.SyntaxKind.SatisfiesExpression)) q = q.parent;
|
|
6009
|
+
return !!q && ts5.isPropertyAssignment(q) && keyName2(q.name) === "test";
|
|
5794
6010
|
}
|
|
5795
6011
|
return keys.some((k) => k !== null && JEST_SELECTION_KEYS.test(k));
|
|
5796
6012
|
}
|
|
5797
6013
|
function extendsOf(el) {
|
|
5798
6014
|
for (const p of el.properties) {
|
|
5799
|
-
if (!
|
|
5800
|
-
if (p.initializer.kind ===
|
|
5801
|
-
if (p.initializer.kind ===
|
|
6015
|
+
if (!ts5.isPropertyAssignment(p) || keyName2(p.name) !== "extends") continue;
|
|
6016
|
+
if (p.initializer.kind === ts5.SyntaxKind.TrueKeyword) return true;
|
|
6017
|
+
if (p.initializer.kind === ts5.SyntaxKind.FalseKeyword) return null;
|
|
5802
6018
|
return "path";
|
|
5803
6019
|
}
|
|
5804
6020
|
return null;
|
|
@@ -5819,14 +6035,14 @@ function readProject(el, root, runner) {
|
|
|
5819
6035
|
return p;
|
|
5820
6036
|
}
|
|
5821
6037
|
function readProjects(e, root, runner) {
|
|
5822
|
-
if (!
|
|
6038
|
+
if (!ts5.isArrayLiteralExpression(e)) {
|
|
5823
6039
|
root.opaque = true;
|
|
5824
6040
|
return [];
|
|
5825
6041
|
}
|
|
5826
6042
|
const list2 = [];
|
|
5827
6043
|
for (const el of e.elements) {
|
|
5828
6044
|
const lit = unwrapConfigCall(el);
|
|
5829
|
-
if (
|
|
6045
|
+
if (ts5.isObjectLiteralExpression(lit)) list2.push(readProject(lit, root, runner));
|
|
5830
6046
|
else root.opaque = true;
|
|
5831
6047
|
}
|
|
5832
6048
|
return list2;
|
|
@@ -5834,14 +6050,14 @@ function readProjects(e, root, runner) {
|
|
|
5834
6050
|
function workspaceArray(sf) {
|
|
5835
6051
|
for (const st of sf.statements) {
|
|
5836
6052
|
let e;
|
|
5837
|
-
if (
|
|
5838
|
-
else if (
|
|
6053
|
+
if (ts5.isExportAssignment(st)) e = st.expression;
|
|
6054
|
+
else if (ts5.isExpressionStatement(st) && ts5.isBinaryExpression(st.expression) && /module\.exports/.test(st.expression.left.getText())) e = st.expression.right;
|
|
5839
6055
|
if (!e) continue;
|
|
5840
|
-
while (
|
|
6056
|
+
while (ts5.isParenthesizedExpression(e) || ts5.isAsExpression(e) || ts5.isSatisfiesExpression(e)) {
|
|
5841
6057
|
e = e.expression;
|
|
5842
6058
|
}
|
|
5843
|
-
if (
|
|
5844
|
-
return
|
|
6059
|
+
if (ts5.isCallExpression(e) && e.arguments.length > 0) e = e.arguments[0];
|
|
6060
|
+
return ts5.isArrayLiteralExpression(e) ? e : null;
|
|
5845
6061
|
}
|
|
5846
6062
|
return null;
|
|
5847
6063
|
}
|
|
@@ -5849,7 +6065,7 @@ function parseSelection(src, runner, path = "") {
|
|
|
5849
6065
|
let sel = empty();
|
|
5850
6066
|
if (runner === "pytest") return pytestSelection(src);
|
|
5851
6067
|
try {
|
|
5852
|
-
const sf =
|
|
6068
|
+
const sf = ts5.createSourceFile("cfg.ts", asExpression2(src), ts5.ScriptTarget.Latest, true, ts5.ScriptKind.TS);
|
|
5853
6069
|
if (runner === "vitest" && isWorkspaceFile(path)) {
|
|
5854
6070
|
const arr = workspaceArray(sf);
|
|
5855
6071
|
if (arr) {
|
|
@@ -5943,7 +6159,7 @@ function suiteNarrowings(before, after, path, samples) {
|
|
|
5943
6159
|
}
|
|
5944
6160
|
|
|
5945
6161
|
// src/detectors/test-deletion.ts
|
|
5946
|
-
var
|
|
6162
|
+
var RULE8 = "test-deletion";
|
|
5947
6163
|
var JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
5948
6164
|
var TEST_DEFS = {
|
|
5949
6165
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -5956,21 +6172,21 @@ var TEST_DEFS = {
|
|
|
5956
6172
|
cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
|
|
5957
6173
|
};
|
|
5958
6174
|
function calleeName(expr) {
|
|
5959
|
-
if (
|
|
5960
|
-
if (
|
|
6175
|
+
if (ts6.isIdentifier(expr)) return expr.text;
|
|
6176
|
+
if (ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression)) {
|
|
5961
6177
|
return expr.expression.text;
|
|
5962
6178
|
}
|
|
5963
6179
|
return null;
|
|
5964
6180
|
}
|
|
5965
6181
|
var TABLE_METHOD = /^(?:each|for)$/;
|
|
5966
|
-
var isEachOf = (expr) =>
|
|
5967
|
-
var isDescribeEach = (expr) =>
|
|
6182
|
+
var isEachOf = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
|
|
6183
|
+
var isDescribeEach = (expr) => ts6.isPropertyAccessExpression(expr) && ts6.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
|
|
5968
6184
|
function loopRows(iterable) {
|
|
5969
|
-
if (
|
|
6185
|
+
if (ts6.isArrayLiteralExpression(iterable)) {
|
|
5970
6186
|
let n = 0;
|
|
5971
6187
|
let open = false;
|
|
5972
6188
|
for (const el of iterable.elements) {
|
|
5973
|
-
if (
|
|
6189
|
+
if (ts6.isSpreadElement(el)) open = true;
|
|
5974
6190
|
else n++;
|
|
5975
6191
|
}
|
|
5976
6192
|
return { n: Math.max(n, open ? 0 : 1), open };
|
|
@@ -5982,20 +6198,20 @@ function templateRows(node) {
|
|
|
5982
6198
|
return Math.max(rows, 1);
|
|
5983
6199
|
}
|
|
5984
6200
|
function hasSubstantiveBody(call) {
|
|
5985
|
-
const fn = call.arguments.find((a) =>
|
|
6201
|
+
const fn = call.arguments.find((a) => ts6.isArrowFunction(a) || ts6.isFunctionExpression(a));
|
|
5986
6202
|
if (!fn) return true;
|
|
5987
6203
|
const body = fn.body;
|
|
5988
|
-
const text =
|
|
6204
|
+
const text = ts6.isBlock(body) ? body.getText().slice(1, -1) : body.getText();
|
|
5989
6205
|
return text.split("\n").some((l) => isSignificantLine(l.trim(), "js"));
|
|
5990
6206
|
}
|
|
5991
6207
|
function eachRows(call) {
|
|
5992
6208
|
const arg = call.arguments[0];
|
|
5993
6209
|
if (!arg) return { n: 1, open: true };
|
|
5994
|
-
if (
|
|
6210
|
+
if (ts6.isArrayLiteralExpression(arg)) {
|
|
5995
6211
|
let n = 0;
|
|
5996
6212
|
let open = false;
|
|
5997
6213
|
for (const el of arg.elements) {
|
|
5998
|
-
if (
|
|
6214
|
+
if (ts6.isSpreadElement(el)) open = true;
|
|
5999
6215
|
else n++;
|
|
6000
6216
|
}
|
|
6001
6217
|
return { n: Math.max(n, open ? 0 : 1), open };
|
|
@@ -6010,35 +6226,35 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
6010
6226
|
for (const line of src.split("\n")) if (re.test(line)) n2++;
|
|
6011
6227
|
return { min: n2, open: false };
|
|
6012
6228
|
}
|
|
6013
|
-
const sf =
|
|
6229
|
+
const sf = ts6.createSourceFile("spec.ts", src, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
|
|
6014
6230
|
let n = 0;
|
|
6015
6231
|
let open = false;
|
|
6016
6232
|
const visit = (node, mult) => {
|
|
6017
|
-
if (
|
|
6233
|
+
if (ts6.isForOfStatement(node)) {
|
|
6018
6234
|
const r = loopRows(node.expression);
|
|
6019
6235
|
if (r.open) open = true;
|
|
6020
6236
|
visit(node.statement, mult * r.n);
|
|
6021
6237
|
return;
|
|
6022
6238
|
}
|
|
6023
|
-
if (
|
|
6239
|
+
if (ts6.isForStatement(node) || ts6.isForInStatement(node) || ts6.isWhileStatement(node) || ts6.isDoStatement(node)) {
|
|
6024
6240
|
open = true;
|
|
6025
6241
|
}
|
|
6026
|
-
if (
|
|
6242
|
+
if (ts6.isCallExpression(node)) {
|
|
6027
6243
|
const callee = node.expression;
|
|
6028
|
-
if (
|
|
6244
|
+
if (ts6.isPropertyAccessExpression(callee) && callee.name.text === "forEach") {
|
|
6029
6245
|
const r = loopRows(callee.expression);
|
|
6030
6246
|
if (r.open) open = true;
|
|
6031
6247
|
for (const a of node.arguments) visit(a, mult * r.n);
|
|
6032
6248
|
return;
|
|
6033
6249
|
}
|
|
6034
6250
|
const head = node.expression;
|
|
6035
|
-
if (
|
|
6251
|
+
if (ts6.isCallExpression(head) && isDescribeEach(head.expression)) {
|
|
6036
6252
|
const r = eachRows(head);
|
|
6037
6253
|
if (r.open) open = true;
|
|
6038
6254
|
for (const a of node.arguments) visit(a, mult * r.n);
|
|
6039
6255
|
return;
|
|
6040
6256
|
}
|
|
6041
|
-
if (
|
|
6257
|
+
if (ts6.isTaggedTemplateExpression(head) && isDescribeEach(head.tag)) {
|
|
6042
6258
|
for (const a of node.arguments) visit(a, mult * templateRows(head));
|
|
6043
6259
|
return;
|
|
6044
6260
|
}
|
|
@@ -6050,10 +6266,10 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
|
|
|
6050
6266
|
const name = calleeName(node.expression);
|
|
6051
6267
|
if ((name === "it" || name === "test") && (!substantiveOnly || hasSubstantiveBody(node))) n += mult;
|
|
6052
6268
|
}
|
|
6053
|
-
} else if (
|
|
6269
|
+
} else if (ts6.isTaggedTemplateExpression(node) && isEachOf(node.tag)) {
|
|
6054
6270
|
n += templateRows(node) * mult;
|
|
6055
6271
|
}
|
|
6056
|
-
|
|
6272
|
+
ts6.forEachChild(node, (c) => visit(c, mult));
|
|
6057
6273
|
};
|
|
6058
6274
|
visit(sf, 1);
|
|
6059
6275
|
return { min: n, open };
|
|
@@ -6090,7 +6306,8 @@ function scriptNarrowings(before, after, path) {
|
|
|
6090
6306
|
if ((path.split("/").pop() ?? path) !== "package.json") return [];
|
|
6091
6307
|
const scriptsOf = (src) => {
|
|
6092
6308
|
try {
|
|
6093
|
-
const
|
|
6309
|
+
const pkg = JSON.parse(src ?? "{}");
|
|
6310
|
+
const s = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts : {};
|
|
6094
6311
|
return Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
|
|
6095
6312
|
} catch {
|
|
6096
6313
|
return {};
|
|
@@ -6135,7 +6352,7 @@ function leavesGlob(dir, dest, policy, ctx) {
|
|
|
6135
6352
|
}
|
|
6136
6353
|
var SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
6137
6354
|
var testDeletion = {
|
|
6138
|
-
id:
|
|
6355
|
+
id: RULE8,
|
|
6139
6356
|
surface: ["file", "command"],
|
|
6140
6357
|
certainty: "mechanical",
|
|
6141
6358
|
run(changes, policy, _view, ctx) {
|
|
@@ -6166,7 +6383,7 @@ var testDeletion = {
|
|
|
6166
6383
|
if (c.op === "delete" && isTest) {
|
|
6167
6384
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
6168
6385
|
out2.push(
|
|
6169
|
-
makeFinding(
|
|
6386
|
+
makeFinding(RULE8, policy, {
|
|
6170
6387
|
file: c.path,
|
|
6171
6388
|
message: "A test file was deleted.",
|
|
6172
6389
|
evidence: c.path,
|
|
@@ -6175,7 +6392,7 @@ var testDeletion = {
|
|
|
6175
6392
|
);
|
|
6176
6393
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
|
|
6177
6394
|
out2.push(
|
|
6178
|
-
makeFinding(
|
|
6395
|
+
makeFinding(RULE8, policy, {
|
|
6179
6396
|
file: c.path,
|
|
6180
6397
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
6181
6398
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -6190,7 +6407,7 @@ var testDeletion = {
|
|
|
6190
6407
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
6191
6408
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
6192
6409
|
out2.push(
|
|
6193
|
-
makeFinding(
|
|
6410
|
+
makeFinding(RULE8, policy, {
|
|
6194
6411
|
file: c.path,
|
|
6195
6412
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
6196
6413
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -6203,7 +6420,7 @@ var testDeletion = {
|
|
|
6203
6420
|
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
6204
6421
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
6205
6422
|
out2.push(
|
|
6206
|
-
makeFinding(
|
|
6423
|
+
makeFinding(RULE8, policy, {
|
|
6207
6424
|
file: c.path,
|
|
6208
6425
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
6209
6426
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -6213,7 +6430,7 @@ var testDeletion = {
|
|
|
6213
6430
|
}
|
|
6214
6431
|
for (const n of scriptNarrowings(c.before, c.after, c.path)) {
|
|
6215
6432
|
out2.push(
|
|
6216
|
-
makeFinding(
|
|
6433
|
+
makeFinding(RULE8, policy, {
|
|
6217
6434
|
file: c.path,
|
|
6218
6435
|
message: `The test script now narrows the suite: ${n.flag} added to scripts.${n.script}.`,
|
|
6219
6436
|
evidence: `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${JSON.stringify(n.after)}`,
|
|
@@ -6301,7 +6518,7 @@ var testDeletion = {
|
|
|
6301
6518
|
}
|
|
6302
6519
|
if (why) {
|
|
6303
6520
|
out2.push(
|
|
6304
|
-
makeFinding(
|
|
6521
|
+
makeFinding(RULE8, policy, {
|
|
6305
6522
|
message: `Test removed via shell: ${why}.`,
|
|
6306
6523
|
evidence,
|
|
6307
6524
|
remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
|
|
@@ -6316,11 +6533,10 @@ var testDeletion = {
|
|
|
6316
6533
|
}
|
|
6317
6534
|
};
|
|
6318
6535
|
function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
6319
|
-
const order = PYTEST_INI_ORDER;
|
|
6320
6536
|
const touched = /* @__PURE__ */ new Map();
|
|
6321
6537
|
for (const c of changes) {
|
|
6322
6538
|
if (c.kind !== "file" || c.oldPath) continue;
|
|
6323
|
-
if (
|
|
6539
|
+
if (PYTEST_INI_ORDER.some((name) => name === c.path)) touched.set(c.path, c);
|
|
6324
6540
|
}
|
|
6325
6541
|
if (touched.size === 0) return [];
|
|
6326
6542
|
const listing = trackedFiles(ctx);
|
|
@@ -6328,7 +6544,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
6328
6544
|
const rootNames = new Set(listing.filter((f) => !f.includes("/")));
|
|
6329
6545
|
const base = /* @__PURE__ */ new Map();
|
|
6330
6546
|
const head = /* @__PURE__ */ new Map();
|
|
6331
|
-
for (const name of
|
|
6547
|
+
for (const name of PYTEST_INI_ORDER) {
|
|
6332
6548
|
const c = touched.get(name);
|
|
6333
6549
|
if (c) {
|
|
6334
6550
|
base.set(name, c.before);
|
|
@@ -6345,7 +6561,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
6345
6561
|
if (!was || !now || was.path === now.path) return [];
|
|
6346
6562
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
6347
6563
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
6348
|
-
(n) => makeFinding(
|
|
6564
|
+
(n) => makeFinding(RULE8, policy, {
|
|
6349
6565
|
file: now.path,
|
|
6350
6566
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
6351
6567
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -6355,8 +6571,8 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
6355
6571
|
}
|
|
6356
6572
|
|
|
6357
6573
|
// src/detectors/test-content-removal.ts
|
|
6358
|
-
import
|
|
6359
|
-
var
|
|
6574
|
+
import ts7 from "typescript";
|
|
6575
|
+
var RULE9 = "test-content-removal";
|
|
6360
6576
|
var MIN_REMOVED_LINES = 3;
|
|
6361
6577
|
var MIN_REMOVED_ROWS = 2;
|
|
6362
6578
|
var ws = (s) => s.replace(/\s+/g, "");
|
|
@@ -6365,13 +6581,13 @@ function eachTables(src, path) {
|
|
|
6365
6581
|
let open = false;
|
|
6366
6582
|
if (langOf(path) !== "js" && langOf(path) !== null) return { rows, open };
|
|
6367
6583
|
try {
|
|
6368
|
-
const sf =
|
|
6584
|
+
const sf = ts7.createSourceFile("spec.ts", src, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
|
|
6369
6585
|
const visit = (node) => {
|
|
6370
|
-
if (
|
|
6586
|
+
if (ts7.isCallExpression(node) && ts7.isPropertyAccessExpression(node.expression) && /^(?:each|for)$/.test(node.expression.name.text) && ts7.isIdentifier(node.expression.expression) && /^(?:it|test|describe)$/.test(node.expression.expression.text)) {
|
|
6371
6587
|
const arg = node.arguments[0];
|
|
6372
|
-
if (arg &&
|
|
6588
|
+
if (arg && ts7.isArrayLiteralExpression(arg)) {
|
|
6373
6589
|
for (const el of arg.elements) {
|
|
6374
|
-
if (
|
|
6590
|
+
if (ts7.isSpreadElement(el)) {
|
|
6375
6591
|
open = true;
|
|
6376
6592
|
continue;
|
|
6377
6593
|
}
|
|
@@ -6381,7 +6597,7 @@ function eachTables(src, path) {
|
|
|
6381
6597
|
}
|
|
6382
6598
|
} else open = true;
|
|
6383
6599
|
}
|
|
6384
|
-
|
|
6600
|
+
ts7.forEachChild(node, visit);
|
|
6385
6601
|
};
|
|
6386
6602
|
visit(sf);
|
|
6387
6603
|
} catch {
|
|
@@ -6399,7 +6615,7 @@ function significantLinesOrdered(src, path) {
|
|
|
6399
6615
|
return out2;
|
|
6400
6616
|
}
|
|
6401
6617
|
var testContentRemoval = {
|
|
6402
|
-
id:
|
|
6618
|
+
id: RULE9,
|
|
6403
6619
|
surface: ["file"],
|
|
6404
6620
|
certainty: "mechanical",
|
|
6405
6621
|
run(changes, policy) {
|
|
@@ -6440,7 +6656,7 @@ var testContentRemoval = {
|
|
|
6440
6656
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
6441
6657
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
6442
6658
|
out2.push(
|
|
6443
|
-
makeFinding(
|
|
6659
|
+
makeFinding(RULE9, policy, {
|
|
6444
6660
|
file: c.path,
|
|
6445
6661
|
message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
|
|
6446
6662
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -6451,7 +6667,7 @@ var testContentRemoval = {
|
|
|
6451
6667
|
}
|
|
6452
6668
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
6453
6669
|
out2.push(
|
|
6454
|
-
makeFinding(
|
|
6670
|
+
makeFinding(RULE9, policy, {
|
|
6455
6671
|
file: c.path,
|
|
6456
6672
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
6457
6673
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -6465,7 +6681,7 @@ var testContentRemoval = {
|
|
|
6465
6681
|
};
|
|
6466
6682
|
|
|
6467
6683
|
// src/detectors/snapshot-rewrite.ts
|
|
6468
|
-
var
|
|
6684
|
+
var RULE10 = "snapshot-rewrite";
|
|
6469
6685
|
var RUNNER2 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
6470
6686
|
var PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
6471
6687
|
var UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -6482,12 +6698,12 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
6482
6698
|
return null;
|
|
6483
6699
|
}
|
|
6484
6700
|
var snapshotRewrite = {
|
|
6485
|
-
id:
|
|
6701
|
+
id: RULE10,
|
|
6486
6702
|
surface: ["command", "file"],
|
|
6487
6703
|
certainty: "mechanical",
|
|
6488
6704
|
run(changes, policy) {
|
|
6489
6705
|
const out2 = [];
|
|
6490
|
-
const warn = (input) => out2.push(makeFinding(
|
|
6706
|
+
const warn = (input) => out2.push(makeFinding(RULE10, policy, { ...input, defaultSeverity: "warn" }));
|
|
6491
6707
|
for (const c of changes) {
|
|
6492
6708
|
if (c.kind === "command") {
|
|
6493
6709
|
for (const seg of segments(c.raw)) {
|
|
@@ -6516,7 +6732,7 @@ var snapshotRewrite = {
|
|
|
6516
6732
|
if (c.op === "add" || !isSnap && !wasSnap) continue;
|
|
6517
6733
|
const what = c.op === "delete" ? "deleted" : c.op === "rename" && !isSnap ? `renamed out of the protected snapshot set (to ${c.path})` : "rewritten";
|
|
6518
6734
|
warn({
|
|
6519
|
-
file: c.op === "rename" && !isSnap ? c.oldPath : c.path,
|
|
6735
|
+
file: c.op === "rename" && !isSnap ? c.oldPath ?? c.path : c.path,
|
|
6520
6736
|
message: `A recorded expected output was ${what}.`,
|
|
6521
6737
|
evidence: c.op === "rename" ? `${c.oldPath} -> ${c.path}` : c.path,
|
|
6522
6738
|
remediation: "If the intended output genuinely changed, a human should confirm the new expectation against the requirement; if the test was failing, fix the code instead of re-recording the expectation from it."
|
|
@@ -6527,7 +6743,7 @@ var snapshotRewrite = {
|
|
|
6527
6743
|
};
|
|
6528
6744
|
|
|
6529
6745
|
// src/detectors/snapshot-only.ts
|
|
6530
|
-
var
|
|
6746
|
+
var RULE11 = "snapshot-only-rewrite";
|
|
6531
6747
|
var COMMIT_VIEWS = ["staged", "range"];
|
|
6532
6748
|
function isSnapshotChange(c, policy) {
|
|
6533
6749
|
if (c.kind !== "file") return false;
|
|
@@ -6536,7 +6752,7 @@ function isSnapshotChange(c, policy) {
|
|
|
6536
6752
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
6537
6753
|
}
|
|
6538
6754
|
var snapshotOnlyRewrite = {
|
|
6539
|
-
id:
|
|
6755
|
+
id: RULE11,
|
|
6540
6756
|
surface: ["file"],
|
|
6541
6757
|
certainty: "mechanical",
|
|
6542
6758
|
run(changes, policy, view) {
|
|
@@ -6546,9 +6762,10 @@ var snapshotOnlyRewrite = {
|
|
|
6546
6762
|
const snaps = files.filter((c) => isSnapshotChange(c, policy));
|
|
6547
6763
|
if (snaps.length === 0 || snaps.length !== files.length) return [];
|
|
6548
6764
|
const first = snaps[0];
|
|
6549
|
-
|
|
6765
|
+
if (!first) return [];
|
|
6766
|
+
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
6550
6767
|
return [
|
|
6551
|
-
makeFinding(
|
|
6768
|
+
makeFinding(RULE11, policy, {
|
|
6552
6769
|
file: first.path,
|
|
6553
6770
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
6554
6771
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -6560,8 +6777,8 @@ var snapshotOnlyRewrite = {
|
|
|
6560
6777
|
};
|
|
6561
6778
|
|
|
6562
6779
|
// src/detectors/assertion-weakening.ts
|
|
6563
|
-
import
|
|
6564
|
-
var
|
|
6780
|
+
import ts8 from "typescript";
|
|
6781
|
+
var RULE12 = "assertion-weakening";
|
|
6565
6782
|
var TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
6566
6783
|
var SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
6567
6784
|
var EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
@@ -6569,55 +6786,55 @@ var WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
|
6569
6786
|
var compact = (s) => s.replace(/\s+/g, "");
|
|
6570
6787
|
function literalTitle(node) {
|
|
6571
6788
|
if (!node) return null;
|
|
6572
|
-
if (
|
|
6789
|
+
if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
6573
6790
|
return null;
|
|
6574
6791
|
}
|
|
6575
6792
|
function scriptKindForPath(path) {
|
|
6576
6793
|
const lower = path.toLowerCase();
|
|
6577
|
-
if (lower.endsWith(".tsx")) return
|
|
6794
|
+
if (lower.endsWith(".tsx")) return ts8.ScriptKind.TSX;
|
|
6578
6795
|
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts")) {
|
|
6579
|
-
return
|
|
6796
|
+
return ts8.ScriptKind.TS;
|
|
6580
6797
|
}
|
|
6581
|
-
if (lower.endsWith(".jsx")) return
|
|
6582
|
-
return
|
|
6798
|
+
if (lower.endsWith(".jsx")) return ts8.ScriptKind.JSX;
|
|
6799
|
+
return ts8.ScriptKind.JS;
|
|
6583
6800
|
}
|
|
6584
6801
|
function staticFacts(node) {
|
|
6585
6802
|
if (!node) return null;
|
|
6586
|
-
if (node.kind ===
|
|
6587
|
-
if (node.kind ===
|
|
6588
|
-
if (node.kind ===
|
|
6589
|
-
if (
|
|
6803
|
+
if (node.kind === ts8.SyntaxKind.TrueKeyword) return { defined: true, truthy: true };
|
|
6804
|
+
if (node.kind === ts8.SyntaxKind.FalseKeyword) return { defined: true, truthy: false };
|
|
6805
|
+
if (node.kind === ts8.SyntaxKind.NullKeyword) return { defined: true, truthy: false };
|
|
6806
|
+
if (ts8.isIdentifier(node) && node.text === "undefined") {
|
|
6590
6807
|
return { defined: false, truthy: false };
|
|
6591
6808
|
}
|
|
6592
|
-
if (
|
|
6593
|
-
if (
|
|
6809
|
+
if (ts8.isVoidExpression(node)) return { defined: false, truthy: false };
|
|
6810
|
+
if (ts8.isStringLiteral(node) || ts8.isNoSubstitutionTemplateLiteral(node)) {
|
|
6594
6811
|
return { defined: true, truthy: node.text.length > 0 };
|
|
6595
6812
|
}
|
|
6596
|
-
if (
|
|
6813
|
+
if (ts8.isNumericLiteral(node)) {
|
|
6597
6814
|
return { defined: true, truthy: Number(node.text) !== 0 };
|
|
6598
6815
|
}
|
|
6599
|
-
if (
|
|
6816
|
+
if (ts8.isBigIntLiteral(node)) {
|
|
6600
6817
|
return { defined: true, truthy: node.text.replace(/n$/i, "") !== "0" };
|
|
6601
6818
|
}
|
|
6602
|
-
if (
|
|
6603
|
-
const value = Number(node.operand.text) * (node.operator ===
|
|
6819
|
+
if (ts8.isPrefixUnaryExpression(node) && (node.operator === ts8.SyntaxKind.MinusToken || node.operator === ts8.SyntaxKind.PlusToken) && ts8.isNumericLiteral(node.operand)) {
|
|
6820
|
+
const value = Number(node.operand.text) * (node.operator === ts8.SyntaxKind.MinusToken ? -1 : 1);
|
|
6604
6821
|
return { defined: true, truthy: value !== 0 };
|
|
6605
6822
|
}
|
|
6606
|
-
if (
|
|
6823
|
+
if (ts8.isObjectLiteralExpression(node) || ts8.isArrayLiteralExpression(node) || ts8.isRegularExpressionLiteral(node) || ts8.isArrowFunction(node) || ts8.isFunctionExpression(node) || ts8.isClassExpression(node) || ts8.isNewExpression(node)) {
|
|
6607
6824
|
return { defined: true, truthy: true };
|
|
6608
6825
|
}
|
|
6609
6826
|
return null;
|
|
6610
6827
|
}
|
|
6611
6828
|
function expectAssertion(node, sf) {
|
|
6612
|
-
if (!
|
|
6829
|
+
if (!ts8.isPropertyAccessExpression(node.expression)) return null;
|
|
6613
6830
|
const matcher = node.expression.name.text;
|
|
6614
6831
|
let receiver = node.expression.expression;
|
|
6615
6832
|
let polarity = "";
|
|
6616
|
-
if (
|
|
6833
|
+
if (ts8.isPropertyAccessExpression(receiver) && receiver.name.text === "not") {
|
|
6617
6834
|
polarity = "not";
|
|
6618
6835
|
receiver = receiver.expression;
|
|
6619
6836
|
}
|
|
6620
|
-
if (!
|
|
6837
|
+
if (!ts8.isCallExpression(receiver) || !ts8.isIdentifier(receiver.expression) || receiver.expression.text !== "expect" || receiver.arguments.length !== 1) {
|
|
6621
6838
|
return null;
|
|
6622
6839
|
}
|
|
6623
6840
|
const subject = compact(receiver.arguments[0].getText(sf));
|
|
@@ -6633,41 +6850,41 @@ function expectAssertion(node, sf) {
|
|
|
6633
6850
|
};
|
|
6634
6851
|
}
|
|
6635
6852
|
function simpleCallName(node) {
|
|
6636
|
-
return
|
|
6853
|
+
return ts8.isIdentifier(node.expression) ? node.expression.text : null;
|
|
6637
6854
|
}
|
|
6638
6855
|
function callbackFor(node) {
|
|
6639
6856
|
const cb = node.arguments[1];
|
|
6640
|
-
return cb && (
|
|
6857
|
+
return cb && (ts8.isArrowFunction(cb) || ts8.isFunctionExpression(cb)) ? cb : null;
|
|
6641
6858
|
}
|
|
6642
6859
|
function blocks(src, path) {
|
|
6643
6860
|
const out2 = /* @__PURE__ */ new Map();
|
|
6644
6861
|
if (langOf(path) !== "js") return out2;
|
|
6645
6862
|
try {
|
|
6646
|
-
const sf =
|
|
6863
|
+
const sf = ts8.createSourceFile(path, src, ts8.ScriptTarget.Latest, true, scriptKindForPath(path));
|
|
6647
6864
|
const visit = (node, suites) => {
|
|
6648
|
-
if (
|
|
6865
|
+
if (ts8.isCallExpression(node)) {
|
|
6649
6866
|
const call = simpleCallName(node);
|
|
6650
6867
|
const title = literalTitle(node.arguments[0]);
|
|
6651
6868
|
const cb = callbackFor(node);
|
|
6652
6869
|
if (call && SUITE_CALLS.has(call) && title !== null && cb) {
|
|
6653
6870
|
const nextSuites = [...suites, title];
|
|
6654
|
-
|
|
6871
|
+
ts8.forEachChild(cb, (child) => visit(child, nextSuites));
|
|
6655
6872
|
return;
|
|
6656
6873
|
}
|
|
6657
6874
|
if (call && TEST_CALLS.has(call) && title !== null && cb) {
|
|
6658
6875
|
const assertions = [];
|
|
6659
6876
|
const collect2 = (child) => {
|
|
6660
|
-
if (child !== cb &&
|
|
6877
|
+
if (child !== cb && ts8.isCallExpression(child)) {
|
|
6661
6878
|
const nestedCall = simpleCallName(child);
|
|
6662
6879
|
if (nestedCall && (TEST_CALLS.has(nestedCall) || SUITE_CALLS.has(nestedCall))) {
|
|
6663
6880
|
return;
|
|
6664
6881
|
}
|
|
6665
6882
|
}
|
|
6666
|
-
if (
|
|
6883
|
+
if (ts8.isCallExpression(child)) {
|
|
6667
6884
|
const a = expectAssertion(child, sf);
|
|
6668
6885
|
if (a) assertions.push(a);
|
|
6669
6886
|
}
|
|
6670
|
-
|
|
6887
|
+
ts8.forEachChild(child, collect2);
|
|
6671
6888
|
};
|
|
6672
6889
|
collect2(cb);
|
|
6673
6890
|
const identity2 = [...suites, title].join(" > ");
|
|
@@ -6677,7 +6894,7 @@ function blocks(src, path) {
|
|
|
6677
6894
|
return;
|
|
6678
6895
|
}
|
|
6679
6896
|
}
|
|
6680
|
-
|
|
6897
|
+
ts8.forEachChild(node, (child) => visit(child, suites));
|
|
6681
6898
|
};
|
|
6682
6899
|
visit(sf, []);
|
|
6683
6900
|
} catch {
|
|
@@ -6726,7 +6943,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
6726
6943
|
const weakened = directionalWeakening(before.assertions, after.assertions);
|
|
6727
6944
|
if (weakened) {
|
|
6728
6945
|
const message = weakened.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened.before.matcher} was replaced by weaker ${weakened.after.matcher} on the same subject.`;
|
|
6729
|
-
return makeFinding(
|
|
6946
|
+
return makeFinding(RULE12, policy, {
|
|
6730
6947
|
file: path,
|
|
6731
6948
|
message,
|
|
6732
6949
|
evidence: `${weakened.before.text} -> ${weakened.after.text}`,
|
|
@@ -6735,7 +6952,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
6735
6952
|
}
|
|
6736
6953
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
6737
6954
|
if (removed) {
|
|
6738
|
-
return makeFinding(
|
|
6955
|
+
return makeFinding(RULE12, policy, {
|
|
6739
6956
|
file: path,
|
|
6740
6957
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
6741
6958
|
evidence: removed.text,
|
|
@@ -6745,7 +6962,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
6745
6962
|
return null;
|
|
6746
6963
|
}
|
|
6747
6964
|
var assertionWeakening = {
|
|
6748
|
-
id:
|
|
6965
|
+
id: RULE12,
|
|
6749
6966
|
surface: ["file"],
|
|
6750
6967
|
certainty: "heuristic",
|
|
6751
6968
|
run(changes, policy) {
|
|
@@ -6771,6 +6988,7 @@ var assertionWeakening = {
|
|
|
6771
6988
|
var allDetectors = [
|
|
6772
6989
|
noVerify,
|
|
6773
6990
|
tsAnyCast,
|
|
6991
|
+
tsCastGrowth,
|
|
6774
6992
|
lintSuppression,
|
|
6775
6993
|
testSkip,
|
|
6776
6994
|
coverageLowering,
|
|
@@ -6812,14 +7030,15 @@ function evaluate(changes, policy, detectors = allDetectors, view, ctx) {
|
|
|
6812
7030
|
process.stderr.write(`tamperward: detector "${d.id}" errored: ${String(e)}
|
|
6813
7031
|
`);
|
|
6814
7032
|
if (view === "staged" || view === "worktree" || view === "range") {
|
|
6815
|
-
|
|
7033
|
+
const failed = {
|
|
6816
7034
|
rule: "detector-error",
|
|
6817
7035
|
severity: "block",
|
|
6818
7036
|
message: `Detector "${d.id}" failed to run; the verdict is incomplete.`,
|
|
6819
7037
|
evidence: String(e).slice(0, 200),
|
|
6820
7038
|
remediation: "A rule that cannot run is not a rule that passed. Fix the input or the detector; do not read this as clean.",
|
|
6821
7039
|
signoff: { required: true, command: `tamperward allow detector-error --reason "..."` }
|
|
6822
|
-
}
|
|
7040
|
+
};
|
|
7041
|
+
out2.push(failed);
|
|
6823
7042
|
}
|
|
6824
7043
|
}
|
|
6825
7044
|
}
|
|
@@ -6860,7 +7079,7 @@ function normalizeVersion(v, where = POLICY_FILE) {
|
|
|
6860
7079
|
return v;
|
|
6861
7080
|
}
|
|
6862
7081
|
var SEVERITIES = ["block", "warn"];
|
|
6863
|
-
var isSeverity = (v) => SEVERITIES.
|
|
7082
|
+
var isSeverity = (v) => SEVERITIES.some((s) => s === v);
|
|
6864
7083
|
var isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
6865
7084
|
var isMapping = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6866
7085
|
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "protected", "rules", "ignore", "signoff", "verify"]);
|
|
@@ -6874,75 +7093,105 @@ function validate(r, where) {
|
|
|
6874
7093
|
throw new PolicyError(`${where}: ${msg}`);
|
|
6875
7094
|
}
|
|
6876
7095
|
const show = (v) => JSON.stringify(v) ?? String(v);
|
|
7096
|
+
const out2 = {};
|
|
7097
|
+
if (r.version !== void 0) out2.version = r.version;
|
|
6877
7098
|
const unknown = Object.keys(r).filter((k) => !TOP_LEVEL_KEYS.has(k));
|
|
6878
7099
|
if (unknown.length) {
|
|
6879
7100
|
bad(`unknown top-level key${unknown.length === 1 ? "" : "s"} ${unknown.map((k) => JSON.stringify(k)).join(", ")} (expected one of ${[...TOP_LEVEL_KEYS].join(", ")})`);
|
|
6880
7101
|
}
|
|
6881
7102
|
if (r.rules !== void 0) {
|
|
6882
7103
|
if (!isMapping(r.rules)) bad(`rules must be a mapping of rule name to { severity, enabled, exclude }, got ${show(r.rules)}`);
|
|
7104
|
+
const rules = {};
|
|
6883
7105
|
for (const [name, cfg] of Object.entries(r.rules)) {
|
|
6884
7106
|
if (!isMapping(cfg)) bad(`rules.${name} must be a mapping like { severity: block }, got ${show(cfg)}`);
|
|
6885
|
-
|
|
6886
|
-
|
|
7107
|
+
const { severity, enabled, exclude } = cfg;
|
|
7108
|
+
if (severity !== void 0 && !isSeverity(severity)) {
|
|
7109
|
+
bad(`rules.${name}.severity must be "block" or "warn", got ${show(severity)}`);
|
|
6887
7110
|
}
|
|
6888
|
-
if (
|
|
6889
|
-
bad(`rules.${name}.enabled must be true or false, got ${show(
|
|
7111
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") {
|
|
7112
|
+
bad(`rules.${name}.enabled must be true or false, got ${show(enabled)}`);
|
|
6890
7113
|
}
|
|
6891
|
-
if (
|
|
6892
|
-
bad(`rules.${name}.exclude must be a list of globs, got ${show(
|
|
7114
|
+
if (exclude !== void 0 && !isStringList(exclude)) {
|
|
7115
|
+
bad(`rules.${name}.exclude must be a list of globs, got ${show(exclude)}`);
|
|
6893
7116
|
}
|
|
7117
|
+
rules[name] = {
|
|
7118
|
+
...severity !== void 0 ? { severity } : {},
|
|
7119
|
+
...enabled !== void 0 ? { enabled } : {},
|
|
7120
|
+
...exclude !== void 0 ? { exclude } : {}
|
|
7121
|
+
};
|
|
6894
7122
|
}
|
|
7123
|
+
out2.rules = rules;
|
|
7124
|
+
}
|
|
7125
|
+
if (r.ignore !== void 0) {
|
|
7126
|
+
if (!isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
|
|
7127
|
+
out2.ignore = r.ignore;
|
|
6895
7128
|
}
|
|
6896
|
-
if (r.ignore !== void 0 && !isStringList(r.ignore)) bad(`ignore must be a list of globs, got ${show(r.ignore)}`);
|
|
6897
7129
|
if (r.protected !== void 0) {
|
|
6898
7130
|
if (!isMapping(r.protected)) bad(`protected must be a mapping of category to a list of globs, got ${show(r.protected)}`);
|
|
7131
|
+
const categories = {};
|
|
6899
7132
|
for (const [cat, globs4] of Object.entries(r.protected)) {
|
|
6900
7133
|
if (!isStringList(globs4)) bad(`protected.${cat} must be a list of globs, got ${show(globs4)}`);
|
|
7134
|
+
categories[cat] = globs4;
|
|
6901
7135
|
}
|
|
7136
|
+
out2.protected = categories;
|
|
6902
7137
|
}
|
|
6903
7138
|
if (r.signoff !== void 0) {
|
|
6904
7139
|
if (!isMapping(r.signoff)) bad(`signoff must be a mapping, got ${show(r.signoff)}`);
|
|
7140
|
+
const signoff = {};
|
|
6905
7141
|
for (const key2 of ["required_for", "requiredFor"]) {
|
|
6906
7142
|
const v = r.signoff[key2];
|
|
6907
|
-
if (v
|
|
7143
|
+
if (v === void 0) continue;
|
|
7144
|
+
if (!(Array.isArray(v) && v.every(isSeverity))) {
|
|
6908
7145
|
bad(`signoff.${key2} must be a list of "block" / "warn", got ${show(v)}`);
|
|
6909
7146
|
}
|
|
7147
|
+
signoff[key2] = v;
|
|
6910
7148
|
}
|
|
6911
7149
|
const ledger = r.signoff.ledger;
|
|
6912
7150
|
if (ledger !== void 0 && typeof ledger !== "string") bad(`signoff.ledger must be a path, got ${show(ledger)}`);
|
|
6913
|
-
if (typeof ledger === "string"
|
|
6914
|
-
bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
|
|
7151
|
+
if (typeof ledger === "string") {
|
|
7152
|
+
if (!ledgerInsideRepo(ledger)) bad(`signoff.ledger must be a relative path inside the repository, got ${show(ledger)}`);
|
|
7153
|
+
signoff.ledger = ledger;
|
|
6915
7154
|
}
|
|
7155
|
+
out2.signoff = signoff;
|
|
6916
7156
|
}
|
|
6917
7157
|
if (r.verify !== void 0) {
|
|
6918
7158
|
if (!isMapping(r.verify)) bad(`verify must be a mapping like { command: "npm test" }, got ${show(r.verify)}`);
|
|
6919
7159
|
const v = r.verify;
|
|
6920
|
-
|
|
6921
|
-
if (
|
|
6922
|
-
|
|
7160
|
+
const { command, budget, inputs, backend, image } = v;
|
|
7161
|
+
if (command !== void 0 && typeof command !== "string") bad(`verify.command must be a string, got ${show(command)}`);
|
|
7162
|
+
if (budget !== void 0 && !(typeof budget === "number" && Number.isFinite(budget) && budget > 0)) {
|
|
7163
|
+
bad(`verify.budget must be a positive number of seconds, got ${show(budget)}`);
|
|
6923
7164
|
}
|
|
6924
|
-
if (
|
|
6925
|
-
if (
|
|
6926
|
-
bad(`verify.backend must be "local" or "container", got ${show(
|
|
7165
|
+
if (inputs !== void 0 && !isStringList(inputs)) bad(`verify.inputs must be a list of globs, got ${show(inputs)}`);
|
|
7166
|
+
if (backend !== void 0 && backend !== "local" && backend !== "container") {
|
|
7167
|
+
bad(`verify.backend must be "local" or "container", got ${show(backend)}`);
|
|
6927
7168
|
}
|
|
6928
|
-
if (
|
|
6929
|
-
bad(`verify.image must be an immutable container image reference, got ${show(
|
|
7169
|
+
if (image !== void 0 && typeof image !== "string") {
|
|
7170
|
+
bad(`verify.image must be an immutable container image reference, got ${show(image)}`);
|
|
6930
7171
|
}
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
if (
|
|
6934
|
-
|
|
6935
|
-
bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(v.image)}`);
|
|
7172
|
+
if ((backend ?? "local") === "container") {
|
|
7173
|
+
if (typeof image !== "string") bad('verify.image is required when verify.backend is "container"');
|
|
7174
|
+
if (!/^(?!-)[^\s@]+@sha256:[0-9a-f]{64}$/i.test(image)) {
|
|
7175
|
+
bad(`verify.image must be pinned by sha256 digest (name@sha256:<64 hex>), got ${show(image)}`);
|
|
6936
7176
|
}
|
|
6937
|
-
} else if (
|
|
7177
|
+
} else if (image !== void 0) {
|
|
6938
7178
|
bad('verify.image is only valid when verify.backend is "container"');
|
|
6939
7179
|
}
|
|
7180
|
+
out2.verify = {
|
|
7181
|
+
...command !== void 0 ? { command } : {},
|
|
7182
|
+
...budget !== void 0 ? { budget } : {},
|
|
7183
|
+
...inputs !== void 0 ? { inputs } : {},
|
|
7184
|
+
...backend !== void 0 ? { backend } : {},
|
|
7185
|
+
...image !== void 0 ? { image } : {}
|
|
7186
|
+
};
|
|
6940
7187
|
}
|
|
7188
|
+
return out2;
|
|
6941
7189
|
}
|
|
6942
7190
|
function parsePolicy(raw, where = POLICY_FILE) {
|
|
6943
|
-
const
|
|
6944
|
-
|
|
6945
|
-
|
|
7191
|
+
const doc = raw ?? {};
|
|
7192
|
+
if (!isMapping(doc)) throw new PolicyError(`${where} is not a policy mapping`);
|
|
7193
|
+
const version = normalizeVersion(doc.version, where);
|
|
7194
|
+
const r = validate(doc, where);
|
|
6946
7195
|
const base = defaultPolicy(version);
|
|
6947
7196
|
const globs4 = (list2) => list2?.map(normalizeGlob);
|
|
6948
7197
|
const userProtected = r.protected ? Object.fromEntries(Object.entries(r.protected).map(([cat, list2]) => [cat, globs4(list2) ?? []])) : void 0;
|
|
@@ -6981,10 +7230,7 @@ function parseOrThrow(src, where) {
|
|
|
6981
7230
|
try {
|
|
6982
7231
|
raw = parse2(src);
|
|
6983
7232
|
} catch (e) {
|
|
6984
|
-
throw new PolicyError(`${where} is not valid YAML: ${e
|
|
6985
|
-
}
|
|
6986
|
-
if (raw !== null && raw !== void 0 && (typeof raw !== "object" || Array.isArray(raw))) {
|
|
6987
|
-
throw new PolicyError(`${where} is not a policy mapping`);
|
|
7233
|
+
throw new PolicyError(`${where} is not valid YAML: ${errorMessage(e)}`);
|
|
6988
7234
|
}
|
|
6989
7235
|
return parsePolicy(raw, where);
|
|
6990
7236
|
}
|
|
@@ -7004,6 +7250,14 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
7004
7250
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync6, existsSync as existsSync5 } from "node:fs";
|
|
7005
7251
|
import { dirname as dirname2, join as join8 } from "node:path";
|
|
7006
7252
|
var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
7253
|
+
function ledgerEntryFrom(value) {
|
|
7254
|
+
if (!isRecord(value)) return null;
|
|
7255
|
+
const { rule, file, fingerprint: fingerprint2, reason, recordedAt, expiresAt } = value;
|
|
7256
|
+
if (typeof rule !== "string" || typeof fingerprint2 !== "string" || typeof reason !== "string") return null;
|
|
7257
|
+
if (typeof recordedAt !== "number" || typeof expiresAt !== "number") return null;
|
|
7258
|
+
if (file !== void 0 && typeof file !== "string") return null;
|
|
7259
|
+
return { rule, ...file !== void 0 ? { file } : {}, fingerprint: fingerprint2, reason, recordedAt, expiresAt };
|
|
7260
|
+
}
|
|
7007
7261
|
function fingerprint(rule, file, evidence) {
|
|
7008
7262
|
return createHash2("sha256").update(`${rule}\0${file ?? ""}\0${evidence}`).digest("hex").slice(0, 16);
|
|
7009
7263
|
}
|
|
@@ -7022,8 +7276,8 @@ function readLedger(cwd, policy) {
|
|
|
7022
7276
|
for (const line of readFileSync6(p, "utf8").split("\n")) {
|
|
7023
7277
|
if (!line.trim()) continue;
|
|
7024
7278
|
try {
|
|
7025
|
-
const e = JSON.parse(line);
|
|
7026
|
-
if (e
|
|
7279
|
+
const e = ledgerEntryFrom(JSON.parse(line));
|
|
7280
|
+
if (e) out2.push(e);
|
|
7027
7281
|
} catch {
|
|
7028
7282
|
}
|
|
7029
7283
|
}
|
|
@@ -7059,9 +7313,10 @@ function oobToken(want, oob, head) {
|
|
|
7059
7313
|
}
|
|
7060
7314
|
const [rule, sha2] = [t.slice(0, at), t.slice(at + 1)];
|
|
7061
7315
|
if (rule !== want) continue;
|
|
7062
|
-
|
|
7316
|
+
if (!head) return t;
|
|
7317
|
+
const normalizedHead = head.trim().toLowerCase();
|
|
7063
7318
|
const normalizedSha = sha2.trim().toLowerCase();
|
|
7064
|
-
if (
|
|
7319
|
+
if (/^[0-9a-f]+$/.test(normalizedHead) && normalizedSha === normalizedHead) return t;
|
|
7065
7320
|
}
|
|
7066
7321
|
return null;
|
|
7067
7322
|
}
|
|
@@ -7293,7 +7548,7 @@ function isGitHubActions(env = process.env) {
|
|
|
7293
7548
|
// src/cli/report.ts
|
|
7294
7549
|
var FORMATS = ["auto", "text", "json", "github"];
|
|
7295
7550
|
function isFormat(s) {
|
|
7296
|
-
return FORMATS.
|
|
7551
|
+
return FORMATS.some((f) => f === s);
|
|
7297
7552
|
}
|
|
7298
7553
|
function resolveFormat(input, env = process.env) {
|
|
7299
7554
|
if (input.json) return "json";
|
|
@@ -7471,6 +7726,17 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
7471
7726
|
import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync8, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7472
7727
|
import { isAbsolute as isAbsolute7, join as join10 } from "node:path";
|
|
7473
7728
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7729
|
+
function ptreeFrom(value) {
|
|
7730
|
+
if (!isRecord(value)) return null;
|
|
7731
|
+
const out2 = {};
|
|
7732
|
+
for (const [path, entry] of Object.entries(value)) {
|
|
7733
|
+
if (!isRecord(entry)) return null;
|
|
7734
|
+
const { hash, mode, size, mtimeMs } = entry;
|
|
7735
|
+
if (typeof hash !== "string" || typeof mode !== "number" || typeof size !== "number" || typeof mtimeMs !== "number") return null;
|
|
7736
|
+
out2[path] = { hash, mode, size, mtimeMs };
|
|
7737
|
+
}
|
|
7738
|
+
return out2;
|
|
7739
|
+
}
|
|
7474
7740
|
var UNSAFE2 = /[^A-Za-z0-9._-]/g;
|
|
7475
7741
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", ".hg", ".svn"]);
|
|
7476
7742
|
function gitDir2(cwd) {
|
|
@@ -7562,7 +7828,7 @@ function driftBetween(expected, current) {
|
|
|
7562
7828
|
function loadTree(p) {
|
|
7563
7829
|
if (!p || !existsSync7(p)) return null;
|
|
7564
7830
|
try {
|
|
7565
|
-
return JSON.parse(readFileSync8(p, "utf8"));
|
|
7831
|
+
return ptreeFrom(JSON.parse(readFileSync8(p, "utf8")));
|
|
7566
7832
|
} catch {
|
|
7567
7833
|
return null;
|
|
7568
7834
|
}
|
|
@@ -7622,8 +7888,25 @@ function watcherHealthPath(log) {
|
|
|
7622
7888
|
function readWatcherHealth(log) {
|
|
7623
7889
|
try {
|
|
7624
7890
|
const value = JSON.parse(readFileSync9(watcherHealthPath(log), "utf8"));
|
|
7625
|
-
if (
|
|
7626
|
-
|
|
7891
|
+
if (!isRecord(value)) return null;
|
|
7892
|
+
const { state, backend, pid, started_at } = value;
|
|
7893
|
+
if (value.version !== 1 || typeof pid !== "number" || typeof started_at !== "string") return null;
|
|
7894
|
+
if (state !== "healthy" && state !== "degraded" && state !== "stopped") return null;
|
|
7895
|
+
return {
|
|
7896
|
+
version: 1,
|
|
7897
|
+
state,
|
|
7898
|
+
backend: backend === "initializing" || backend === "recursive" ? backend : "fallback",
|
|
7899
|
+
pid,
|
|
7900
|
+
started_at,
|
|
7901
|
+
stopped_at: nullableString(value.stopped_at) ?? null,
|
|
7902
|
+
watched_dirs: finiteNumber(value.watched_dirs) ?? 0,
|
|
7903
|
+
last_append_at: nullableString(value.last_append_at) ?? null,
|
|
7904
|
+
event_count: finiteNumber(value.event_count) ?? 0,
|
|
7905
|
+
dropped_events: finiteNumber(value.dropped_events) ?? 0,
|
|
7906
|
+
error_count: finiteNumber(value.error_count) ?? 0,
|
|
7907
|
+
last_error: nullableString(value.last_error) ?? null,
|
|
7908
|
+
log: stringOrUndefined(value.log) ?? log
|
|
7909
|
+
};
|
|
7627
7910
|
} catch {
|
|
7628
7911
|
return null;
|
|
7629
7912
|
}
|
|
@@ -7634,7 +7917,7 @@ function pidAlive(pid) {
|
|
|
7634
7917
|
process.kill(pid, 0);
|
|
7635
7918
|
return true;
|
|
7636
7919
|
} catch (e) {
|
|
7637
|
-
return e
|
|
7920
|
+
return errnoCode(e) === "EPERM";
|
|
7638
7921
|
}
|
|
7639
7922
|
}
|
|
7640
7923
|
function watcherTelemetry(log) {
|
|
@@ -7838,7 +8121,23 @@ function runWatch(args) {
|
|
|
7838
8121
|
|
|
7839
8122
|
// src/detectors/fs-events.ts
|
|
7840
8123
|
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync } from "node:fs";
|
|
7841
|
-
|
|
8124
|
+
function fsEventFrom(line) {
|
|
8125
|
+
let value;
|
|
8126
|
+
try {
|
|
8127
|
+
value = JSON.parse(line);
|
|
8128
|
+
} catch {
|
|
8129
|
+
return null;
|
|
8130
|
+
}
|
|
8131
|
+
if (!isRecord(value)) return null;
|
|
8132
|
+
const { ts: ts9, path, kind } = value;
|
|
8133
|
+
if (typeof ts9 !== "string" || typeof path !== "string" || kind !== "change" && kind !== "rename") return null;
|
|
8134
|
+
const mode = nullableNumber(value.mode);
|
|
8135
|
+
const size = nullableNumber(value.size);
|
|
8136
|
+
const hash = nullableString(value.hash);
|
|
8137
|
+
if (mode === void 0 || size === void 0 || hash === void 0) return null;
|
|
8138
|
+
return { ts: ts9, path, kind, mode, size, hash };
|
|
8139
|
+
}
|
|
8140
|
+
var RULE13 = "transient-protected-mutation";
|
|
7842
8141
|
var MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
7843
8142
|
var MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
7844
8143
|
var DEFAULT_EVENT_READ_IO = {
|
|
@@ -7909,11 +8208,9 @@ function readEvents(log, offset, maxBytes = MAX_EVENT_READ_BYTES, io = DEFAULT_E
|
|
|
7909
8208
|
const complete = chunk.subarray(0, lastNewline + 1).toString("utf8");
|
|
7910
8209
|
for (const line of complete.split("\n")) {
|
|
7911
8210
|
if (!line.trim()) continue;
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
malformedLines++;
|
|
7916
|
-
}
|
|
8211
|
+
const event = fsEventFrom(line);
|
|
8212
|
+
if (event) events.push(event);
|
|
8213
|
+
else malformedLines++;
|
|
7917
8214
|
}
|
|
7918
8215
|
const newOffset = start + lastNewline + 1;
|
|
7919
8216
|
return {
|
|
@@ -8033,7 +8330,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
8033
8330
|
};
|
|
8034
8331
|
}
|
|
8035
8332
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
8036
|
-
if (!isEnabled(
|
|
8333
|
+
if (!isEnabled(RULE13, policy)) return [];
|
|
8037
8334
|
const byPath = /* @__PURE__ */ new Map();
|
|
8038
8335
|
for (const e of events) {
|
|
8039
8336
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -8053,7 +8350,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
8053
8350
|
const modeChurn = modes.size > 1;
|
|
8054
8351
|
if (!contentChurn && !modeChurn) continue;
|
|
8055
8352
|
const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
|
|
8056
|
-
const f = makeFinding(
|
|
8353
|
+
const f = makeFinding(RULE13, policy, {
|
|
8057
8354
|
file: path,
|
|
8058
8355
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
8059
8356
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -8087,10 +8384,16 @@ function parseInput(raw) {
|
|
|
8087
8384
|
} catch (e) {
|
|
8088
8385
|
throw new HookInputError(`the hook payload is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
8089
8386
|
}
|
|
8090
|
-
if (
|
|
8387
|
+
if (!isRecord(parsed)) {
|
|
8091
8388
|
throw new HookInputError(`the hook payload is not a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})`);
|
|
8092
8389
|
}
|
|
8093
|
-
return
|
|
8390
|
+
return {
|
|
8391
|
+
...typeof parsed.tool_name === "string" ? { tool_name: parsed.tool_name } : {},
|
|
8392
|
+
...isRecord(parsed.tool_input) ? { tool_input: parsed.tool_input } : {},
|
|
8393
|
+
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
8394
|
+
...typeof parsed.stop_hook_active === "boolean" ? { stop_hook_active: parsed.stop_hook_active } : {},
|
|
8395
|
+
...typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}
|
|
8396
|
+
};
|
|
8094
8397
|
}
|
|
8095
8398
|
function recordDenylog(blocks2) {
|
|
8096
8399
|
const log = process.env.TAMPERWARD_DENYLOG;
|
|
@@ -8733,6 +9036,9 @@ function missingTools(matcher) {
|
|
|
8733
9036
|
if (have === null) return [];
|
|
8734
9037
|
return PRE_MATCHER.split("|").filter((t) => !have.has(t));
|
|
8735
9038
|
}
|
|
9039
|
+
function isHooksShape(hooks) {
|
|
9040
|
+
return hooksShapeError(hooks) === null;
|
|
9041
|
+
}
|
|
8736
9042
|
function hooksShapeError(hooks) {
|
|
8737
9043
|
if (hooks === void 0 || hooks === null) return null;
|
|
8738
9044
|
if (!isPlainObject(hooks)) return "hooks is not an object";
|
|
@@ -8755,7 +9061,7 @@ function verifierCandidates(cwd) {
|
|
|
8755
9061
|
};
|
|
8756
9062
|
try {
|
|
8757
9063
|
const pkg = JSON.parse(readFileSync11(join12(cwd, "package.json"), "utf8"));
|
|
8758
|
-
const test = pkg.scripts
|
|
9064
|
+
const test = isRecord(pkg) && isRecord(pkg.scripts) ? pkg.scripts.test : void 0;
|
|
8759
9065
|
if (typeof test === "string" && test.trim() !== "" && !/error:\s*no test specified/i.test(test)) add("npm test");
|
|
8760
9066
|
} catch {
|
|
8761
9067
|
}
|
|
@@ -8854,14 +9160,14 @@ function planClaudeHooks(cwd) {
|
|
|
8854
9160
|
detail: "exists but is not valid JSON \u2014 fix it, then re-run init (refusing to overwrite)"
|
|
8855
9161
|
};
|
|
8856
9162
|
}
|
|
8857
|
-
if (
|
|
9163
|
+
if (!isRecord(parsed)) {
|
|
8858
9164
|
return { item: "agent", path: rel, status: "error", detail: "exists but is not a JSON object \u2014 refusing to overwrite" };
|
|
8859
9165
|
}
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
9166
|
+
const shapeError = hooksShapeError(parsed.hooks);
|
|
9167
|
+
if (shapeError || !isHooksShape(parsed.hooks)) {
|
|
9168
|
+
return { item: "agent", path: rel, status: "error", detail: `${shapeError ?? "hooks is not the shape Claude Code reads"} \u2014 fix it, then re-run init (refusing to overwrite)` };
|
|
9169
|
+
}
|
|
9170
|
+
settings = { ...parsed, hooks: parsed.hooks ?? void 0 };
|
|
8865
9171
|
}
|
|
8866
9172
|
const hooks = settings.hooks ??= {};
|
|
8867
9173
|
const entriesWith = (arr, needle) => (arr ?? []).filter((m) => (m.hooks ?? []).some((h) => needle.test(String(h.command ?? ""))));
|
|
@@ -9085,7 +9391,7 @@ function planCodeowners(cwd) {
|
|
|
9085
9391
|
const path = join12(cwd, existingRel ?? rel);
|
|
9086
9392
|
const existing = existingRel ? readFileSync11(path, "utf8") : null;
|
|
9087
9393
|
const missing = CODEOWNERS_PATHS.filter((p) => existing === null || !coveredBy(existing, p));
|
|
9088
|
-
if (missing.length === 0) {
|
|
9394
|
+
if (existingRel !== void 0 && missing.length === 0) {
|
|
9089
9395
|
return { item: "codeowners", path: existingRel, status: "ok", detail: "gate paths already have code owners" };
|
|
9090
9396
|
}
|
|
9091
9397
|
const inferred = inferOwner(cwd);
|
|
@@ -9711,14 +10017,15 @@ function decodeUtf8Tail(tail) {
|
|
|
9711
10017
|
while (start < tail.length && (tail[start] & 192) === 128) start++;
|
|
9712
10018
|
return tail.subarray(start).toString("utf8");
|
|
9713
10019
|
}
|
|
9714
|
-
function streamFromRaw(
|
|
10020
|
+
function streamFromRaw(value) {
|
|
10021
|
+
const raw = isRecord(value) ? value : {};
|
|
9715
10022
|
let tail = Buffer.alloc(0);
|
|
9716
10023
|
try {
|
|
9717
|
-
tail = Buffer.from(raw
|
|
10024
|
+
tail = Buffer.from(typeof raw.tail_b64 === "string" ? raw.tail_b64 : "", "base64");
|
|
9718
10025
|
} catch {
|
|
9719
10026
|
tail = Buffer.alloc(0);
|
|
9720
10027
|
}
|
|
9721
|
-
const captured = Number.isFinite(raw
|
|
10028
|
+
const captured = typeof raw.captured_bytes === "number" && Number.isFinite(raw.captured_bytes) ? Math.max(0, raw.captured_bytes) : tail.length;
|
|
9722
10029
|
return {
|
|
9723
10030
|
captured_bytes: captured,
|
|
9724
10031
|
retained_bytes: tail.length,
|
|
@@ -9729,7 +10036,7 @@ function streamFromRaw(raw) {
|
|
|
9729
10036
|
function scrubDiagnosticText(input) {
|
|
9730
10037
|
let out2 = "";
|
|
9731
10038
|
for (const ch of input) {
|
|
9732
|
-
const cp = ch.codePointAt(0);
|
|
10039
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
9733
10040
|
if (ch === "\n" || ch === " ") {
|
|
9734
10041
|
out2 += ch;
|
|
9735
10042
|
} else if (cp === 13) {
|
|
@@ -9932,7 +10239,7 @@ function parseCapturedSupervisorResult(stdoutText) {
|
|
|
9932
10239
|
} catch {
|
|
9933
10240
|
return null;
|
|
9934
10241
|
}
|
|
9935
|
-
if (!
|
|
10242
|
+
if (!isRecord(raw)) return null;
|
|
9936
10243
|
const diagnostics = {
|
|
9937
10244
|
stdout: streamFromRaw(raw.stdout),
|
|
9938
10245
|
stderr: streamFromRaw(raw.stderr)
|
|
@@ -9983,7 +10290,7 @@ function runCapturedProcessSync(executable, args, opts) {
|
|
|
9983
10290
|
const parsed = parseCapturedSupervisorResult(supervisor.stdout ?? "");
|
|
9984
10291
|
const diagnostics = parsed?.diagnostics ?? emptySuiteDiagnostics();
|
|
9985
10292
|
if (!parsed) {
|
|
9986
|
-
const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error
|
|
10293
|
+
const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
|
|
9987
10294
|
return {
|
|
9988
10295
|
exit: null,
|
|
9989
10296
|
signal: supervisor.signal ? String(supervisor.signal) : null,
|
|
@@ -10078,16 +10385,16 @@ function dockerAvailable(enginePath, host) {
|
|
|
10078
10385
|
}
|
|
10079
10386
|
}
|
|
10080
10387
|
function declaredImageVolumePaths(metadata) {
|
|
10081
|
-
if (!
|
|
10388
|
+
if (!isRecord(metadata)) {
|
|
10082
10389
|
throw new Error("image metadata must be a Docker image-inspect object");
|
|
10083
10390
|
}
|
|
10084
10391
|
const config = metadata.Config;
|
|
10085
|
-
if (!
|
|
10392
|
+
if (!isRecord(config)) {
|
|
10086
10393
|
throw new Error("image metadata Config must be an object");
|
|
10087
10394
|
}
|
|
10088
10395
|
const volumes = config.Volumes;
|
|
10089
10396
|
if (volumes === void 0 || volumes === null) return [];
|
|
10090
|
-
if (
|
|
10397
|
+
if (!isRecord(volumes)) {
|
|
10091
10398
|
throw new Error("image metadata Config.Volumes must be a mapping or null");
|
|
10092
10399
|
}
|
|
10093
10400
|
return Object.keys(volumes).sort();
|
|
@@ -10591,8 +10898,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
10591
10898
|
const pending = linkParts(target);
|
|
10592
10899
|
const seen = /* @__PURE__ */ new Set();
|
|
10593
10900
|
let hops = 0;
|
|
10594
|
-
|
|
10595
|
-
const part = pending.shift();
|
|
10901
|
+
for (let part = pending.shift(); part !== void 0; part = pending.shift()) {
|
|
10596
10902
|
if (part === "..") {
|
|
10597
10903
|
if (!stack.length) throw linkEscape(label, target);
|
|
10598
10904
|
stack.pop();
|
|
@@ -10604,7 +10910,7 @@ function validateSymlinkGraph(target, out2, root, dependencyRoot, label) {
|
|
|
10604
10910
|
stack = [];
|
|
10605
10911
|
continue;
|
|
10606
10912
|
}
|
|
10607
|
-
const base = domain === "
|
|
10913
|
+
const base = domain === "deps" && dependencyRoot ? dependencyRoot : root;
|
|
10608
10914
|
const current = join16(base, ...stack);
|
|
10609
10915
|
let st;
|
|
10610
10916
|
try {
|
|
@@ -11623,7 +11929,7 @@ function runAgentSupervised(argv2, cwd, budgetSecs, linuxPythonCandidates, lifec
|
|
|
11623
11929
|
}
|
|
11624
11930
|
}
|
|
11625
11931
|
);
|
|
11626
|
-
const supervisorTimedOut = Boolean(supervisor.error) && supervisor.error
|
|
11932
|
+
const supervisorTimedOut = Boolean(supervisor.error) && errnoCode(supervisor.error) === "ETIMEDOUT";
|
|
11627
11933
|
const completedNormally = !supervisorTimedOut && !supervisor.error && supervisor.status === 0 && supervisor.signal == null;
|
|
11628
11934
|
if (!completedNormally) {
|
|
11629
11935
|
return {
|
|
@@ -11674,7 +11980,7 @@ function pidAlive2(pid) {
|
|
|
11674
11980
|
process.kill(pid, 0);
|
|
11675
11981
|
return true;
|
|
11676
11982
|
} catch (e) {
|
|
11677
|
-
return e
|
|
11983
|
+
return errnoCode(e) === "EPERM";
|
|
11678
11984
|
}
|
|
11679
11985
|
}
|
|
11680
11986
|
function supervisedObserverLog(cwd) {
|
|
@@ -11717,7 +12023,7 @@ function stopObserverProcess(observer) {
|
|
|
11717
12023
|
waitMs(100);
|
|
11718
12024
|
const beforeStop = watcherTelemetry(observer.log);
|
|
11719
12025
|
const pid = observer.pid;
|
|
11720
|
-
if (pidAlive2(pid)) {
|
|
12026
|
+
if (pid !== null && pidAlive2(pid)) {
|
|
11721
12027
|
try {
|
|
11722
12028
|
process.kill(pid, "SIGTERM");
|
|
11723
12029
|
} catch {
|
|
@@ -12191,7 +12497,7 @@ function policyFor(opts, cwd) {
|
|
|
12191
12497
|
return atBase ?? defaultPolicy();
|
|
12192
12498
|
}
|
|
12193
12499
|
function asMapping(value) {
|
|
12194
|
-
return
|
|
12500
|
+
return isRecord(value) ? value : null;
|
|
12195
12501
|
}
|
|
12196
12502
|
function wiringState(status, authority) {
|
|
12197
12503
|
if (status === "ok") return "OK";
|
|
@@ -12512,8 +12818,8 @@ function githubApi(cwd, endpoint) {
|
|
|
12512
12818
|
);
|
|
12513
12819
|
return JSON.parse(stdout);
|
|
12514
12820
|
} catch (e) {
|
|
12515
|
-
const
|
|
12516
|
-
const detail =
|
|
12821
|
+
const failure = execFailure(e);
|
|
12822
|
+
const detail = failure.stderr ? failure.stderr.replace(/\s+/g, " ").trim() : failure.message;
|
|
12517
12823
|
throw new Error(
|
|
12518
12824
|
"GitHub API " + endpoint + " failed: " + (detail || "unknown error")
|
|
12519
12825
|
);
|
|
@@ -12824,7 +13130,7 @@ function summarizeTraceRuns(opts) {
|
|
|
12824
13130
|
for (const item of run) {
|
|
12825
13131
|
const rel = normalRepoPath(opts.root, item.path);
|
|
12826
13132
|
const isTracked = rel !== null && opts.tracked.has(rel);
|
|
12827
|
-
const keyPath =
|
|
13133
|
+
const keyPath = rel !== null ? opts.tracked.has(rel) ? rel : null : resolve9(opts.root, item.path);
|
|
12828
13134
|
if (keyPath === null) continue;
|
|
12829
13135
|
const target = isTracked ? repo : external;
|
|
12830
13136
|
const key2 = `${isTracked ? "repo" : "external"}\0${keyPath}`;
|
|
@@ -12909,7 +13215,7 @@ function materializeBase(base, cwd, dest) {
|
|
|
12909
13215
|
}
|
|
12910
13216
|
function traceFiles(prefix) {
|
|
12911
13217
|
const dir = resolve9(prefix, "..");
|
|
12912
|
-
const base = prefix.split(sep3).at(-1);
|
|
13218
|
+
const base = prefix.split(sep3).at(-1) ?? prefix;
|
|
12913
13219
|
return readdirSync6(dir).filter((name) => name === base || name.startsWith(base + ".")).map((name) => join19(dir, name)).sort();
|
|
12914
13220
|
}
|
|
12915
13221
|
function rewriteTraceRoot(accesses, actualRoot) {
|