diffninja 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questions for the reviewing agent's own model.
|
|
3
|
+
*
|
|
4
|
+
* diffninja calls no model. Where a judgment needs meaning rather than syntax —
|
|
5
|
+
* does this change what callers observe, does a test exercise it, does a test
|
|
6
|
+
* change weaken it, does the documentation match the code, does the hunk serve
|
|
7
|
+
* the stated goal — the report asks the agent that requested it. Questions are
|
|
8
|
+
* fixed templates bound to specific hunks, generated deterministically from the
|
|
9
|
+
* facts, each with a closed set of options that always includes `cannot-tell`.
|
|
10
|
+
*
|
|
11
|
+
* Answers are recorded through the `record_answers` tool, validated against
|
|
12
|
+
* these options, attributed to the answering MCP client, and shown beside the
|
|
13
|
+
* hunk. They never change a status, a priority, or the report order: an answer
|
|
14
|
+
* is another reader's view, not a verdict.
|
|
15
|
+
*/
|
|
16
|
+
import { factQuestionsFor } from "./change-facts.js";
|
|
17
|
+
import { testLikeFile } from "./file-role.js";
|
|
18
|
+
/** Most questions one report asks; hunks earlier in the report are asked first. */
|
|
19
|
+
export const MAX_REVIEW_QUESTIONS = 36;
|
|
20
|
+
/** Most revert and removed-fix questions one report asks; the rest stay in the project context. */
|
|
21
|
+
export const MAX_HISTORY_QUESTIONS = 3;
|
|
22
|
+
export const QUESTION_OPTIONS = {
|
|
23
|
+
behaviorChange: ["changes-behavior", "no-behavior-change", "cannot-tell"],
|
|
24
|
+
testCoverage: ["exercised", "not-exercised", "cannot-tell"],
|
|
25
|
+
testWeakened: ["weakens", "does-not-weaken", "cannot-tell"],
|
|
26
|
+
docMatchesCode: ["matches", "contradicts", "cannot-tell"],
|
|
27
|
+
intentFit: ["serves", "supports", "unrelated", "contradicts", "cannot-tell"],
|
|
28
|
+
undoesFix: ["keeps-its-purpose", "undoes-it", "cannot-tell"],
|
|
29
|
+
repeatsRevert: ["reintroduces-it", "different-change", "cannot-tell"],
|
|
30
|
+
followsGuidelines: ["follows", "breaks-a-rule", "not-covered", "cannot-tell"],
|
|
31
|
+
followsConvention: ["should-follow", "differs-for-a-reason", "cannot-tell"],
|
|
32
|
+
};
|
|
33
|
+
/** The short label a page shows for each answer, so the question's full wording stays with the agent. */
|
|
34
|
+
export const ANSWER_VERDICTS = {
|
|
35
|
+
behaviorChange: {
|
|
36
|
+
"changes-behavior": { label: "Changes behavior", tone: "quiet" },
|
|
37
|
+
"no-behavior-change": { label: "No behavior change", tone: "quiet" },
|
|
38
|
+
"cannot-tell": { label: "Behavior change unclear", tone: "unsure" },
|
|
39
|
+
},
|
|
40
|
+
testCoverage: {
|
|
41
|
+
exercised: { label: "Tested in this PR", tone: "ok" },
|
|
42
|
+
"not-exercised": { label: "No test in this PR", tone: "watch" },
|
|
43
|
+
"cannot-tell": { label: "Test coverage unclear", tone: "unsure" },
|
|
44
|
+
},
|
|
45
|
+
testWeakened: {
|
|
46
|
+
weakens: { label: "Weakens a test", tone: "watch" },
|
|
47
|
+
"does-not-weaken": { label: "Test not weakened", tone: "ok" },
|
|
48
|
+
"cannot-tell": { label: "Test change unclear", tone: "unsure" },
|
|
49
|
+
},
|
|
50
|
+
docMatchesCode: {
|
|
51
|
+
matches: { label: "Docs match the code", tone: "ok" },
|
|
52
|
+
contradicts: { label: "Docs contradict the code", tone: "watch" },
|
|
53
|
+
"cannot-tell": { label: "Docs unclear", tone: "unsure" },
|
|
54
|
+
},
|
|
55
|
+
intentFit: {
|
|
56
|
+
serves: { label: "Serves the PR goal", tone: "ok" },
|
|
57
|
+
supports: { label: "Supports the PR goal", tone: "quiet" },
|
|
58
|
+
unrelated: { label: "Unrelated to the PR goal", tone: "watch" },
|
|
59
|
+
contradicts: { label: "Works against the PR goal", tone: "watch" },
|
|
60
|
+
"cannot-tell": { label: "Fit with the goal unclear", tone: "unsure" },
|
|
61
|
+
},
|
|
62
|
+
undoesFix: {
|
|
63
|
+
"keeps-its-purpose": { label: "Keeps an earlier fix", tone: "ok" },
|
|
64
|
+
"undoes-it": { label: "Undoes an earlier fix", tone: "watch" },
|
|
65
|
+
"cannot-tell": { label: "Earlier fix unclear", tone: "unsure" },
|
|
66
|
+
},
|
|
67
|
+
repeatsRevert: {
|
|
68
|
+
"reintroduces-it": { label: "Repeats a reverted change", tone: "watch" },
|
|
69
|
+
"different-change": { label: "Not a reverted change", tone: "ok" },
|
|
70
|
+
"cannot-tell": { label: "Revert history unclear", tone: "unsure" },
|
|
71
|
+
},
|
|
72
|
+
followsGuidelines: {
|
|
73
|
+
follows: { label: "Follows project guidelines", tone: "ok" },
|
|
74
|
+
"breaks-a-rule": { label: "Breaks a project guideline", tone: "watch" },
|
|
75
|
+
"not-covered": { label: "No guideline applies", tone: "quiet" },
|
|
76
|
+
"cannot-tell": { label: "Guidelines unclear", tone: "unsure" },
|
|
77
|
+
},
|
|
78
|
+
followsConvention: {
|
|
79
|
+
"should-follow": { label: "Should match its sibling files", tone: "watch" },
|
|
80
|
+
"differs-for-a-reason": { label: "Differs from siblings on purpose", tone: "quiet" },
|
|
81
|
+
"cannot-tell": { label: "Convention unclear", tone: "unsure" },
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/** The verdict for a recorded answer, or undefined for an answer the question does not list. */
|
|
85
|
+
export function verdictOf(kind, choice) {
|
|
86
|
+
const table = ANSWER_VERDICTS[kind];
|
|
87
|
+
return Object.hasOwn(table, choice) ? table[choice] : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** Removed assertions or added skips: the shapes a weakened test takes. */
|
|
90
|
+
const ASSERTION_LINE = /\b(?:expect|assert\w*|should)\b|\bt\.\w+\(|\bself\.assert\w*\(/;
|
|
91
|
+
const SKIP_LINE = /\b(?:it|test|describe)\.(?:skip|todo)\b|\bx(?:it|describe)\(|@(?:pytest\.mark\.)?skip\b|\.only\(/;
|
|
92
|
+
function hunkName(item) {
|
|
93
|
+
return `${item.file} ${item.header.split(" @@")[0]} @@`;
|
|
94
|
+
}
|
|
95
|
+
function changedLines(item, marker) {
|
|
96
|
+
return item.diff.split("\n").filter((line) => line.startsWith(marker) && !line.startsWith(marker.repeat(3))).map((line) => line.slice(1));
|
|
97
|
+
}
|
|
98
|
+
/** A test hunk that removes an assertion or adds a skip or an exclusive `.only`. */
|
|
99
|
+
function mayWeakenTest(item) {
|
|
100
|
+
return changedLines(item, "-").some((line) => ASSERTION_LINE.test(line)) || changedLines(item, "+").some((line) => SKIP_LINE.test(line));
|
|
101
|
+
}
|
|
102
|
+
function isRead(item) {
|
|
103
|
+
// Import-only hunks are wiring: whatever they enable is asked where it is used.
|
|
104
|
+
return item.facts !== undefined && item.facts.language !== null && item.status !== "passed" && item.facts.importsOnly !== true;
|
|
105
|
+
}
|
|
106
|
+
/** The questions for one report, in report order, at most {@link MAX_REVIEW_QUESTIONS}. */
|
|
107
|
+
export function reviewQuestions(items, intent, project) {
|
|
108
|
+
const drafts = [];
|
|
109
|
+
const ask = (kind, unitIds, text) => drafts.push({ kind, unitIds, text, options: QUESTION_OPTIONS[kind] });
|
|
110
|
+
const tests = items.filter((item) => isRead(item) && testLikeFile(item.file));
|
|
111
|
+
const goal = intent?.title.trim() ?? "";
|
|
112
|
+
// Project questions first: there are few, and each needs the agent to read beyond the diff.
|
|
113
|
+
const firstRead = items.find((item) => isRead(item) && !testLikeFile(item.file)) ?? items.find(isRead);
|
|
114
|
+
if (project !== undefined && firstRead !== undefined) {
|
|
115
|
+
for (const revert of project.reverts.slice(0, MAX_HISTORY_QUESTIONS)) {
|
|
116
|
+
const reason = revert.reason;
|
|
117
|
+
const owner = reason.kind === "file" ? items.find((item) => item.file === reason.file && isRead(item)) : undefined;
|
|
118
|
+
ask("repeatsRevert", [(owner ?? firstRead).id], `Commit ${revert.commit} (${revert.date}) was a revert: "${revert.subject}". Read it (git show ${revert.commit}). ` +
|
|
119
|
+
"Does this change reintroduce what was reverted, or a close variant of it?");
|
|
120
|
+
}
|
|
121
|
+
if (project.guidelines.length > 0) {
|
|
122
|
+
ask("followsGuidelines", [firstRead.id], `This repository has contributor guidelines: ${project.guidelines.join(", ")}. Read the ones that apply to the changed files. ` +
|
|
123
|
+
"Does the change follow them (process, versioning or preview rules, style, tests, documentation)?");
|
|
124
|
+
}
|
|
125
|
+
for (const convention of project.conventions) {
|
|
126
|
+
const owner = items.find((item) => item.file === convention.file) ?? firstRead;
|
|
127
|
+
const names = convention.common.map((entry) => `${entry.name} (${entry.peers}/${convention.peers})`).join(", ");
|
|
128
|
+
ask("followsConvention", [owner.id], `Most files matching ${convention.pattern} use ${names}; the new ${convention.file} uses none of them. ` +
|
|
129
|
+
"Read two or three of those files. Should the new file follow their pattern?");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let fixQuestions = 0;
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
if (!isRead(item))
|
|
135
|
+
continue;
|
|
136
|
+
const language = item.facts.language;
|
|
137
|
+
// Tests and their snapshots change with the code they check; asked about in code only.
|
|
138
|
+
const origin = testLikeFile(item.file) ? undefined : item.history?.origins.find((entry) => entry.notable);
|
|
139
|
+
if (origin !== undefined && fixQuestions < MAX_HISTORY_QUESTIONS) {
|
|
140
|
+
fixQuestions += 1;
|
|
141
|
+
ask("undoesFix", [item.id], `${hunkName(item)} removes or rewrites ${origin.lines} line(s) last changed by ${origin.commit} (${origin.date}) "${origin.subject}". ` +
|
|
142
|
+
`Read that commit (git show ${origin.commit}). Does this hunk undo what it did, without keeping its purpose some other way?`);
|
|
143
|
+
}
|
|
144
|
+
if (testLikeFile(item.file)) {
|
|
145
|
+
if (mayWeakenTest(item)) {
|
|
146
|
+
ask("testWeakened", [item.id], `Does this change to ${hunkName(item)} weaken what the test checks (a removed or loosened assertion, a skipped or exclusive test)?`);
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (language === "prose") {
|
|
151
|
+
const yes = factQuestionsFor("prose").filter((question) => item.facts.answers[question] === "yes");
|
|
152
|
+
if (yes.includes("instructionChanged") || yes.includes("limitChanged")) {
|
|
153
|
+
ask("docMatchesCode", [item.id], `Does what ${hunkName(item)} now tells readers match how the code actually behaves?`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (item.status === "attention") {
|
|
157
|
+
ask("behaviorChange", [item.id], `Taken on its own, does ${hunkName(item)} change what callers, users, or operators of this code observe or must provide? ` +
|
|
158
|
+
"Answer no-behavior-change when the hunk only adds or renames a declaration (a new function, type, field, " +
|
|
159
|
+
"import, or injected dependency) that other hunks put to use: the use is asked about where it happens.");
|
|
160
|
+
if (language !== "config") {
|
|
161
|
+
ask("testCoverage", [item.id, ...tests.map((test) => test.id)], tests.length > 0
|
|
162
|
+
? `Does any test changed in this diff exercise the behavior ${hunkName(item)} changes?`
|
|
163
|
+
: `No test file changed in this diff. Does an existing test exercise the behavior ${hunkName(item)} changes?`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (goal !== "" && item.status === "attention") {
|
|
167
|
+
ask("intentFit", [item.id], `How does ${hunkName(item)} relate to the stated goal: "${goal}"? ` +
|
|
168
|
+
"serves: it makes the change the goal describes. supports: it does not make that change itself, but a " +
|
|
169
|
+
"change that does relies on it (a helper, type, query, wiring, or refactor) or it is a related fix for the " +
|
|
170
|
+
"same problem. unrelated: neither. contradicts: it works against the goal.");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return drafts.slice(0, MAX_REVIEW_QUESTIONS).map((draft, index) => ({ id: `q${index + 1}`, ...draft }));
|
|
174
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AutomaticFinding, CheckCoverage } from "./evidence-types.js";
|
|
2
|
+
import type { ReviewUnit } from "./types.js";
|
|
3
|
+
export interface ReferenceCheckResult {
|
|
4
|
+
findings: AutomaticFinding[];
|
|
5
|
+
check: CheckCoverage;
|
|
6
|
+
}
|
|
7
|
+
export declare function checkReferences(repo: string, base: string, head: string, units: readonly ReviewUnit[], project: string): Promise<ReferenceCheckResult>;
|