tamperward 2.24.2 → 2.24.4

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +525 -22
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -3713,6 +3713,83 @@ var init_lint_suppression = __esm({
3713
3713
  }
3714
3714
  });
3715
3715
 
3716
+ // src/detectors/runner-chain.ts
3717
+ function readRunnerChain(root, props) {
3718
+ let runner = root;
3719
+ const modifiers = [];
3720
+ let table = null;
3721
+ let skipMarker = null;
3722
+ let skipIndex = -1;
3723
+ for (let i = 0; i < props.length; i++) {
3724
+ const p = props[i];
3725
+ if (table) return null;
3726
+ if (RUNNER_NAMES.has(p) && modifiers.length === 0) {
3727
+ runner = p;
3728
+ continue;
3729
+ }
3730
+ if (CHAIN_MODIFIERS.has(p)) {
3731
+ modifiers.push(p);
3732
+ const marker = skipMarkerOf(p);
3733
+ if (marker && !skipMarker) {
3734
+ skipMarker = marker;
3735
+ skipIndex = i;
3736
+ }
3737
+ continue;
3738
+ }
3739
+ const t = tableOf(p);
3740
+ if (t) {
3741
+ table = t;
3742
+ continue;
3743
+ }
3744
+ return null;
3745
+ }
3746
+ return { runner, modifiers, table, skipMarker, skipIndex };
3747
+ }
3748
+ function readCallee(expr) {
3749
+ const props = [];
3750
+ let cur = expr;
3751
+ while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
3752
+ if (ts.isPropertyAccessExpression(cur)) {
3753
+ props.unshift(cur.name.text);
3754
+ } else {
3755
+ const arg = cur.argumentExpression;
3756
+ if (!ts.isStringLiteralLike(arg)) return null;
3757
+ props.unshift(arg.text);
3758
+ }
3759
+ cur = cur.expression;
3760
+ }
3761
+ if (!ts.isIdentifier(cur)) return null;
3762
+ return readRunnerChain(cur.text, props);
3763
+ }
3764
+ function readTableCallee(expr) {
3765
+ const head = ts.isCallExpression(expr) ? expr.expression : ts.isTaggedTemplateExpression(expr) ? expr.tag : null;
3766
+ if (!head) return null;
3767
+ const chain = readCallee(head);
3768
+ return chain?.table ? chain : null;
3769
+ }
3770
+ var RUNNER_NAMES, CHAIN_MODIFIERS, skipMarkerOf, tableOf;
3771
+ var init_runner_chain = __esm({
3772
+ "src/detectors/runner-chain.ts"() {
3773
+ "use strict";
3774
+ init_ts_lazy();
3775
+ RUNNER_NAMES = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
3776
+ CHAIN_MODIFIERS = /* @__PURE__ */ new Set([
3777
+ "concurrent",
3778
+ "sequential",
3779
+ "shuffle",
3780
+ "serial",
3781
+ "parallel",
3782
+ "skip",
3783
+ "only",
3784
+ "todo",
3785
+ "fails",
3786
+ "failing"
3787
+ ]);
3788
+ skipMarkerOf = (name) => name === "skip" || name === "only" || name === "todo" ? name : null;
3789
+ tableOf = (name) => name === "each" || name === "for" ? name : null;
3790
+ }
3791
+ });
3792
+
3716
3793
  // src/detectors/test-skip.ts
3717
3794
  import { posix as posix2 } from "node:path";
3718
3795
  function pyMarkAliases(source) {
@@ -3742,6 +3819,47 @@ function matchesOutsideString(p, content, lang) {
3742
3819
  }
3743
3820
  return false;
3744
3821
  }
3822
+ function afterLinesOf(c) {
3823
+ const out3 = /* @__PURE__ */ new Map();
3824
+ if (c.after != null) {
3825
+ c.after.split("\n").forEach((l, i) => out3.set(i + 1, l));
3826
+ return out3;
3827
+ }
3828
+ for (const h of c.hunks) for (const l of h.lines) if (l.newLine != null && l.type !== "del") out3.set(l.newLine, l.content);
3829
+ return out3;
3830
+ }
3831
+ function contextSkipHits(c, lang) {
3832
+ if (lang !== "go" && lang !== "rs") return [];
3833
+ const lines = afterLinesOf(c);
3834
+ const hits = [];
3835
+ for (const l of addedLines(c)) {
3836
+ if (l.newLine == null) continue;
3837
+ if (lang === "go") {
3838
+ const m = GO_TEST_MAIN.exec(l.content);
3839
+ if (!m) continue;
3840
+ const runs = new RegExp("\\b" + m[1] + "\\.Run\\(\\)");
3841
+ let ran = false;
3842
+ for (const [, text] of lines) if (runs.test(text) && !isCommentLine(text.trim(), lang)) ran = true;
3843
+ if (!ran) hits.push({ line: l.newLine, why: `a TestMain that never calls ${m[1]}.Run() (no test in the package runs)`, evidence: l.content.trim() });
3844
+ continue;
3845
+ }
3846
+ const cfg = RS_CFG.exec(l.content);
3847
+ if (!cfg || cfg[1].trim() === "test") continue;
3848
+ let first = l.newLine;
3849
+ while (RS_ATTR.test(lines.get(first - 1) ?? "")) first--;
3850
+ let at = l.newLine + 1;
3851
+ while (RS_ATTR.test(lines.get(at) ?? "") || lines.has(at) && !/\S/.test(lines.get(at) ?? "")) at++;
3852
+ const item = lines.get(at) ?? "";
3853
+ let besideTest = false;
3854
+ for (let i = first; i < at; i++) if (RS_TEST_ATTR.test(lines.get(i) ?? "")) besideTest = true;
3855
+ if (RS_MOD.test(item)) {
3856
+ hits.push({ line: l.newLine, why: "a #[cfg(\u2026)] on the test module (the tests are compiled out under every ordinary configuration)", evidence: l.content.trim() });
3857
+ } else if (besideTest && RS_FN.test(item)) {
3858
+ hits.push({ line: l.newLine, why: "a #[cfg(\u2026)] beside #[test] (the test is compiled out under every ordinary configuration)", evidence: l.content.trim() });
3859
+ }
3860
+ }
3861
+ return hits;
3862
+ }
3745
3863
  function scriptKind2(path) {
3746
3864
  if (/\.tsx$/i.test(path)) return ts.ScriptKind.TSX;
3747
3865
  if (/\.jsx$/i.test(path)) return ts.ScriptKind.JSX;
@@ -3886,12 +4004,14 @@ function staticPropertyName(expr, ctx, strings) {
3886
4004
  }
3887
4005
  function runnerChain(expr, ctx, binder, strings) {
3888
4006
  const props = [];
4007
+ const propNodes = [];
3889
4008
  const causes = [];
3890
4009
  let cur = expr;
3891
4010
  let terminalNode = expr;
3892
4011
  while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
3893
4012
  if (ts.isPropertyAccessExpression(cur)) {
3894
4013
  props.unshift(cur.name.text);
4014
+ propNodes.unshift(cur.name);
3895
4015
  if (props.length === 1) terminalNode = cur.name;
3896
4016
  cur = cur.expression;
3897
4017
  continue;
@@ -3900,6 +4020,7 @@ function runnerChain(expr, ctx, binder, strings) {
3900
4020
  const property = staticPropertyName(cur.argumentExpression, ctx, strings);
3901
4021
  if (!property) return null;
3902
4022
  props.unshift(property.name);
4023
+ propNodes.unshift(cur.argumentExpression);
3903
4024
  causes.push(...property.causes);
3904
4025
  if (props.length === 1) terminalNode = cur.argumentExpression;
3905
4026
  cur = cur.expression;
@@ -3916,17 +4037,21 @@ function runnerChain(expr, ctx, binder, strings) {
3916
4037
  }
3917
4038
  } else {
3918
4039
  namespace = requiredModule(cur);
4040
+ if (namespace == null && readTableCallee(cur)) return null;
3919
4041
  verdict2 = namespace == null ? UNKNOWN : NON_RUNNER;
3920
4042
  }
3921
4043
  let root = ts.isIdentifier(cur) ? cur.text : "";
3922
4044
  if (namespace != null && props.length > 0) {
3923
4045
  verdict2 = withCause(binder.memberOf(namespace, props[0]), cur);
3924
- if (verdict2.kind === "runner") root = props.shift() ?? root;
4046
+ if (verdict2.kind === "runner") {
4047
+ root = props.shift() ?? root;
4048
+ propNodes.shift();
4049
+ }
3925
4050
  }
3926
4051
  if (verdict2.kind === "unknown") return "unknown";
3927
4052
  if (verdict2.kind === "non-runner") return null;
3928
4053
  causes.push(...verdict2.causes);
3929
- return { root, props, terminalNode, causes };
4054
+ return { root, props, propNodes, terminalNode, causes };
3930
4055
  }
3931
4056
  function optionDisables(value) {
3932
4057
  return !(value.kind === ts.SyntaxKind.FalseKeyword || ts.isNumericLiteral(value) && Number(value.text) === 0);
@@ -3965,8 +4090,9 @@ function semanticSkipHits(ctx, binder) {
3965
4090
  const last = lineOf(callee.getEnd());
3966
4091
  for (let line = first; line <= last; line++) unowned.add(line);
3967
4092
  } else if (chain) {
3968
- const { props, terminalNode, causes } = chain;
4093
+ const { root, props, propNodes, terminalNode, causes } = chain;
3969
4094
  const terminal = props.at(-1);
4095
+ const shape = readRunnerChain(root, props);
3970
4096
  const push = (target, extraCauses, why) => {
3971
4097
  hits.push({
3972
4098
  semanticKey: `${callKey}\0${why}`,
@@ -3976,13 +4102,13 @@ function semanticSkipHits(ctx, binder) {
3976
4102
  evidence: callText
3977
4103
  });
3978
4104
  };
3979
- if (terminal && (terminal === "skip" || terminal === "only" || terminal === "todo") && props.slice(0, -1).every((p) => JS_CHAIN_MODIFIERS.has(p) || JS_RUNNERS.has(p))) {
3980
- push(terminalNode, [], "a .skip/.only/.todo marker");
4105
+ if (shape?.skipMarker) {
4106
+ push(propNodes[shape.skipIndex] ?? terminalNode, [], "a .skip/.only/.todo marker");
3981
4107
  } else if (terminal && ["skipIf", "runIf"].includes(terminal) && props.length === 1) {
3982
4108
  push(terminalNode, [], "a .skipIf()/.runIf() condition (the test runs only when the condition allows)");
3983
4109
  } else if (terminal && ["fails", "failing"].includes(terminal) && props.length === 1) {
3984
4110
  push(terminalNode, [], "a .fails/.failing marker (the test now passes by failing)");
3985
- } else if (terminal === "each" && props.length === 1 && node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
4111
+ } else if (shape?.table === "each" && node.arguments.length > 0 && ts.isArrayLiteralExpression(node.arguments[0]) && node.arguments[0].elements.length === 0) {
3986
4112
  push(terminalNode, [], "an empty .each table (no case ever runs)");
3987
4113
  }
3988
4114
  if (props.length === 0) {
@@ -4062,11 +4188,12 @@ function astSkipHits(c, resolvers) {
4062
4188
  }
4063
4189
  return { hits, authoritative: true, unowned: analysis.unowned };
4064
4190
  }
4065
- var RULE4, acc, JS_RUNNER, JS_MOD, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS, JS_CHAIN_MODIFIERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
4191
+ var RULE4, acc, JS_RUNNER, JS_MOD, JVM_CONDITIONAL, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, GO_TEST_MAIN, RS_CFG, RS_ATTR, RS_TEST_ATTR, RS_MOD, RS_FN, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
4066
4192
  var init_test_skip = __esm({
4067
4193
  "src/detectors/test-skip.ts"() {
4068
4194
  "use strict";
4069
4195
  init_ts_lazy();
4196
+ init_runner_chain();
4070
4197
  init_select();
4071
4198
  init_policy();
4072
4199
  init_repo();
@@ -4076,6 +4203,7 @@ var init_test_skip = __esm({
4076
4203
  acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
4077
4204
  JS_RUNNER = "\\b(?:it|test|describe|suite)";
4078
4205
  JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
4206
+ JVM_CONDITIONAL = /@(?:[\w.]+\.)?(?:EnabledIf|DisabledIf)\w*\b/;
4079
4207
  GO_SKIP_PATTERNS = [
4080
4208
  { re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
4081
4209
  { re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
@@ -4142,7 +4270,10 @@ var init_test_skip = __esm({
4142
4270
  ...GO_SKIP_PATTERNS,
4143
4271
  // #441: any receiver, and testify's T()
4144
4272
  { re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
4145
- { re: /^\s*\/\/\s*(?:go:build|\+build)\s+ignore\b/, why: "a build-ignore constraint (the file is excluded from the test run)", comment: true }
4273
+ // Any constraint on a `_test.go` — `ignore`, `integration`, `never`, `!ci` — takes
4274
+ // the file out of the default `go test ./...` run; the tag's name is the agent's
4275
+ // to choose (#431).
4276
+ { re: /^\s*\/\/\s*(?:go:build|\+build)\b/, why: "a build constraint on a test file (the file is excluded from the default test run)", comment: true }
4146
4277
  ],
4147
4278
  rs: [
4148
4279
  { re: /#\[ignore\b/, why: "an #[ignore] attribute (the test no longer runs by default)" },
@@ -4153,14 +4284,19 @@ var init_test_skip = __esm({
4153
4284
  { re: /(?:^|[\s;{(])x(?:it|describe|context|specify|example)\s*[('"]/, why: "an x-prefixed disabled example" },
4154
4285
  { re: /(?:^|[\s;{(])f(?:it|describe|context)\s*[('"]/, why: "an f-prefixed focused example (narrows the suite)" },
4155
4286
  { re: /\b(?:skip|pending):\s*(?:true|['"])/, why: "a skip/pending metadata flag" },
4156
- { re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" }
4287
+ { re: /^\s*(?:it|specify|example)\s+(['"])(?:(?!\1).)*\1\s*$/, why: "an example with no block (RSpec reports it as pending)" },
4288
+ // `it 'x', if: false do` — RSpec's conditional filter with a constant that never
4289
+ // lets the example run (#431). `if: ENV['SLOW']` is a real condition and passes.
4290
+ { re: /(?:^|[\s;{(])(?:it|specify|example|describe|context|scenario|feature)\s*[('"].*(?:\bif:\s*false\b|\bunless:\s*true\b|:if\s*=>\s*false\b|:unless\s*=>\s*true\b)/, why: "a constant-false if:/unless: filter (the example never runs)" }
4157
4291
  ],
4158
4292
  java: [
4159
4293
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4294
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4160
4295
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4161
4296
  ],
4162
4297
  kt: [
4163
4298
  { re: /@(?:[\w.]+\.)?(?:Ignore|Disabled)\b/, why: "an @Ignore/@Disabled annotation" },
4299
+ { re: JVM_CONDITIONAL, why: "a conditional-execution annotation (@EnabledIf\u2026 / @DisabledIf\u2026: the test runs only when the condition allows)" },
4164
4300
  { re: /\bassume(?:True\(\s*false|False\(\s*true)\s*[,)]/, why: "an assumption that never holds (the test aborts as skipped)" }
4165
4301
  ],
4166
4302
  php: [{ re: /\bmarkTest(?:Skipped|Incomplete)\(/, why: "a markTestSkipped()/markTestIncomplete() call" }],
@@ -4172,7 +4308,6 @@ var init_test_skip = __esm({
4172
4308
  ]
4173
4309
  };
4174
4310
  JS_RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe", "suite"]);
4175
- JS_CHAIN_MODIFIERS = /* @__PURE__ */ new Set(["concurrent", "sequential", "shuffle", "serial", "parallel"]);
4176
4311
  JS_DEFAULT_RUNNER_MODULES = /* @__PURE__ */ new Set(["node:test", "node:test/promises", "@playwright/test"]);
4177
4312
  JS_TEST_MODULES = /* @__PURE__ */ new Set([
4178
4313
  "vitest",
@@ -4183,6 +4318,12 @@ var init_test_skip = __esm({
4183
4318
  "bun:test",
4184
4319
  "@playwright/test"
4185
4320
  ]);
4321
+ GO_TEST_MAIN = /^func\s+TestMain\s*\(\s*(\w+)\s+\*testing\.M\s*\)/;
4322
+ RS_CFG = /^\s*#\[cfg\((.*)\)\]\s*$/;
4323
+ RS_ATTR = /^\s*#!?\[/;
4324
+ RS_TEST_ATTR = /^\s*#\[[\w:]*test\b/;
4325
+ RS_MOD = /^\s*(?:pub(?:\([^)]*\))?\s+)?mod\s+\w+/;
4326
+ RS_FN = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+/;
4186
4327
  RUNNER2 = { kind: "runner", causes: [] };
4187
4328
  NON_RUNNER = { kind: "non-runner" };
4188
4329
  UNKNOWN = { kind: "unknown" };
@@ -4474,6 +4615,18 @@ var init_test_skip = __esm({
4474
4615
  );
4475
4616
  }
4476
4617
  }
4618
+ for (const hit of contextSkipHits(c, lang)) {
4619
+ astHitLines.add(hit.line);
4620
+ out3.push(
4621
+ makeFinding(RULE4, policy, {
4622
+ file: c.path,
4623
+ line: hit.line,
4624
+ message: `Test skipped or narrowed: ${hit.why}.`,
4625
+ evidence: hit.evidence,
4626
+ remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
4627
+ })
4628
+ );
4629
+ }
4477
4630
  if (lang === "py") {
4478
4631
  for (const hit of pyAliasDecoratorHits(c)) {
4479
4632
  if (hit.line != null) astHitLines.add(hit.line);
@@ -7872,12 +8025,297 @@ They WILL FAIL in v10.0.0
7872
8025
  }
7873
8026
  });
7874
8027
 
8028
+ // src/detectors/reachability.ts
8029
+ function constTruth(expr) {
8030
+ const v = constValue(expr);
8031
+ if (v === null) return null;
8032
+ return v.kind === "unknown" ? v.truthy : Boolean(v.value);
8033
+ }
8034
+ function constValue(expr) {
8035
+ if (ts.isParenthesizedExpression(expr)) return constValue(expr.expression);
8036
+ if (expr.kind === ts.SyntaxKind.TrueKeyword) return { kind: "value", value: true };
8037
+ if (expr.kind === ts.SyntaxKind.FalseKeyword) return { kind: "value", value: false };
8038
+ if (expr.kind === ts.SyntaxKind.NullKeyword) return { kind: "value", value: null };
8039
+ if (ts.isIdentifier(expr)) {
8040
+ if (expr.text === "undefined") return { kind: "value", value: void 0 };
8041
+ if (expr.text === "NaN") return { kind: "value", value: NaN };
8042
+ return null;
8043
+ }
8044
+ if (ts.isNumericLiteral(expr)) return { kind: "value", value: Number(expr.text) };
8045
+ if (ts.isBigIntLiteral(expr)) return { kind: "unknown", truthy: !/^0+n$/.test(expr.text) };
8046
+ if (ts.isStringLiteralLike(expr)) return { kind: "value", value: expr.text };
8047
+ if (ts.isVoidExpression(expr)) return { kind: "value", value: void 0 };
8048
+ if (ts.isPrefixUnaryExpression(expr)) {
8049
+ if (expr.operator === ts.SyntaxKind.ExclamationToken) {
8050
+ const inner = constTruth(expr.operand);
8051
+ return inner === null ? null : { kind: "value", value: !inner };
8052
+ }
8053
+ if (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken) {
8054
+ const inner = constValue(expr.operand);
8055
+ if (inner === null || inner.kind !== "value" || typeof inner.value !== "number") return null;
8056
+ return { kind: "value", value: expr.operator === ts.SyntaxKind.MinusToken ? -inner.value : inner.value };
8057
+ }
8058
+ return null;
8059
+ }
8060
+ if (ts.isBinaryExpression(expr)) {
8061
+ const op = expr.operatorToken.kind;
8062
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
8063
+ const l2 = constTruth(expr.left);
8064
+ const r2 = constTruth(expr.right);
8065
+ const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
8066
+ if (and) {
8067
+ if (l2 === false || r2 === false) return { kind: "unknown", truthy: false };
8068
+ if (l2 === true) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8069
+ if (r2 === true) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8070
+ return null;
8071
+ }
8072
+ if (l2 === true || r2 === true) return { kind: "unknown", truthy: true };
8073
+ if (l2 === false) return r2 === null ? null : { kind: "unknown", truthy: r2 };
8074
+ if (r2 === false) return l2 === null ? null : { kind: "unknown", truthy: l2 };
8075
+ return null;
8076
+ }
8077
+ const l = constValue(expr.left);
8078
+ const r = constValue(expr.right);
8079
+ if (l === null || r === null || l.kind !== "value" || r.kind !== "value") return null;
8080
+ const a = l.value;
8081
+ const b = r.value;
8082
+ switch (op) {
8083
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
8084
+ return { kind: "value", value: a === b };
8085
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
8086
+ return { kind: "value", value: a !== b };
8087
+ case ts.SyntaxKind.EqualsEqualsToken:
8088
+ return { kind: "value", value: a == b };
8089
+ case ts.SyntaxKind.ExclamationEqualsToken:
8090
+ return { kind: "value", value: a != b };
8091
+ default:
8092
+ break;
8093
+ }
8094
+ if (typeof a === "number" && typeof b === "number") {
8095
+ switch (op) {
8096
+ case ts.SyntaxKind.LessThanToken:
8097
+ return { kind: "value", value: a < b };
8098
+ case ts.SyntaxKind.LessThanEqualsToken:
8099
+ return { kind: "value", value: a <= b };
8100
+ case ts.SyntaxKind.GreaterThanToken:
8101
+ return { kind: "value", value: a > b };
8102
+ case ts.SyntaxKind.GreaterThanEqualsToken:
8103
+ return { kind: "value", value: a >= b };
8104
+ default:
8105
+ break;
8106
+ }
8107
+ }
8108
+ return null;
8109
+ }
8110
+ return null;
8111
+ }
8112
+ function isTerminal(stmt) {
8113
+ return ts.isReturnStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBreakStatement(stmt) || ts.isContinueStatement(stmt);
8114
+ }
8115
+ function namedFunctionBody(node) {
8116
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) return { name: node.name.text, body: node.body, owner: node };
8117
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
8118
+ let init = node.initializer;
8119
+ while (ts.isParenthesizedExpression(init) || ts.isAsExpression(init) || ts.isSatisfiesExpression(init)) init = init.expression;
8120
+ if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { name: node.name.text, body: init.body, owner: node };
8121
+ }
8122
+ return null;
8123
+ }
8124
+ function isExportedDeclaration(node) {
8125
+ const stmt = ts.isVariableDeclaration(node) ? node.parent.parent : node;
8126
+ if (ts.isExportAssignment(stmt)) return true;
8127
+ return ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
8128
+ }
8129
+ function unreachableNodes(sf) {
8130
+ const dead = /* @__PURE__ */ new Set();
8131
+ const markBranches = (node) => {
8132
+ if (ts.isIfStatement(node)) {
8133
+ const t = constTruth(node.expression);
8134
+ if (t === false) dead.add(node.thenStatement);
8135
+ else if (t === true && node.elseStatement) dead.add(node.elseStatement);
8136
+ } else if (ts.isWhileStatement(node) || ts.isForStatement(node)) {
8137
+ const cond = ts.isWhileStatement(node) ? node.expression : node.condition;
8138
+ if (cond && constTruth(cond) === false) dead.add(node.statement);
8139
+ } else if (ts.isConditionalExpression(node)) {
8140
+ const t = constTruth(node.condition);
8141
+ if (t === false) dead.add(node.whenTrue);
8142
+ else if (t === true) dead.add(node.whenFalse);
8143
+ } else if (ts.isBinaryExpression(node)) {
8144
+ const op = node.operatorToken.kind;
8145
+ const l = constTruth(node.left);
8146
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken && l === false || op === ts.SyntaxKind.BarBarToken && l === true) {
8147
+ dead.add(node.right);
8148
+ }
8149
+ }
8150
+ if (ts.isSourceFile(node) || ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
8151
+ let ended = false;
8152
+ for (const stmt of node.statements) {
8153
+ if (ended && !ts.isFunctionDeclaration(stmt)) dead.add(stmt);
8154
+ if (isTerminal(stmt)) ended = true;
8155
+ }
8156
+ }
8157
+ ts.forEachChild(node, markBranches);
8158
+ };
8159
+ markBranches(sf);
8160
+ const candidates = [];
8161
+ const collect2 = (node) => {
8162
+ const fn = namedFunctionBody(node);
8163
+ if (fn && !isExportedDeclaration(fn.owner)) candidates.push(fn);
8164
+ ts.forEachChild(node, collect2);
8165
+ };
8166
+ collect2(sf);
8167
+ if (candidates.length === 0) return dead;
8168
+ const inDead = (node) => {
8169
+ for (let n = node; n; n = n.parent) if (dead.has(n)) return true;
8170
+ return false;
8171
+ };
8172
+ const within2 = (node, owner) => node.pos >= owner.pos && node.end <= owner.end;
8173
+ for (let round = 0; round < MAX_ROUNDS; round++) {
8174
+ const live = /* @__PURE__ */ new Map();
8175
+ const gather = (node) => {
8176
+ if (dead.has(node)) return;
8177
+ if (ts.isIdentifier(node)) {
8178
+ const list3 = live.get(node.text);
8179
+ if (list3) list3.push(node);
8180
+ else live.set(node.text, [node]);
8181
+ }
8182
+ ts.forEachChild(node, gather);
8183
+ };
8184
+ gather(sf);
8185
+ let changed = false;
8186
+ for (const fn of candidates) {
8187
+ if (dead.has(fn.body) || inDead(fn.owner)) continue;
8188
+ const refs = (live.get(fn.name) ?? []).filter((id) => !within2(id, fn.owner));
8189
+ if (refs.length === 0) {
8190
+ dead.add(fn.body);
8191
+ changed = true;
8192
+ }
8193
+ }
8194
+ if (!changed) break;
8195
+ }
8196
+ return dead;
8197
+ }
8198
+ function literalInteriors(sf) {
8199
+ const out3 = [];
8200
+ const visit = (node) => {
8201
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
8202
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8203
+ } else if (ts.isTemplateHead(node) || ts.isTemplateMiddle(node)) {
8204
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 2)]);
8205
+ } else if (ts.isTemplateTail(node)) {
8206
+ out3.push([node.getStart(sf) + 1, Math.max(node.getStart(sf) + 1, node.getEnd() - 1)]);
8207
+ }
8208
+ ts.forEachChild(node, visit);
8209
+ };
8210
+ visit(sf);
8211
+ return out3;
8212
+ }
8213
+ function partitionJs(src) {
8214
+ const sf = parseSource("spec.ts", src);
8215
+ if (!sf) return null;
8216
+ const dead = unreachableNodes(sf);
8217
+ const deadRanges = [];
8218
+ for (const n of dead) deadRanges.push([n.getStart(sf), n.getEnd()]);
8219
+ const literals3 = literalInteriors(sf);
8220
+ const mask = new Uint8Array(src.length);
8221
+ for (const [s, e] of deadRanges) for (let i = s; i < e; i++) mask[i] = 1;
8222
+ for (const [s, e] of literals3) for (let i = s; i < e; i++) if (mask[i] === 0) mask[i] = 2;
8223
+ const live = src.split("");
8224
+ const hidden = src.split("");
8225
+ const inLiteral = new Uint8Array(src.length);
8226
+ for (let i = 0; i < src.length; i++) {
8227
+ if (mask[i] === 1) {
8228
+ if (live[i] !== "\n") live[i] = " ";
8229
+ } else if (mask[i] === 2) {
8230
+ inLiteral[i] = 1;
8231
+ } else if (hidden[i] !== "\n") hidden[i] = " ";
8232
+ }
8233
+ let lineStart = 0;
8234
+ for (let i = 0; i <= src.length; i++) {
8235
+ if (i === src.length || src[i] === "\n") {
8236
+ let all = i > lineStart;
8237
+ for (let j = lineStart; j < i && all; j++) if (mask[j] !== 2 && /\S/.test(src[j])) all = false;
8238
+ if (all) blankRange(live, lineStart, i);
8239
+ lineStart = i + 1;
8240
+ }
8241
+ }
8242
+ return { live: live.join(""), hidden: hidden.join(""), inLiteral };
8243
+ }
8244
+ function partitionPy(src) {
8245
+ const lines = src.split("\n");
8246
+ const dead = new Array(lines.length).fill(false);
8247
+ let killIndent = null;
8248
+ let suiteIndent = null;
8249
+ for (let i = 0; i < lines.length; i++) {
8250
+ const l = lines[i];
8251
+ if (!/\S/.test(l) || /^\s*#/.test(l)) continue;
8252
+ const indent = PY_INDENT(l);
8253
+ if (killIndent !== null && indent < killIndent) killIndent = null;
8254
+ if (suiteIndent !== null && indent <= suiteIndent) suiteIndent = null;
8255
+ if (killIndent !== null && indent >= killIndent || suiteIndent !== null && indent > suiteIndent) {
8256
+ dead[i] = true;
8257
+ continue;
8258
+ }
8259
+ if (PY_TERMINAL.test(l)) killIndent = indent;
8260
+ else if (PY_CONST_FALSE.test(l)) suiteIndent = indent;
8261
+ }
8262
+ const live = lines.map((l, i) => dead[i] ? "" : l).join("\n");
8263
+ const hidden = lines.map((l, i) => dead[i] ? l : "").join("\n");
8264
+ return { live, hidden, inLiteral: new Uint8Array(live.length) };
8265
+ }
8266
+ function partition(src, path) {
8267
+ const lang = langOf(path);
8268
+ if (lang === "js" || lang === null) {
8269
+ const p = partitionJs(src);
8270
+ if (p) return p;
8271
+ } else if (lang === "py") {
8272
+ return partitionPy(src);
8273
+ }
8274
+ return { live: src, hidden: "", inLiteral: new Uint8Array(src.length) };
8275
+ }
8276
+ function pytestCollectedDefs(src) {
8277
+ let n = 0;
8278
+ const stack = [];
8279
+ for (const line of src.split("\n")) {
8280
+ if (!/\S/.test(line) || /^\s*#/.test(line)) continue;
8281
+ const indent = PY_INDENT(line);
8282
+ while (stack.length && indent <= stack[stack.length - 1].indent) stack.pop();
8283
+ const cls = PY_CLASS.exec(line);
8284
+ if (cls) {
8285
+ stack.push({ indent, collected: PY_COLLECTED_CLASS(cls[1], cls[2] ?? "") });
8286
+ continue;
8287
+ }
8288
+ if (PY_TEST_DEF.test(line) && stack.every((c) => c.collected)) n++;
8289
+ }
8290
+ return n;
8291
+ }
8292
+ var MAX_ROUNDS, blankRange, PY_INDENT, PY_TERMINAL, PY_CONST_FALSE, PY_CLASS, PY_TEST_DEF, PY_COLLECTED_CLASS;
8293
+ var init_reachability = __esm({
8294
+ "src/detectors/reachability.ts"() {
8295
+ "use strict";
8296
+ init_ts_lazy();
8297
+ init_files();
8298
+ MAX_ROUNDS = 8;
8299
+ blankRange = (chars, start, end) => {
8300
+ for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n") chars[i] = " ";
8301
+ };
8302
+ PY_INDENT = (line) => line.length - line.trimStart().length;
8303
+ PY_TERMINAL = /^\s*(?:return\b|raise\b)/;
8304
+ PY_CONST_FALSE = /^\s*(?:if|while)\s+(?:False|0|None|not\s+True)\s*:/;
8305
+ PY_CLASS = /^\s*class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:/;
8306
+ PY_TEST_DEF = /^\s*(?:async\s+)?def\s+test\w*\s*\(/;
8307
+ PY_COLLECTED_CLASS = (name, bases) => /^Test/.test(name) || /Test/.test(bases);
8308
+ }
8309
+ });
8310
+
7875
8311
  // src/detectors/test-deletion.ts
7876
8312
  function calleeName(expr) {
7877
8313
  if (ts.isIdentifier(expr)) return expr.text;
7878
8314
  if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) {
7879
8315
  return expr.expression.text;
7880
8316
  }
8317
+ const chain = readCallee(expr);
8318
+ if (chain && !chain.table) return chain.runner;
7881
8319
  return null;
7882
8320
  }
7883
8321
  function loopRows(iterable) {
@@ -7919,6 +8357,7 @@ function eachRows(call) {
7919
8357
  }
7920
8358
  function countTests(src, path = "spec.ts", substantiveOnly = false) {
7921
8359
  const lang = langOf(path);
8360
+ if (lang === "py") return { min: pytestCollectedDefs(src), open: false };
7922
8361
  if (lang && lang !== "js") {
7923
8362
  const re = TEST_DEFS[lang];
7924
8363
  let n2 = 0;
@@ -7929,7 +8368,9 @@ function countTests(src, path = "spec.ts", substantiveOnly = false) {
7929
8368
  if (!sf) return { min: 0, open: true };
7930
8369
  let n = 0;
7931
8370
  let open = false;
8371
+ const dead = unreachableNodes(sf);
7932
8372
  const visit = (node, mult) => {
8373
+ if (dead.has(node)) return;
7933
8374
  if (ts.isForOfStatement(node)) {
7934
8375
  const r = loopRows(node.expression);
7935
8376
  if (r.open) open = true;
@@ -8098,7 +8539,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
8098
8539
  })
8099
8540
  );
8100
8541
  }
8101
- var RULE9, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
8542
+ var RULE9, JVM_TEST, TEST_DEFS, isTableOf, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
8102
8543
  var init_test_deletion = __esm({
8103
8544
  "src/detectors/test-deletion.ts"() {
8104
8545
  "use strict";
@@ -8111,6 +8552,8 @@ var init_test_deletion = __esm({
8111
8552
  init_suite_config();
8112
8553
  init_invocation();
8113
8554
  init_narrow();
8555
+ init_reachability();
8556
+ init_runner_chain();
8114
8557
  RULE9 = "test-deletion";
8115
8558
  JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
8116
8559
  TEST_DEFS = {
@@ -8123,9 +8566,12 @@ var init_test_deletion = __esm({
8123
8566
  php: /^\s*(?:(?:public\s+)?function\s+test\w*\s*\(|#\[Test\]|\*\s*@test\b)/,
8124
8567
  cs: /^\s*\[(?:Fact|Theory|Test|TestMethod|TestCase|DataTestMethod)\b/
8125
8568
  };
8126
- TABLE_METHOD = /^(?:each|for)$/;
8127
- isEachOf = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && (expr.expression.text === "it" || expr.expression.text === "test") && TABLE_METHOD.test(expr.name.text);
8128
- isDescribeEach = (expr) => ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "describe" && TABLE_METHOD.test(expr.name.text);
8569
+ isTableOf = (expr, runners) => {
8570
+ const chain = readCallee(expr);
8571
+ return chain != null && chain.table != null && runners.includes(chain.runner);
8572
+ };
8573
+ isEachOf = (expr) => isTableOf(expr, ["it", "test"]);
8574
+ isDescribeEach = (expr) => isTableOf(expr, ["describe"]);
8129
8575
  OTHER_RUNNER_DIR = /(?:^|\/)(?:e2e|cypress|playwright|fixtures|__fixtures__|__mocks__)\//;
8130
8576
  WHOLE_SUITE_SCRIPT = /^(?:test|test:ci|check|pretest)$/;
8131
8577
  SLICE_SCRIPT = /^test:(?:unit|integration)$/;
@@ -8373,7 +8819,10 @@ function significantLinesOrdered(src, path) {
8373
8819
  }
8374
8820
  return out3;
8375
8821
  }
8376
- var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
8822
+ function liveSignificantLines(src, path) {
8823
+ return significantLinesOrdered(partition(src, path).live, path);
8824
+ }
8825
+ var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, KeptPool, testContentRemoval;
8377
8826
  var init_test_content_removal = __esm({
8378
8827
  "src/detectors/test-content-removal.ts"() {
8379
8828
  "use strict";
@@ -8382,22 +8831,58 @@ var init_test_content_removal = __esm({
8382
8831
  init_finding();
8383
8832
  init_test_deletion();
8384
8833
  init_files();
8834
+ init_reachability();
8385
8835
  RULE10 = "test-content-removal";
8386
8836
  MIN_REMOVED_LINES = 3;
8387
8837
  MIN_REMOVED_ROWS = 2;
8388
8838
  ws = (s) => s.replace(/\s+/g, "");
8389
8839
  TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
8840
+ KeptPool = class {
8841
+ text = "";
8842
+ inLiteral = [];
8843
+ addLive(src, path) {
8844
+ const p = partition(src, path);
8845
+ const lang = langOf(path);
8846
+ let lineStart = 0;
8847
+ for (let i = 0; i <= p.live.length; i++) {
8848
+ if (i < p.live.length && p.live[i] !== "\n") continue;
8849
+ const line = p.live.slice(lineStart, i);
8850
+ if (isSignificantLine(line.trim(), lang)) {
8851
+ for (let j = lineStart; j < i; j++) {
8852
+ if (/\s/.test(p.live[j])) continue;
8853
+ this.text += p.live[j];
8854
+ this.inLiteral.push(p.inLiteral[j]);
8855
+ }
8856
+ }
8857
+ lineStart = i + 1;
8858
+ }
8859
+ }
8860
+ /** Text added outside any spec (a case table moved to `test/fixtures/`): code. */
8861
+ addCode(ws2) {
8862
+ this.text += ws2;
8863
+ for (let i = 0; i < ws2.length; i++) this.inLiteral.push(0);
8864
+ }
8865
+ keeps(wsLine) {
8866
+ if (wsLine.length === 0) return true;
8867
+ let at = this.text.indexOf(wsLine);
8868
+ while (at !== -1) {
8869
+ if (this.inLiteral[at] === 0) return true;
8870
+ at = this.text.indexOf(wsLine, at + 1);
8871
+ }
8872
+ return false;
8873
+ }
8874
+ };
8390
8875
  testContentRemoval = {
8391
8876
  id: RULE10,
8392
8877
  surface: ["file"],
8393
8878
  certainty: "mechanical",
8394
8879
  run(changes, policy) {
8395
8880
  const out3 = [];
8396
- let keptPool = "";
8881
+ const keptPool = new KeptPool();
8397
8882
  let addedPool = "";
8398
8883
  for (const c of changes) {
8399
8884
  if (c.kind !== "file" || c.after == null) continue;
8400
- if (isProtected(c.path, policy, "tests")) keptPool += ws(significantLinesOrdered(c.after, c.path).join("\n"));
8885
+ if (isProtected(c.path, policy, "tests")) keptPool.addLive(c.after, c.path);
8401
8886
  let addedHere = "";
8402
8887
  if (c.op === "add") addedHere = ws(c.after);
8403
8888
  else if (c.before != null) {
@@ -8405,7 +8890,7 @@ var init_test_content_removal = __esm({
8405
8890
  addedHere = ws(c.after.split("\n").filter((l) => !had.has(l.trim())).join("\n"));
8406
8891
  }
8407
8892
  addedPool += addedHere;
8408
- if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool += addedHere;
8893
+ if (!isProtected(c.path, policy, "tests") && TEST_DIR_FILE.test(c.path) && langOf(c.path) === "js") keptPool.addCode(addedHere);
8409
8894
  }
8410
8895
  for (const c of changes) {
8411
8896
  if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
@@ -8414,16 +8899,18 @@ var init_test_content_removal = __esm({
8414
8899
  const blocksBefore = countTests(c.before, c.path);
8415
8900
  const blocksAfter = countTests(c.after, c.path);
8416
8901
  if (blocksAfter.min < blocksBefore.min && !blocksAfter.open) continue;
8417
- const beforeSig = significantLinesOrdered(c.before, c.path);
8418
- const afterSig = significantLinesOrdered(c.after, c.path);
8902
+ const beforeSig = liveSignificantLines(c.before, c.path);
8903
+ const afterSig = liveSignificantLines(c.after, c.path);
8419
8904
  const netDrop = beforeSig.length - afterSig.length;
8420
8905
  const afterSet = new Set(afterSig);
8421
8906
  const gone = [];
8422
8907
  for (const line of new Set(beforeSig)) {
8423
8908
  if (afterSet.has(line)) continue;
8424
- if (keptPool.includes(ws(line))) continue;
8909
+ if (keptPool.keeps(ws(line))) continue;
8425
8910
  gone.push(line);
8426
8911
  }
8912
+ const hiddenPool = ws(partition(c.after, c.path).hidden);
8913
+ const hidden = gone.filter((line) => hiddenPool.includes(ws(line)));
8427
8914
  const table = eachTables(c.before, c.path);
8428
8915
  const tableAfter = eachTables(c.after, c.path);
8429
8916
  const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
@@ -8438,6 +8925,17 @@ var init_test_content_removal = __esm({
8438
8925
  );
8439
8926
  continue;
8440
8927
  }
8928
+ if (hidden.length >= MIN_REMOVED_LINES) {
8929
+ out3.push(
8930
+ makeFinding(RULE10, policy, {
8931
+ file: c.path,
8932
+ message: `Test content made unreachable in a surviving spec: ${hidden.length} significant line(s) kept only where they cannot run (a string, a template literal, a dead branch, after a return).`,
8933
+ evidence: hidden.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
8934
+ remediation: "Fix the code under test. Assertions parked in a string or behind a constant-false branch or an early return no longer execute; the spec is weakened exactly as if they were deleted."
8935
+ })
8936
+ );
8937
+ continue;
8938
+ }
8441
8939
  if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
8442
8940
  out3.push(
8443
8941
  makeFinding(RULE10, policy, {
@@ -8635,7 +9133,11 @@ function expectAssertion(node, sf) {
8635
9133
  };
8636
9134
  }
8637
9135
  function simpleCallName(node) {
8638
- return ts.isIdentifier(node.expression) ? node.expression.text : null;
9136
+ if (ts.isIdentifier(node.expression)) return node.expression.text;
9137
+ const chain = readCallee(node.expression) ?? readTableCallee(node.expression);
9138
+ if (!chain) return null;
9139
+ if (chain.table && !ts.isCallExpression(node.expression) && !ts.isTaggedTemplateExpression(node.expression)) return null;
9140
+ return chain.runner;
8639
9141
  }
8640
9142
  function callbackFor(node) {
8641
9143
  const cb = node.arguments[1];
@@ -8755,6 +9257,7 @@ var init_assertion_weakening = __esm({
8755
9257
  init_policy();
8756
9258
  init_finding();
8757
9259
  init_files();
9260
+ init_runner_chain();
8758
9261
  RULE13 = "assertion-weakening";
8759
9262
  TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
8760
9263
  SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.24.2",
3
+ "version": "2.24.4",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",