llm-relay 0.54.0 → 0.55.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/cli.js +23 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/delegate-gate/cast-necessity.d.ts +14 -0
  4. package/dist/delegate-gate/cast-necessity.js +150 -0
  5. package/dist/delegate-gate/cast-necessity.js.map +1 -0
  6. package/dist/delegate-gate/cli.d.ts +20 -0
  7. package/dist/delegate-gate/cli.js +51 -0
  8. package/dist/delegate-gate/cli.js.map +1 -0
  9. package/dist/delegate-gate/diff-parser.d.ts +59 -0
  10. package/dist/delegate-gate/diff-parser.js +147 -0
  11. package/dist/delegate-gate/diff-parser.js.map +1 -0
  12. package/dist/delegate-gate/fix.d.ts +16 -0
  13. package/dist/delegate-gate/fix.js +43 -0
  14. package/dist/delegate-gate/fix.js.map +1 -0
  15. package/dist/delegate-gate/gate.d.ts +18 -0
  16. package/dist/delegate-gate/gate.js +42 -0
  17. package/dist/delegate-gate/gate.js.map +1 -0
  18. package/dist/delegate-gate/minimality.d.ts +30 -0
  19. package/dist/delegate-gate/minimality.js +120 -0
  20. package/dist/delegate-gate/minimality.js.map +1 -0
  21. package/dist/delegate-gate/post-image.d.ts +10 -0
  22. package/dist/delegate-gate/post-image.js +45 -0
  23. package/dist/delegate-gate/post-image.js.map +1 -0
  24. package/dist/delegate-gate/shared-state.d.ts +15 -0
  25. package/dist/delegate-gate/shared-state.js +158 -0
  26. package/dist/delegate-gate/shared-state.js.map +1 -0
  27. package/dist/delegate-gate/test-assertions.d.ts +18 -0
  28. package/dist/delegate-gate/test-assertions.js +273 -0
  29. package/dist/delegate-gate/test-assertions.js.map +1 -0
  30. package/dist/delegate-gate/types.d.ts +26 -0
  31. package/dist/delegate-gate/types.js +19 -0
  32. package/dist/delegate-gate/types.js.map +1 -0
  33. package/dist/refusal-interpretation.d.ts +3 -0
  34. package/dist/refusal-interpretation.js +7 -1
  35. package/dist/refusal-interpretation.js.map +1 -1
  36. package/package.json +3 -3
@@ -0,0 +1,120 @@
1
+ import { reportedPath } from "./diff-parser.js";
2
+ /** Two lines carry the same code but differ only in leading whitespace. */
3
+ function isWhitespaceOnlyChange(removed, added) {
4
+ return removed !== added && removed.trimStart() === added.trimStart();
5
+ }
6
+ /** Two lines are byte-identical once surrounding whitespace is trimmed — a change that reads as
7
+ * "different bytes, same code" (re-indentation, a trailing-space touch, a blank line shuffled). */
8
+ function hasNoSemanticDelta(removed, added) {
9
+ return removed !== added && removed.trim() === added.trim();
10
+ }
11
+ /**
12
+ * Pair a hunk's removed lines against its added lines by position.
13
+ *
14
+ * This is deliberately naive — position pairing, not a diff-of-diffs — because a delegate-diff
15
+ * gate is reading output a lane already produced, not re-deriving the edit. It is precise for the
16
+ * common single-block replacement shape (a contiguous run of `-` lines immediately followed by a
17
+ * contiguous run of `+` lines, equal in count) and says nothing when counts differ rather than
18
+ * guessing a pairing.
19
+ */
20
+ function pairedRemoveAddRuns(hunk) {
21
+ const pairs = [];
22
+ const lines = hunk.lines;
23
+ let i = 0;
24
+ while (i < lines.length) {
25
+ if (lines[i].kind !== "remove") {
26
+ i += 1;
27
+ continue;
28
+ }
29
+ const removeStart = i;
30
+ while (i < lines.length && lines[i].kind === "remove")
31
+ i += 1;
32
+ const removeRun = lines.slice(removeStart, i);
33
+ const addStart = i;
34
+ while (i < lines.length && lines[i].kind === "add")
35
+ i += 1;
36
+ const addRun = lines.slice(addStart, i);
37
+ if (removeRun.length === addRun.length) {
38
+ for (let k = 0; k < removeRun.length; k++) {
39
+ const removed = removeRun[k];
40
+ const added = addRun[k];
41
+ pairs.push({ removed: removed.content, added: added.content, newLine: added.newLine });
42
+ }
43
+ }
44
+ }
45
+ return pairs;
46
+ }
47
+ const NON_MINIMAL_MIN_CHANGED_LINES = 6;
48
+ const NON_MINIMAL_NO_DELTA_RATIO = 0.5;
49
+ function totalOfKind(hunk, kind) {
50
+ return hunk.lines.reduce((count, line) => (line.kind === kind ? count + 1 : count), 0);
51
+ }
52
+ /**
53
+ * Is this hunk PURE indentation churn: every removed and every added line accounted for by a
54
+ * whitespace-only pair, with nothing left unpaired?
55
+ *
56
+ * The "fully paired" requirement (every remove and every add line in the hunk is consumed by a
57
+ * 1:1 pair) is what makes the auto-fix in `fix.ts` safe: dropping a fully-paired churn hunk
58
+ * removes an equal number of old-side and new-side lines, so it can never shift another hunk's
59
+ * line numbers. A hunk containing an unequal-length remove/add run alongside pure churn is NOT
60
+ * flagged here — `pairedRemoveAddRuns` silently skips unequal runs, and treating the REST of the
61
+ * hunk as "100% churn" while ignoring lines it could not pair would both misreport the hunk and
62
+ * make an auto-fix unsafe.
63
+ */
64
+ export function isPureIndentationChurnHunk(hunk) {
65
+ const pairs = pairedRemoveAddRuns(hunk);
66
+ if (pairs.length === 0)
67
+ return false;
68
+ if (pairs.length !== totalOfKind(hunk, "remove") || pairs.length !== totalOfKind(hunk, "add"))
69
+ return false;
70
+ return pairs.every((p) => isWhitespaceOnlyChange(p.removed, p.added));
71
+ }
72
+ /**
73
+ * Flag indentation-only churn and count changed lines with no semantic delta.
74
+ *
75
+ * Two finding shapes:
76
+ * - `indentation-churn`: a hunk whose every line is accounted for by a whitespace-only
77
+ * remove/add pair — the hunk changed formatting and nothing else. Auto-fixable: dropping the
78
+ * whole hunk is safe (see `isPureIndentationChurnHunk`).
79
+ * - `non-minimal-diff`: a hunk that is NOT pure churn but where at least half its PAIRED changed
80
+ * lines (≥6 of them) carry no semantic delta — evidence of a diff that reformats lines it did
81
+ * not need to touch alongside a real change, i.e. class (e), "non-minimal diffs touching
82
+ * unrelated lines". Not auto-fixable: a partial hunk mixes a real edit with the noise, and
83
+ * removing only the noise without re-deriving the hunk boundaries risks corrupting the real
84
+ * change.
85
+ */
86
+ export function analyzeDiffMinimality(files) {
87
+ const findings = [];
88
+ for (const file of files) {
89
+ const path = reportedPath(file);
90
+ for (const hunk of file.hunks) {
91
+ if (isPureIndentationChurnHunk(hunk)) {
92
+ const pairs = pairedRemoveAddRuns(hunk);
93
+ findings.push({
94
+ class: "indentation-churn",
95
+ file: path,
96
+ line: pairs[0].newLine,
97
+ detail: `hunk @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ changes ${pairs.length} line(s) by leading whitespace only`,
98
+ autoFixable: true,
99
+ });
100
+ continue;
101
+ }
102
+ const pairs = pairedRemoveAddRuns(hunk);
103
+ if (pairs.length === 0)
104
+ continue;
105
+ const noDeltaPairs = pairs.filter((p) => hasNoSemanticDelta(p.removed, p.added));
106
+ if (pairs.length >= NON_MINIMAL_MIN_CHANGED_LINES
107
+ && noDeltaPairs.length / pairs.length >= NON_MINIMAL_NO_DELTA_RATIO) {
108
+ findings.push({
109
+ class: "non-minimal-diff",
110
+ file: path,
111
+ line: noDeltaPairs[0].newLine,
112
+ detail: `hunk @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ has no semantic delta on ${noDeltaPairs.length}/${pairs.length} changed lines — likely touches lines unrelated to the actual edit`,
113
+ autoFixable: false,
114
+ });
115
+ }
116
+ }
117
+ }
118
+ return findings;
119
+ }
120
+ //# sourceMappingURL=minimality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"minimality.js","sourceRoot":"","sources":["../../src/delegate-gate/minimality.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { DiffFile } from "./diff-parser.js";
2
+ export interface PostImage {
3
+ /** The reconstructed post-patch file content, one line per array entry (no trailing newline
4
+ * entries). */
5
+ readonly lines: readonly string[];
6
+ /** 1-based post-image line numbers the diff actually ADDED — the only lines an AST detector
7
+ * may report a finding against ("introduced by the diff"). */
8
+ readonly addedLines: ReadonlySet<number>;
9
+ }
10
+ export declare function reconstructPostImage(original: string | null, file: DiffFile): PostImage;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Reconstruct a file's post-patch content from its pre-image plus the diff's hunks.
3
+ *
4
+ * `original` is the pre-image content (read from `--repo`), or `null` for a newly created file —
5
+ * a `null` pre-image is only valid when every hunk's removed-line count is zero, which is what a
6
+ * `diff --git` "new file" section always produces.
7
+ *
8
+ * Lines outside any hunk are copied through unchanged from the pre-image; a hunk's `context`
9
+ * lines are trusted as given (this is a diff CONSUMER, not a patch verifier — a hunk that lies
10
+ * about context is the delegate's problem, not this gate's to detect).
11
+ */
12
+ function applyHunkLines(hunk, out, added, startOldCursor) {
13
+ let oldCursor = startOldCursor;
14
+ for (const line of hunk.lines) {
15
+ if (line.kind === "remove") {
16
+ oldCursor += 1;
17
+ continue;
18
+ }
19
+ out.push(line.content);
20
+ if (line.kind === "add")
21
+ added.add(out.length);
22
+ if (line.kind === "context")
23
+ oldCursor += 1;
24
+ }
25
+ return oldCursor;
26
+ }
27
+ export function reconstructPostImage(original, file) {
28
+ const originalLines = original === null ? [] : original.replace(/\r\n/g, "\n").split("\n");
29
+ const out = [];
30
+ const added = new Set();
31
+ let oldCursor = 0; // 0-based index into originalLines, next unconsumed line
32
+ for (const hunk of file.hunks) {
33
+ const hunkStartIndex = hunk.oldStart === 0 ? 0 : hunk.oldStart - 1;
34
+ // Copy the untouched span between the previous hunk (or file start) and this one.
35
+ for (; oldCursor < hunkStartIndex; oldCursor++) {
36
+ out.push(originalLines[oldCursor] ?? "");
37
+ }
38
+ oldCursor = applyHunkLines(hunk, out, added, oldCursor);
39
+ }
40
+ for (; oldCursor < originalLines.length; oldCursor++) {
41
+ out.push(originalLines[oldCursor] ?? "");
42
+ }
43
+ return { lines: out, addedLines: added };
44
+ }
45
+ //# sourceMappingURL=post-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-image.js","sourceRoot":"","sources":["../../src/delegate-gate/post-image.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,IAA+B,EAAE,GAAa,EAAE,KAAkB,EAAE,cAAsB;IAChH,IAAI,SAAS,GAAG,cAAc,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,SAAS,IAAI,CAAC,CAAC;YACf,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS,IAAI,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAuB,EAAE,IAAc;IAC1E,MAAM,aAAa,GAAG,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3F,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAEhC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,yDAAyD;IAC5E,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QACnE,kFAAkF;QAClF,OAAO,SAAS,GAAG,cAAc,EAAE,SAAS,EAAE,EAAE,CAAC;YAC/C,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,SAAS,GAAG,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;QACrD,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { DiffFile } from "./diff-parser.js";
2
+ import type { Finding } from "./types.js";
3
+ /**
4
+ * Flag statements the diff ADDS that assign into module-level (top-level, exported, or
5
+ * object-shaped) state from inside a function body.
6
+ *
7
+ * A tracked binding is either exported, or a top-level `const`/`let`/`var` initialized to an
8
+ * object/array literal or a `new X(...)` instance — the shapes a mutation actually changes for
9
+ * every future reader of the module, as opposed to a local primitive that happens to sit at
10
+ * module scope. Both a direct assignment (`state.x = …`, compound operators included) and a call
11
+ * to a known mutating method (`push`, `set`, `delete`, …) on a property/element-access chain
12
+ * rooted at that binding count. Only sites on lines the diff ADDED are reported — a mutation the
13
+ * delegate did not touch is not this diff's defect.
14
+ */
15
+ export declare function analyzeSharedStateMutation(files: readonly DiffFile[], readOriginal: (path: string) => string | null): Finding[];
@@ -0,0 +1,158 @@
1
+ import ts from "typescript";
2
+ import { reportedPath } from "./diff-parser.js";
3
+ import { reconstructPostImage } from "./post-image.js";
4
+ const TS_FILE = /\.(ts|tsx|mts|cts)$/;
5
+ /** Top-level bindings whose value shape makes an in-place mutation meaningful: object/array
6
+ * literals and `new X(...)` instances (Map, Set, a class instance holding its own state) — plus
7
+ * every EXPORTED top-level binding regardless of shape, since exporting it makes it part of the
8
+ * module's shared surface by declaration. */
9
+ function collectModuleScopeBindings(sourceFile) {
10
+ const names = new Set();
11
+ for (const statement of sourceFile.statements) {
12
+ if (!ts.isVariableStatement(statement))
13
+ continue;
14
+ const isExported = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
15
+ for (const decl of statement.declarationList.declarations) {
16
+ if (!ts.isIdentifier(decl.name))
17
+ continue;
18
+ const initializer = decl.initializer;
19
+ const isStateShaped = initializer !== undefined
20
+ && (ts.isObjectLiteralExpression(initializer)
21
+ || ts.isArrayLiteralExpression(initializer)
22
+ || ts.isNewExpression(initializer));
23
+ if (isExported || isStateShaped)
24
+ names.add(decl.name.text);
25
+ }
26
+ }
27
+ return names;
28
+ }
29
+ /** Walk up from an expression to the root identifier of a member/index-access chain:
30
+ * `state.rows[0].value` -> `state`. Returns null for anything else (a call result, `this`, …). */
31
+ function rootIdentifier(node) {
32
+ let current = node;
33
+ while (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
34
+ current = current.expression;
35
+ }
36
+ return ts.isIdentifier(current) ? current.text : null;
37
+ }
38
+ const MUTATING_METHODS = new Set([
39
+ "push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin",
40
+ "set", "delete", "clear", "add",
41
+ ]);
42
+ function isAssignmentTarget(node) {
43
+ if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
44
+ return node.left;
45
+ }
46
+ return null;
47
+ }
48
+ function isAssignmentOperator(kind) {
49
+ switch (kind) {
50
+ case ts.SyntaxKind.EqualsToken:
51
+ case ts.SyntaxKind.PlusEqualsToken:
52
+ case ts.SyntaxKind.MinusEqualsToken:
53
+ case ts.SyntaxKind.AsteriskEqualsToken:
54
+ case ts.SyntaxKind.SlashEqualsToken:
55
+ case ts.SyntaxKind.AmpersandEqualsToken:
56
+ case ts.SyntaxKind.BarEqualsToken:
57
+ case ts.SyntaxKind.CaretEqualsToken:
58
+ case ts.SyntaxKind.QuestionQuestionEqualsToken:
59
+ return true;
60
+ default:
61
+ return false;
62
+ }
63
+ }
64
+ /** `state.rows.push(x)` / `cache.set(k, v)` / `seen.delete(k)` — a call to a known mutating
65
+ * built-in method on a member-access chain rooted at a tracked binding. */
66
+ function mutatingCallTarget(node) {
67
+ if (!ts.isCallExpression(node))
68
+ return null;
69
+ const callee = node.expression;
70
+ if (!ts.isPropertyAccessExpression(callee))
71
+ return null;
72
+ if (!MUTATING_METHODS.has(callee.name.text))
73
+ return null;
74
+ return callee.expression;
75
+ }
76
+ function isFunctionLikeContainer(node) {
77
+ return ts.isFunctionDeclaration(node)
78
+ || ts.isFunctionExpression(node)
79
+ || ts.isArrowFunction(node)
80
+ || ts.isMethodDeclaration(node)
81
+ || ts.isGetAccessor(node)
82
+ || ts.isSetAccessor(node)
83
+ || ts.isConstructorDeclaration(node);
84
+ }
85
+ /** Is `node` lexically inside a function body (as opposed to sitting directly among a source
86
+ * file's top-level statements)? `node` itself is never function-like here (it is an assignment or
87
+ * call expression), so this only ever matches an ANCESTOR. */
88
+ function isInsideFunctionBody(node) {
89
+ return ts.findAncestor(node, isFunctionLikeContainer) !== undefined;
90
+ }
91
+ function findMutationSites(sourceFile) {
92
+ const sites = [];
93
+ const visit = (node) => {
94
+ const assignmentTarget = isAssignmentTarget(node);
95
+ if (assignmentTarget !== null && isInsideFunctionBody(node)) {
96
+ sites.push({ node, target: assignmentTarget });
97
+ }
98
+ const mutatingTarget = mutatingCallTarget(node);
99
+ if (mutatingTarget !== null && isInsideFunctionBody(node)) {
100
+ sites.push({ node, target: mutatingTarget });
101
+ }
102
+ ts.forEachChild(node, visit);
103
+ };
104
+ visit(sourceFile);
105
+ return sites;
106
+ }
107
+ function findingsForFile(file, path, readOriginal) {
108
+ const original = file.isNew ? null : readOriginal(path);
109
+ const { lines, addedLines } = reconstructPostImage(original, file);
110
+ if (addedLines.size === 0)
111
+ return [];
112
+ const content = lines.join("\n");
113
+ const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
114
+ const moduleScope = collectModuleScopeBindings(sourceFile);
115
+ if (moduleScope.size === 0)
116
+ return [];
117
+ const findings = [];
118
+ for (const site of findMutationSites(sourceFile)) {
119
+ const { line } = sourceFile.getLineAndCharacterOfPosition(site.node.getStart(sourceFile));
120
+ const lineNo = line + 1;
121
+ if (!addedLines.has(lineNo))
122
+ continue;
123
+ const root = rootIdentifier(site.target);
124
+ if (root === null || !moduleScope.has(root))
125
+ continue;
126
+ findings.push({
127
+ class: "shared-state-mutation",
128
+ file: path,
129
+ line: lineNo,
130
+ detail: `\`${site.node.getText().slice(0, 120)}\` mutates module-level state \`${root}\` from inside a function`,
131
+ autoFixable: false,
132
+ });
133
+ }
134
+ return findings;
135
+ }
136
+ /**
137
+ * Flag statements the diff ADDS that assign into module-level (top-level, exported, or
138
+ * object-shaped) state from inside a function body.
139
+ *
140
+ * A tracked binding is either exported, or a top-level `const`/`let`/`var` initialized to an
141
+ * object/array literal or a `new X(...)` instance — the shapes a mutation actually changes for
142
+ * every future reader of the module, as opposed to a local primitive that happens to sit at
143
+ * module scope. Both a direct assignment (`state.x = …`, compound operators included) and a call
144
+ * to a known mutating method (`push`, `set`, `delete`, …) on a property/element-access chain
145
+ * rooted at that binding count. Only sites on lines the diff ADDED are reported — a mutation the
146
+ * delegate did not touch is not this diff's defect.
147
+ */
148
+ export function analyzeSharedStateMutation(files, readOriginal) {
149
+ const findings = [];
150
+ for (const file of files) {
151
+ const path = reportedPath(file);
152
+ if (!TS_FILE.test(path) || file.hunks.length === 0)
153
+ continue;
154
+ findings.push(...findingsForFile(file, path, readOriginal));
155
+ }
156
+ return findings;
157
+ }
158
+ //# sourceMappingURL=shared-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared-state.js","sourceRoot":"","sources":["../../src/delegate-gate/shared-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { DiffFile } from "./diff-parser.js";
2
+ import type { Finding } from "./types.js";
3
+ /**
4
+ * Flag assertions added by the diff, in changed TEST files, that cannot fail or that show no
5
+ * traceable connection to the code under test.
6
+ *
7
+ * Sharp (always correct) checks: `expect(<literal>).toBeDefined()`, `expect(true).toBe(true)` /
8
+ * `expect(false).toBe(false)`, `expect(x).toBe(x)` on the identical identifier, and the
9
+ * `toBeTruthy`/`toBeFalsy` equivalents on a constant.
10
+ *
11
+ * Best-effort (may under-report on purpose, per the "prefer false negatives" rule): an assertion
12
+ * whose value is produced by calling a function DECLARED LOCALLY in the same test file rather
13
+ * than one it imported — the "hand-copied replica of production control flow, asserted against
14
+ * the replica" shape from `docs/delegate-gate.md`'s cited real defect. Narrower than "no reference
15
+ * to any import at all" on purpose: that broader form flags ordinary local-variable assertions
16
+ * (`const a = 1; expect(a + 1).toBe(2)`), which is legitimate and common.
17
+ */
18
+ export declare function analyzeTestAssertions(files: readonly DiffFile[], readOriginal: (path: string) => string | null): Finding[];