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,150 @@
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
+ const PRIMITIVE_KEYWORD_TO_LITERAL_KIND = new Map([
6
+ [ts.SyntaxKind.StringKeyword, [ts.SyntaxKind.StringLiteral, ts.SyntaxKind.NoSubstitutionTemplateLiteral]],
7
+ [ts.SyntaxKind.NumberKeyword, [ts.SyntaxKind.NumericLiteral]],
8
+ [ts.SyntaxKind.BooleanKeyword, [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword]],
9
+ ]);
10
+ /** `x as unknown as T` (or any AsExpression whose own expression is itself an AsExpression to
11
+ * `unknown`) — a double cast that exists only to defeat the type checker. */
12
+ function isDoubleUnknownCast(node) {
13
+ const inner = node.expression;
14
+ if (!ts.isAsExpression(inner))
15
+ return false;
16
+ return ts.isTypeReferenceNode(inner.type) === false
17
+ && inner.type.kind === ts.SyntaxKind.UnknownKeyword;
18
+ }
19
+ function isAnyCast(node) {
20
+ return node.type.kind === ts.SyntaxKind.AnyKeyword;
21
+ }
22
+ /** `"x" as string`, `5 as number`, `true as boolean` — a cast of a literal to the primitive type
23
+ * it already trivially has. Deliberately narrow: this is a syntactic check, not a type-checker
24
+ * one, so it only fires on the unambiguous literal-vs-keyword-type case and never guesses about
25
+ * an identifier's inferred type. */
26
+ function isTriviallyRedundantLiteralCast(node) {
27
+ const allowedKinds = PRIMITIVE_KEYWORD_TO_LITERAL_KIND.get(node.type.kind);
28
+ if (allowedKinds === undefined)
29
+ return false;
30
+ return allowedKinds.includes(node.expression.kind);
31
+ }
32
+ function findAsExpressionsOnAddedLines(sourceFile, addedLines) {
33
+ const found = [];
34
+ const visit = (node) => {
35
+ if (ts.isAsExpression(node)) {
36
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
37
+ if (addedLines.has(line + 1))
38
+ found.push(node);
39
+ }
40
+ ts.forEachChild(node, visit);
41
+ };
42
+ visit(sourceFile);
43
+ return found;
44
+ }
45
+ function describeCast(node) {
46
+ if (isAnyCast(node)) {
47
+ return { detail: `\`${node.getText()}\` casts to \`any\`, discarding type safety`, autoFixable: false };
48
+ }
49
+ if (isDoubleUnknownCast(node)) {
50
+ return { detail: `\`${node.getText()}\` is a double cast through \`unknown\` — it exists only to bypass the type checker`, autoFixable: false };
51
+ }
52
+ if (isTriviallyRedundantLiteralCast(node)) {
53
+ return { detail: `\`${node.getText()}\` casts a literal to the primitive type it already has — the cast adds nothing`, autoFixable: true };
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Flag `as` casts introduced by the diff that add nothing.
59
+ *
60
+ * Always flagged: `as any` and `x as unknown as T` double casts. Best-effort beyond that: a
61
+ * literal cast to the primitive type it already trivially has (`"x" as string`). Deciding
62
+ * necessity in the general case needs a full type-checker `Program` over the whole repo — slow,
63
+ * and only as good as whether the diff's target compiles in isolation — so this deliberately stays
64
+ * syntactic rather than risk a false positive on a cast that genuinely narrows or widens a type.
65
+ */
66
+ function findingsForFile(file, path, readOriginal) {
67
+ const original = file.isNew ? null : readOriginal(path);
68
+ const { lines, addedLines } = reconstructPostImage(original, file);
69
+ if (addedLines.size === 0)
70
+ return [];
71
+ const content = lines.join("\n");
72
+ const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
73
+ const findings = [];
74
+ for (const node of findAsExpressionsOnAddedLines(sourceFile, addedLines)) {
75
+ const described = describeCast(node);
76
+ if (described === null)
77
+ continue;
78
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
79
+ findings.push({
80
+ class: "unnecessary-cast",
81
+ file: path,
82
+ line: line + 1,
83
+ detail: described.detail,
84
+ autoFixable: described.autoFixable,
85
+ });
86
+ }
87
+ return findings;
88
+ }
89
+ export function analyzeCastNecessity(files, readOriginal) {
90
+ const findings = [];
91
+ for (const file of files) {
92
+ const path = reportedPath(file);
93
+ if (!TS_FILE.test(path) || file.hunks.length === 0)
94
+ continue;
95
+ findings.push(...findingsForFile(file, path, readOriginal));
96
+ }
97
+ return findings;
98
+ }
99
+ /** The diff's own `add` line at post-image line `newLine`, or undefined if none matches — used to
100
+ * translate an AST node's line number back into the raw diff line the auto-fix edits. */
101
+ function addedDiffLine(file, newLine) {
102
+ for (const hunk of file.hunks) {
103
+ for (const line of hunk.lines) {
104
+ if (line.kind === "add" && line.newLine === newLine)
105
+ return line;
106
+ }
107
+ }
108
+ return undefined;
109
+ }
110
+ function castEditsForFile(file, path, readOriginal) {
111
+ const original = file.isNew ? null : readOriginal(path);
112
+ const { lines, addedLines } = reconstructPostImage(original, file);
113
+ if (addedLines.size === 0)
114
+ return [];
115
+ const content = lines.join("\n");
116
+ const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
117
+ const edits = [];
118
+ for (const node of findAsExpressionsOnAddedLines(sourceFile, addedLines)) {
119
+ if (!isTriviallyRedundantLiteralCast(node))
120
+ continue; // the only auto-fixable cast shape
121
+ const start = node.getStart(sourceFile);
122
+ const { line: lineIndex, character: startCol } = sourceFile.getLineAndCharacterOfPosition(start);
123
+ const endCol = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).character;
124
+ const lineText = lines[lineIndex];
125
+ if (lineText === undefined)
126
+ continue;
127
+ const diffLine = addedDiffLine(file, lineIndex + 1);
128
+ if (diffLine === undefined)
129
+ continue;
130
+ const replacementText = node.expression.getText(sourceFile);
131
+ const newContent = lineText.slice(0, startCol) + replacementText + lineText.slice(endCol);
132
+ edits.push({ rawIndex: diffLine.rawIndex, newContent });
133
+ }
134
+ return edits;
135
+ }
136
+ /** Mechanical corrections for the `unnecessary-cast` findings this module can safely apply on its
137
+ * own: only the trivially-redundant-literal-cast shape (`"x" as string` -> `"x"`). `as any` and
138
+ * the double-unknown cast are never auto-fixed — removing them changes what the surrounding code
139
+ * type-checks as, which is a judgment call for a human, not a mechanical rewrite. */
140
+ export function findFixableCastEdits(files, readOriginal) {
141
+ const edits = [];
142
+ for (const file of files) {
143
+ const path = reportedPath(file);
144
+ if (!TS_FILE.test(path) || file.hunks.length === 0)
145
+ continue;
146
+ edits.push(...castEditsForFile(file, path, readOriginal));
147
+ }
148
+ return edits;
149
+ }
150
+ //# sourceMappingURL=cast-necessity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cast-necessity.js","sourceRoot":"","sources":["../../src/delegate-gate/cast-necessity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface DelegateGateCliOptions {
2
+ readonly diffPath: string | undefined;
3
+ readonly repoRoot: string | undefined;
4
+ readonly fix: boolean;
5
+ }
6
+ export interface DelegateGateCliResult {
7
+ readonly exitCode: 0 | 1;
8
+ readonly stdout: string;
9
+ readonly stderr: string;
10
+ }
11
+ /**
12
+ * `llm-relay delegate-gate <diff-file> --repo <root> [--fix]`
13
+ *
14
+ * Pure over injected IO so the contract tests can drive it without touching the real filesystem.
15
+ * `readFile`/`writeFile` default to Node's `fs`; a caller only overrides them in tests.
16
+ */
17
+ export declare function runDelegateGateCli(options: DelegateGateCliOptions, io?: {
18
+ readonly readFile?: (path: string) => string;
19
+ readonly writeFile?: (path: string, content: string) => void;
20
+ }): DelegateGateCliResult;
@@ -0,0 +1,51 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { evaluateDiffText, repoFileReader } from "./gate.js";
4
+ import { autoRepairDiff } from "./fix.js";
5
+ /**
6
+ * `llm-relay delegate-gate <diff-file> --repo <root> [--fix]`
7
+ *
8
+ * Pure over injected IO so the contract tests can drive it without touching the real filesystem.
9
+ * `readFile`/`writeFile` default to Node's `fs`; a caller only overrides them in tests.
10
+ */
11
+ export function runDelegateGateCli(options, io = {}) {
12
+ const readFile = io.readFile ?? ((path) => readFileSync(path, "utf8"));
13
+ const writeFile = io.writeFile ?? ((path, content) => writeFileSync(path, content, "utf8"));
14
+ if (options.diffPath === undefined) {
15
+ return { exitCode: 1, stdout: "", stderr: "delegate-gate: missing required <diff-file> argument\n" };
16
+ }
17
+ if (options.repoRoot === undefined) {
18
+ return { exitCode: 1, stdout: "", stderr: "delegate-gate: --repo <root> is required\n" };
19
+ }
20
+ let diffText;
21
+ try {
22
+ diffText = readFile(options.diffPath);
23
+ }
24
+ catch (e) {
25
+ return { exitCode: 1, stdout: "", stderr: `delegate-gate: could not read "${options.diffPath}": ${e.message}\n` };
26
+ }
27
+ const repoRoot = resolve(options.repoRoot);
28
+ const readOriginal = repoFileReader(repoRoot);
29
+ let verdict;
30
+ try {
31
+ verdict = evaluateDiffText(diffText, readOriginal);
32
+ }
33
+ catch (e) {
34
+ return { exitCode: 1, stdout: "", stderr: `delegate-gate: could not analyze the diff: ${e.message}\n` };
35
+ }
36
+ let stderr = "";
37
+ if (options.fix) {
38
+ const repaired = autoRepairDiff(diffText, readOriginal);
39
+ if (repaired.fixedCount > 0) {
40
+ const fixedPath = `${options.diffPath}.fixed.patch`;
41
+ writeFile(fixedPath, repaired.fixedText);
42
+ stderr += `delegate-gate: wrote ${repaired.fixedCount} mechanical fix(es) to ${fixedPath}\n`;
43
+ }
44
+ else {
45
+ stderr += "delegate-gate: no mechanically fixable findings; no .fixed.patch written\n";
46
+ }
47
+ }
48
+ const stdout = `${JSON.stringify(verdict, null, 2)}\n`;
49
+ return { exitCode: verdict.pass ? 0 : 1, stdout, stderr };
50
+ }
51
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/delegate-gate/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAe1C;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA+B,EAC/B,KAGI,EAAE;IAEN,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/E,MAAM,SAAS,GAAG,EAAE,CAAC,SAAS,IAAI,CAAC,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAE5G,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,wDAAwD,EAAE,CAAC;IACvG,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,4CAA4C,EAAE,CAAC;IAC3F,CAAC;IAED,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,kCAAkC,OAAO,CAAC,QAAQ,MAAO,CAAW,CAAC,OAAO,IAAI,EAAE,CAAC;IAC/H,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,gBAAgB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,8CAA+C,CAAW,CAAC,OAAO,IAAI,EAAE,CAAC;IACrH,CAAC;IAED,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QACxD,IAAI,QAAQ,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,SAAS,GAAG,GAAG,OAAO,CAAC,QAAQ,cAAc,CAAC;YACpD,SAAS,CAAC,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;YACzC,MAAM,IAAI,wBAAwB,QAAQ,CAAC,UAAU,0BAA0B,SAAS,IAAI,CAAC;QAC/F,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,4EAA4E,CAAC;QACzF,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACvD,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A minimal unified-diff parser (git's `diff --git` dialect) — just enough structure for the
3
+ * detectors in this module: per-file hunks, each line typed context/add/remove and carrying its
4
+ * line number in whichever image it belongs to.
5
+ *
6
+ * Deliberately narrow: this is not a general patch-apply library. It accepts the shape `git diff`
7
+ * / `git format-patch` produce and refuses (throws `DiffParseError`) rather than guess on anything
8
+ * else — the same "refuse, don't mangle" rule `documents.ts` and `openai-request.ts` follow
9
+ * elsewhere in this repo.
10
+ */
11
+ export type DiffLineKind = "context" | "add" | "remove";
12
+ export interface DiffLine {
13
+ readonly kind: DiffLineKind;
14
+ /** Line content, WITHOUT the leading `+`/`-`/` ` marker and without a trailing newline. */
15
+ readonly content: string;
16
+ /** 1-based line number in the pre-image; undefined for an added line. */
17
+ readonly oldLine: number | undefined;
18
+ /** 1-based line number in the post-image; undefined for a removed line. */
19
+ readonly newLine: number | undefined;
20
+ /** Index of this line's raw (marker-included) text in the diff's full line array. The
21
+ * auto-repair pass edits/removes raw lines by this index rather than re-deriving offsets — the
22
+ * ONE place the diff's own text layout is exposed outside this parser. */
23
+ readonly rawIndex: number;
24
+ }
25
+ export interface DiffHunk {
26
+ readonly oldStart: number;
27
+ readonly oldLines: number;
28
+ readonly newStart: number;
29
+ readonly newLines: number;
30
+ readonly lines: readonly DiffLine[];
31
+ /** Raw-line index of this hunk's `@@ ... @@` header. */
32
+ readonly headerRawIndex: number;
33
+ /** Raw-line index one past this hunk's last body line — `[headerRawIndex, endRawIndex)` is the
34
+ * whole hunk, header included, safe to delete as a unit. */
35
+ readonly endRawIndex: number;
36
+ }
37
+ export interface DiffFile {
38
+ /** Pre-image path (`a/...` with the prefix stripped), or null for a newly created file. */
39
+ readonly oldPath: string | null;
40
+ /** Post-image path (`b/...` with the prefix stripped), or null for a deleted file. */
41
+ readonly newPath: string | null;
42
+ readonly isNew: boolean;
43
+ readonly isDeleted: boolean;
44
+ readonly hunks: readonly DiffHunk[];
45
+ }
46
+ export declare class DiffParseError extends Error {
47
+ }
48
+ /**
49
+ * Parse a unified diff into per-file hunks.
50
+ *
51
+ * Accepts multiple `diff --git` sections. Each file section must open with a `diff --git a/X b/Y`
52
+ * line; `---`/`+++`/`@@` lines follow standard git conventions. A file section with no hunks
53
+ * (e.g. a pure rename or mode change) is skipped — there is nothing here for a line-level
54
+ * detector to look at.
55
+ */
56
+ export declare function parseUnifiedDiff(text: string): readonly DiffFile[];
57
+ /** The path a detector should report findings under — the post-image path, falling back to the
58
+ * pre-image path for a deletion (which carries no post-image). */
59
+ export declare function reportedPath(file: DiffFile): string;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A minimal unified-diff parser (git's `diff --git` dialect) — just enough structure for the
3
+ * detectors in this module: per-file hunks, each line typed context/add/remove and carrying its
4
+ * line number in whichever image it belongs to.
5
+ *
6
+ * Deliberately narrow: this is not a general patch-apply library. It accepts the shape `git diff`
7
+ * / `git format-patch` produce and refuses (throws `DiffParseError`) rather than guess on anything
8
+ * else — the same "refuse, don't mangle" rule `documents.ts` and `openai-request.ts` follow
9
+ * elsewhere in this repo.
10
+ */
11
+ export class DiffParseError extends Error {
12
+ }
13
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
14
+ /** Strip a single leading `a/` or `b/` path prefix, the form git always emits. */
15
+ function stripPrefix(path) {
16
+ if (path.startsWith("a/") || path.startsWith("b/"))
17
+ return path.slice(2);
18
+ return path;
19
+ }
20
+ function pathFromHeaderLine(line, marker) {
21
+ const rest = line.slice(marker.length + 1);
22
+ // A trailing tab introduces a timestamp in some diff dialects; git's own output never does,
23
+ // but tolerate it rather than folding it into the path.
24
+ const tabAt = rest.indexOf("\t");
25
+ const raw = (tabAt === -1 ? rest : rest.slice(0, tabAt)).trim();
26
+ if (raw === "/dev/null")
27
+ return null;
28
+ return stripPrefix(raw);
29
+ }
30
+ /** Consume the file-header block (index/mode/rename/similarity lines, then `---`/`+++`) starting
31
+ * at `rawLines[start]`. Returns the parsed header and the index of the first line after it. */
32
+ function parseFileHeader(rawLines, start) {
33
+ let isNew = false;
34
+ let isDeleted = false;
35
+ let oldPath = null;
36
+ let newPath = null;
37
+ let i = start;
38
+ while (i < rawLines.length) {
39
+ const line = rawLines[i];
40
+ if (line.startsWith("diff --git ") || line.startsWith("@@"))
41
+ break;
42
+ if (line.startsWith("new file mode"))
43
+ isNew = true;
44
+ else if (line.startsWith("deleted file mode"))
45
+ isDeleted = true;
46
+ else if (line.startsWith("--- "))
47
+ oldPath = pathFromHeaderLine(line, "---");
48
+ else if (line.startsWith("+++ "))
49
+ newPath = pathFromHeaderLine(line, "+++");
50
+ i += 1;
51
+ if (line.startsWith("+++ "))
52
+ break;
53
+ }
54
+ return { header: { isNew, isDeleted, oldPath, newPath }, next: i };
55
+ }
56
+ /** Classify one hunk body line and advance the pre/post cursors it carries. */
57
+ function classifyHunkBodyLine(body, oldCursor, newCursor, rawIndex) {
58
+ const marker = body.charAt(0);
59
+ if (marker === "+")
60
+ return { kind: "add", content: body.slice(1), oldLine: undefined, newLine: newCursor, rawIndex };
61
+ if (marker === "-")
62
+ return { kind: "remove", content: body.slice(1), oldLine: oldCursor, newLine: undefined, rawIndex };
63
+ // A context line always starts with a single space in a conforming diff; an empty string (a
64
+ // blank context line some tools emit unmarked) is accepted the same way.
65
+ const content = marker === " " ? body.slice(1) : body;
66
+ return { kind: "context", content, oldLine: oldCursor, newLine: newCursor, rawIndex };
67
+ }
68
+ /** Parse one `@@ -l,s +l,s @@` hunk starting at `rawLines[start]`. Returns the hunk and the
69
+ * index of the first line after it. */
70
+ function parseHunk(rawLines, start) {
71
+ const headerLine = rawLines[start];
72
+ const match = HUNK_HEADER.exec(headerLine);
73
+ if (!match)
74
+ throw new DiffParseError(`malformed hunk header: ${headerLine}`);
75
+ const oldStart = Number(match[1]);
76
+ const oldLines = match[2] === undefined ? 1 : Number(match[2]);
77
+ const newStart = Number(match[3]);
78
+ const newLines = match[4] === undefined ? 1 : Number(match[4]);
79
+ const lines = [];
80
+ let oldCursor = oldStart;
81
+ let newCursor = newStart;
82
+ let consumedOld = 0;
83
+ let consumedNew = 0;
84
+ let i = start + 1;
85
+ while (consumedOld < oldLines || consumedNew < newLines) {
86
+ if (i >= rawLines.length)
87
+ break;
88
+ const body = rawLines[i];
89
+ if (body.startsWith("diff --git ") || body.startsWith("@@"))
90
+ break;
91
+ // A bare "" marker carries no content of its own.
92
+ if (body.startsWith("\\ ")) {
93
+ i += 1;
94
+ continue;
95
+ }
96
+ const line = classifyHunkBodyLine(body, oldCursor, newCursor, i);
97
+ lines.push(line);
98
+ if (line.kind !== "remove") {
99
+ newCursor += 1;
100
+ consumedNew += 1;
101
+ }
102
+ if (line.kind !== "add") {
103
+ oldCursor += 1;
104
+ consumedOld += 1;
105
+ }
106
+ i += 1;
107
+ }
108
+ return { hunk: { oldStart, oldLines, newStart, newLines, lines, headerRawIndex: start, endRawIndex: i }, next: i };
109
+ }
110
+ /**
111
+ * Parse a unified diff into per-file hunks.
112
+ *
113
+ * Accepts multiple `diff --git` sections. Each file section must open with a `diff --git a/X b/Y`
114
+ * line; `---`/`+++`/`@@` lines follow standard git conventions. A file section with no hunks
115
+ * (e.g. a pure rename or mode change) is skipped — there is nothing here for a line-level
116
+ * detector to look at.
117
+ */
118
+ export function parseUnifiedDiff(text) {
119
+ // Normalize line endings so a CRLF-authored diff parses identically to LF.
120
+ const rawLines = text.replace(/\r\n/g, "\n").split("\n");
121
+ const files = [];
122
+ let i = 0;
123
+ while (i < rawLines.length) {
124
+ if (!rawLines[i].startsWith("diff --git ")) {
125
+ i += 1;
126
+ continue;
127
+ }
128
+ const { header, next: afterHeader } = parseFileHeader(rawLines, i + 1);
129
+ i = afterHeader;
130
+ const hunks = [];
131
+ while (i < rawLines.length && rawLines[i].startsWith("@@")) {
132
+ const { hunk, next } = parseHunk(rawLines, i);
133
+ hunks.push(hunk);
134
+ i = next;
135
+ }
136
+ if (hunks.length > 0 || header.isNew || header.isDeleted) {
137
+ files.push({ oldPath: header.oldPath, newPath: header.newPath, isNew: header.isNew, isDeleted: header.isDeleted, hunks });
138
+ }
139
+ }
140
+ return files;
141
+ }
142
+ /** The path a detector should report findings under — the post-image path, falling back to the
143
+ * pre-image path for a deletion (which carries no post-image). */
144
+ export function reportedPath(file) {
145
+ return file.newPath ?? file.oldPath ?? "(unknown)";
146
+ }
147
+ //# sourceMappingURL=diff-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-parser.js","sourceRoot":"","sources":["../../src/delegate-gate/diff-parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { ReadOriginal } from "./gate.js";
2
+ export interface AutoRepairResult {
3
+ readonly fixedText: string;
4
+ readonly fixedCount: number;
5
+ }
6
+ /**
7
+ * Produce a corrected diff for the two MECHANICAL finding classes: pure indentation-churn hunks
8
+ * (dropped whole) and trivially-redundant literal casts (rewritten in place). Every other finding
9
+ * class is a judgment call and is left in the verdict for a human/reviewer to act on — this
10
+ * function never touches the target repo, only the diff text itself.
11
+ *
12
+ * Both transforms are line-count-preserving or hunk-boundary-preserving by construction (see
13
+ * `isPureIndentationChurnHunk` and `findFixableCastEdits`), so no other hunk's `@@` header ever
14
+ * needs recomputing.
15
+ */
16
+ export declare function autoRepairDiff(diffText: string, readOriginal: ReadOriginal): AutoRepairResult;
@@ -0,0 +1,43 @@
1
+ import { parseUnifiedDiff } from "./diff-parser.js";
2
+ import { isPureIndentationChurnHunk } from "./minimality.js";
3
+ import { findFixableCastEdits } from "./cast-necessity.js";
4
+ /**
5
+ * Produce a corrected diff for the two MECHANICAL finding classes: pure indentation-churn hunks
6
+ * (dropped whole) and trivially-redundant literal casts (rewritten in place). Every other finding
7
+ * class is a judgment call and is left in the verdict for a human/reviewer to act on — this
8
+ * function never touches the target repo, only the diff text itself.
9
+ *
10
+ * Both transforms are line-count-preserving or hunk-boundary-preserving by construction (see
11
+ * `isPureIndentationChurnHunk` and `findFixableCastEdits`), so no other hunk's `@@` header ever
12
+ * needs recomputing.
13
+ */
14
+ export function autoRepairDiff(diffText, readOriginal) {
15
+ const normalized = diffText.replace(/\r\n/g, "\n");
16
+ const rawLines = normalized.split("\n");
17
+ const files = parseUnifiedDiff(normalized);
18
+ const dropped = new Set();
19
+ let fixedCount = 0;
20
+ for (const file of files) {
21
+ for (const hunk of file.hunks) {
22
+ if (!isPureIndentationChurnHunk(hunk))
23
+ continue;
24
+ for (let i = hunk.headerRawIndex; i < hunk.endRawIndex; i++)
25
+ dropped.add(i);
26
+ fixedCount += 1;
27
+ }
28
+ }
29
+ const replacements = new Map();
30
+ for (const edit of findFixableCastEdits(files, readOriginal)) {
31
+ replacements.set(edit.rawIndex, edit.newContent);
32
+ fixedCount += 1;
33
+ }
34
+ const out = [];
35
+ for (let i = 0; i < rawLines.length; i++) {
36
+ if (dropped.has(i))
37
+ continue;
38
+ const replacement = replacements.get(i);
39
+ out.push(replacement === undefined ? rawLines[i] : `+${replacement}`);
40
+ }
41
+ return { fixedText: out.join("\n"), fixedCount };
42
+ }
43
+ //# sourceMappingURL=fix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix.js","sourceRoot":"","sources":["../../src/delegate-gate/fix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAQ3D;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,YAA0B;IACzE,MAAM,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC;gBAAE,SAAS;YAChD,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,EAAE;gBAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC5E,UAAU,IAAI,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,KAAK,MAAM,IAAI,IAAI,oBAAoB,CAAC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC;QAC7D,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACjD,UAAU,IAAI,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QAC7B,MAAM,WAAW,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACxC,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;AACnD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type DiffFile } from "./diff-parser.js";
2
+ import type { Verdict } from "./types.js";
3
+ export type ReadOriginal = (path: string) => string | null;
4
+ /** Read a file the diff modifies from `--repo` — the PRE-image every AST detector reconstructs
5
+ * the post-image against. Absent/unreadable is a legitimate outcome (a file the diff claims to
6
+ * modify but that does not exist at this repo root — quarantine to "no finding", never throw:
7
+ * the gate must still report on every OTHER file). */
8
+ export declare function repoFileReader(repoRoot: string): ReadOriginal;
9
+ /**
10
+ * Run every detector over a parsed diff and fold the results into one verdict.
11
+ *
12
+ * Each detector is independent and pure over (files, readOriginal); a detector throwing is a
13
+ * detector bug, not a caller concern, so callers should treat any throw here as "the gate could
14
+ * not evaluate this diff" rather than catch and silently drop a class.
15
+ */
16
+ export declare function evaluateDiff(files: readonly DiffFile[], readOriginal: ReadOriginal): Verdict;
17
+ /** Parse a unified diff and evaluate it in one call — the entry point the CLI and tests use. */
18
+ export declare function evaluateDiffText(diffText: string, readOriginal: ReadOriginal): Verdict;
@@ -0,0 +1,42 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseUnifiedDiff } from "./diff-parser.js";
4
+ import { analyzeDiffMinimality } from "./minimality.js";
5
+ import { analyzeTestAssertions } from "./test-assertions.js";
6
+ import { analyzeCastNecessity } from "./cast-necessity.js";
7
+ import { analyzeSharedStateMutation } from "./shared-state.js";
8
+ /** Read a file the diff modifies from `--repo` — the PRE-image every AST detector reconstructs
9
+ * the post-image against. Absent/unreadable is a legitimate outcome (a file the diff claims to
10
+ * modify but that does not exist at this repo root — quarantine to "no finding", never throw:
11
+ * the gate must still report on every OTHER file). */
12
+ export function repoFileReader(repoRoot) {
13
+ return (path) => {
14
+ try {
15
+ return readFileSync(join(repoRoot, path), "utf8");
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ }
22
+ /**
23
+ * Run every detector over a parsed diff and fold the results into one verdict.
24
+ *
25
+ * Each detector is independent and pure over (files, readOriginal); a detector throwing is a
26
+ * detector bug, not a caller concern, so callers should treat any throw here as "the gate could
27
+ * not evaluate this diff" rather than catch and silently drop a class.
28
+ */
29
+ export function evaluateDiff(files, readOriginal) {
30
+ const findings = [
31
+ ...analyzeDiffMinimality(files),
32
+ ...analyzeTestAssertions(files, readOriginal),
33
+ ...analyzeCastNecessity(files, readOriginal),
34
+ ...analyzeSharedStateMutation(files, readOriginal),
35
+ ];
36
+ return { pass: findings.length === 0, findings };
37
+ }
38
+ /** Parse a unified diff and evaluate it in one call — the entry point the CLI and tests use. */
39
+ export function evaluateDiffText(diffText, readOriginal) {
40
+ return evaluateDiff(parseUnifiedDiff(diffText), readOriginal);
41
+ }
42
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/delegate-gate/gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAiB,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAK/D;;;sDAGsD;AACtD,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,OAAO,CAAC,IAAY,EAAiB,EAAE;QACrC,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,KAA0B,EAAE,YAA0B;IACjF,MAAM,QAAQ,GAAc;QAC1B,GAAG,qBAAqB,CAAC,KAAK,CAAC;QAC/B,GAAG,qBAAqB,CAAC,KAAK,EAAE,YAAY,CAAC;QAC7C,GAAG,oBAAoB,CAAC,KAAK,EAAE,YAAY,CAAC;QAC5C,GAAG,0BAA0B,CAAC,KAAK,EAAE,YAAY,CAAC;KACnD,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;AACnD,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,YAA0B;IAC3E,OAAO,YAAY,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { DiffFile, DiffHunk } from "./diff-parser.js";
2
+ import type { Finding } from "./types.js";
3
+ /**
4
+ * Is this hunk PURE indentation churn: every removed and every added line accounted for by a
5
+ * whitespace-only pair, with nothing left unpaired?
6
+ *
7
+ * The "fully paired" requirement (every remove and every add line in the hunk is consumed by a
8
+ * 1:1 pair) is what makes the auto-fix in `fix.ts` safe: dropping a fully-paired churn hunk
9
+ * removes an equal number of old-side and new-side lines, so it can never shift another hunk's
10
+ * line numbers. A hunk containing an unequal-length remove/add run alongside pure churn is NOT
11
+ * flagged here — `pairedRemoveAddRuns` silently skips unequal runs, and treating the REST of the
12
+ * hunk as "100% churn" while ignoring lines it could not pair would both misreport the hunk and
13
+ * make an auto-fix unsafe.
14
+ */
15
+ export declare function isPureIndentationChurnHunk(hunk: DiffHunk): boolean;
16
+ /**
17
+ * Flag indentation-only churn and count changed lines with no semantic delta.
18
+ *
19
+ * Two finding shapes:
20
+ * - `indentation-churn`: a hunk whose every line is accounted for by a whitespace-only
21
+ * remove/add pair — the hunk changed formatting and nothing else. Auto-fixable: dropping the
22
+ * whole hunk is safe (see `isPureIndentationChurnHunk`).
23
+ * - `non-minimal-diff`: a hunk that is NOT pure churn but where at least half its PAIRED changed
24
+ * lines (≥6 of them) carry no semantic delta — evidence of a diff that reformats lines it did
25
+ * not need to touch alongside a real change, i.e. class (e), "non-minimal diffs touching
26
+ * unrelated lines". Not auto-fixable: a partial hunk mixes a real edit with the noise, and
27
+ * removing only the noise without re-deriving the hunk boundaries risks corrupting the real
28
+ * change.
29
+ */
30
+ export declare function analyzeDiffMinimality(files: readonly DiffFile[]): Finding[];