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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/calltree.d.ts +47 -0
  4. package/dist/calltree.js +296 -0
  5. package/dist/cli.d.ts +57 -0
  6. package/dist/cli.js +340 -0
  7. package/dist/diff.d.ts +7 -0
  8. package/dist/diff.js +114 -0
  9. package/dist/extract.d.ts +26 -0
  10. package/dist/extract.js +152 -0
  11. package/dist/git.d.ts +40 -0
  12. package/dist/git.js +288 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +8 -0
  15. package/dist/infer.d.ts +21 -0
  16. package/dist/infer.js +189 -0
  17. package/dist/languages/bash.d.ts +2 -0
  18. package/dist/languages/bash.js +208 -0
  19. package/dist/languages/c.d.ts +2 -0
  20. package/dist/languages/c.js +218 -0
  21. package/dist/languages/call-syntax.d.ts +125 -0
  22. package/dist/languages/call-syntax.js +997 -0
  23. package/dist/languages/cpp.d.ts +2 -0
  24. package/dist/languages/cpp.js +321 -0
  25. package/dist/languages/csharp.d.ts +2 -0
  26. package/dist/languages/csharp.js +324 -0
  27. package/dist/languages/elixir.d.ts +2 -0
  28. package/dist/languages/elixir.js +331 -0
  29. package/dist/languages/go.d.ts +2 -0
  30. package/dist/languages/go.js +299 -0
  31. package/dist/languages/grammars.d.ts +50 -0
  32. package/dist/languages/grammars.js +351 -0
  33. package/dist/languages/haskell.d.ts +2 -0
  34. package/dist/languages/haskell.js +250 -0
  35. package/dist/languages/java.d.ts +2 -0
  36. package/dist/languages/java.js +351 -0
  37. package/dist/languages/javascript.d.ts +4 -0
  38. package/dist/languages/javascript.js +648 -0
  39. package/dist/languages/kotlin.d.ts +2 -0
  40. package/dist/languages/kotlin.js +368 -0
  41. package/dist/languages/lua.d.ts +2 -0
  42. package/dist/languages/lua.js +212 -0
  43. package/dist/languages/ocaml.d.ts +2 -0
  44. package/dist/languages/ocaml.js +291 -0
  45. package/dist/languages/perl.d.ts +2 -0
  46. package/dist/languages/perl.js +418 -0
  47. package/dist/languages/php.d.ts +2 -0
  48. package/dist/languages/php.js +397 -0
  49. package/dist/languages/python.d.ts +2 -0
  50. package/dist/languages/python.js +376 -0
  51. package/dist/languages/registry.d.ts +7 -0
  52. package/dist/languages/registry.js +69 -0
  53. package/dist/languages/ruby.d.ts +2 -0
  54. package/dist/languages/ruby.js +391 -0
  55. package/dist/languages/rust.d.ts +2 -0
  56. package/dist/languages/rust.js +261 -0
  57. package/dist/languages/scala.d.ts +2 -0
  58. package/dist/languages/scala.js +307 -0
  59. package/dist/languages/solidity.d.ts +2 -0
  60. package/dist/languages/solidity.js +240 -0
  61. package/dist/languages/swift.d.ts +2 -0
  62. package/dist/languages/swift.js +268 -0
  63. package/dist/languages/types.d.ts +36 -0
  64. package/dist/languages/types.js +74 -0
  65. package/dist/languages/typescript-contracts.d.ts +57 -0
  66. package/dist/languages/typescript-contracts.js +528 -0
  67. package/dist/languages/typescript-dispatch.d.ts +68 -0
  68. package/dist/languages/typescript-dispatch.js +710 -0
  69. package/dist/languages/typescript.d.ts +4 -0
  70. package/dist/languages/typescript.js +722 -0
  71. package/dist/languages/zig.d.ts +2 -0
  72. package/dist/languages/zig.js +243 -0
  73. package/dist/loc.d.ts +17 -0
  74. package/dist/loc.js +34 -0
  75. package/dist/reach.d.ts +17 -0
  76. package/dist/reach.js +65 -0
  77. package/dist/render.d.ts +18 -0
  78. package/dist/render.js +83 -0
  79. package/dist/review/brand.d.ts +8 -0
  80. package/dist/review/brand.js +25 -0
  81. package/dist/review/call-context.d.ts +27 -0
  82. package/dist/review/call-context.js +446 -0
  83. package/dist/review/call-flow-html.d.ts +32 -0
  84. package/dist/review/call-flow-html.js +1870 -0
  85. package/dist/review/call-flow-nav.d.ts +151 -0
  86. package/dist/review/call-flow-nav.js +317 -0
  87. package/dist/review/call-flow.d.ts +47 -0
  88. package/dist/review/call-flow.js +229 -0
  89. package/dist/review/change-facts.d.ts +69 -0
  90. package/dist/review/change-facts.js +729 -0
  91. package/dist/review/cli.d.ts +2 -0
  92. package/dist/review/cli.js +50 -0
  93. package/dist/review/connected-analysis.d.ts +100 -0
  94. package/dist/review/connected-analysis.js +163 -0
  95. package/dist/review/connected-html.d.ts +17 -0
  96. package/dist/review/connected-html.js +2853 -0
  97. package/dist/review/connected.d.ts +23 -0
  98. package/dist/review/connected.js +141 -0
  99. package/dist/review/escape-html.d.ts +2 -0
  100. package/dist/review/escape-html.js +9 -0
  101. package/dist/review/evidence-html.d.ts +21 -0
  102. package/dist/review/evidence-html.js +521 -0
  103. package/dist/review/evidence-syntax.d.ts +132 -0
  104. package/dist/review/evidence-syntax.js +478 -0
  105. package/dist/review/evidence-types.d.ts +62 -0
  106. package/dist/review/evidence-types.js +1 -0
  107. package/dist/review/evidence.d.ts +31 -0
  108. package/dist/review/evidence.js +1603 -0
  109. package/dist/review/file-role.d.ts +9 -0
  110. package/dist/review/file-role.js +29 -0
  111. package/dist/review/github.d.ts +204 -0
  112. package/dist/review/github.js +1245 -0
  113. package/dist/review/history.d.ts +101 -0
  114. package/dist/review/history.js +412 -0
  115. package/dist/review/html.d.ts +34 -0
  116. package/dist/review/html.js +1104 -0
  117. package/dist/review/input.d.ts +10 -0
  118. package/dist/review/input.js +113 -0
  119. package/dist/review/intent.d.ts +4 -0
  120. package/dist/review/intent.js +75 -0
  121. package/dist/review/mcp-cli.d.ts +2 -0
  122. package/dist/review/mcp-cli.js +25 -0
  123. package/dist/review/mcp.d.ts +12 -0
  124. package/dist/review/mcp.js +414 -0
  125. package/dist/review/module-resolution.d.ts +2 -0
  126. package/dist/review/module-resolution.js +86 -0
  127. package/dist/review/palette.d.ts +7 -0
  128. package/dist/review/palette.js +104 -0
  129. package/dist/review/pipeline.d.ts +77 -0
  130. package/dist/review/pipeline.js +227 -0
  131. package/dist/review/pr-input.d.ts +19 -0
  132. package/dist/review/pr-input.js +130 -0
  133. package/dist/review/questions.d.ts +201 -0
  134. package/dist/review/questions.js +174 -0
  135. package/dist/review/reference-check.d.ts +7 -0
  136. package/dist/review/reference-check.js +733 -0
  137. package/dist/review/report-pages.d.ts +109 -0
  138. package/dist/review/report-pages.js +328 -0
  139. package/dist/review/service.d.ts +23 -0
  140. package/dist/review/service.js +198 -0
  141. package/dist/review/setup.d.ts +112 -0
  142. package/dist/review/setup.js +549 -0
  143. package/dist/review/source.d.ts +26 -0
  144. package/dist/review/source.js +276 -0
  145. package/dist/review/toml.d.ts +38 -0
  146. package/dist/review/toml.js +565 -0
  147. package/dist/review/types.d.ts +179 -0
  148. package/dist/review/types.js +1 -0
  149. package/dist/run.d.ts +49 -0
  150. package/dist/run.js +311 -0
  151. package/dist/types.d.ts +366 -0
  152. package/dist/types.js +83 -0
  153. package/package.json +88 -0
  154. package/scripts/ensure-native-grammar.mjs +188 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { runSetup, setupHelp } from "./setup.js";
4
+ /**
5
+ * diffninja runs inside an agent CLI through its MCP server; this command only
6
+ * registers that server. Reviews are requested from the agent, which calls the
7
+ * `review_diff` tool and, for a pull request link, returns the connected review
8
+ * page. There is no terminal review path, so any other invocation says how to
9
+ * get one instead of guessing what was meant.
10
+ */
11
+ const help = `diffninja. Pull request review inside your coding agent.
12
+
13
+ diffninja setup [--cli claude,codex,omp,pi] [--uninstall] [--dry-run] [--no-install]
14
+ Register the diffninja MCP server on every detected agent CLI.
15
+ diffninja --help
16
+
17
+ After setup, ask your agent to review a GitHub pull request link, a diff, or a git
18
+ range. The agent calls the review_diff tool; a pull request link opens a connected
19
+ review page where you read the change, comment inline, and submit the review
20
+ yourself. diffninja has no terminal review mode.
21
+ `;
22
+ const NO_TERMINAL_REVIEW = "diffninja reviews run inside an agent CLI (Claude Code, Codex, pi, ...) through MCP. " +
23
+ "Run `diffninja setup` to register it, then ask your agent to review a pull request link, a diff, or a git range. " +
24
+ "See `diffninja --help`.";
25
+ async function main() {
26
+ const [command, ...args] = process.argv.slice(2);
27
+ if (command === undefined || command === "--help" || command === "-h" || command === "help") {
28
+ console.log(help);
29
+ return;
30
+ }
31
+ if (command !== "setup")
32
+ throw new Error(NO_TERMINAL_REVIEW);
33
+ const { values, positionals } = parseArgs({ args, options: {
34
+ cli: { type: "string" }, uninstall: { type: "boolean" },
35
+ "dry-run": { type: "boolean" }, "no-install": { type: "boolean" }, help: { type: "boolean" },
36
+ }, strict: true, allowPositionals: true });
37
+ if (values.help) {
38
+ console.log(setupHelp);
39
+ return;
40
+ }
41
+ if (positionals.length > 0)
42
+ throw new Error("setup takes no positional arguments. Use --help.");
43
+ await runSetup({
44
+ clis: values.cli === undefined ? undefined : values.cli.split(",").map((part) => part.trim()).filter((part) => part !== ""),
45
+ uninstall: values.uninstall === true,
46
+ dryRun: values["dry-run"] === true,
47
+ noInstall: values["no-install"] === true,
48
+ });
49
+ }
50
+ main().catch(error => { console.error(`diffninja: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The local analysis of a connected pull request, as the connected page shows it.
3
+ *
4
+ * The connected page reviews the canonical GitHub patch; this is the same
5
+ * deterministic analysis a static review produces for that patch — status,
6
+ * priority, change facts with their cited lines, the reading agenda, and the
7
+ * questions for the agent with any answers recorded — reduced to what the page
8
+ * renders. It is bound to one snapshot id, so a page never shows analysis of a
9
+ * revision other than the one it reviews. Nothing here is model output: the
10
+ * facts are lexical, and an answer is a closed-set choice attributed to the
11
+ * MCP client that recorded it.
12
+ */
13
+ import { type QuestionKind, type Verdict } from "./questions.js";
14
+ import type { ReviewReport, ReviewStatus, SuggestedComment } from "./types.js";
15
+ /** Most agenda entries the page lists; the full report has the rest. */
16
+ export declare const CONNECTED_AGENDA_LIMIT = 5;
17
+ export interface ConnectedFact {
18
+ readonly label: string;
19
+ readonly side: "added" | "removed";
20
+ readonly text: string;
21
+ /** Where the cited line is in the diff, so the page can jump to it; absent when it cannot be placed. */
22
+ at?: HunkLanding;
23
+ }
24
+ export interface ConnectedQuestion {
25
+ readonly id: string;
26
+ readonly kind: QuestionKind;
27
+ readonly text: string;
28
+ readonly options: readonly string[];
29
+ choice?: string;
30
+ answeredBy?: string;
31
+ /** The short label the page shows for the recorded answer. */
32
+ verdict?: Verdict;
33
+ }
34
+ export interface ConnectedHunk {
35
+ readonly id: string;
36
+ readonly file: string;
37
+ readonly header: string;
38
+ readonly added: number;
39
+ readonly removed: number;
40
+ /** First line to scroll to, on the side the diff shows it: the new side unless the hunk only removes. */
41
+ readonly line: number;
42
+ readonly side: "LEFT" | "RIGHT";
43
+ readonly status: ReviewStatus;
44
+ readonly facts: readonly ConnectedFact[];
45
+ /** One fixed sentence when the hunk was not read: a metadata unit or an unread file type. */
46
+ note?: string;
47
+ readonly questions: readonly ConnectedQuestion[];
48
+ }
49
+ /** Whether the analysis had a local clone's definitions and call flows. */
50
+ export interface ConnectedScope {
51
+ readonly source: "repository" | "patch";
52
+ readonly note: string;
53
+ }
54
+ export interface ConnectedAnalysis {
55
+ readonly available: true;
56
+ readonly scope: ConnectedScope;
57
+ readonly snapshotId: string;
58
+ readonly reviewId: string;
59
+ readonly reportUrl: string;
60
+ readonly counts: Readonly<Record<ReviewStatus, number>>;
61
+ readonly agenda: readonly {
62
+ readonly title: string;
63
+ readonly reason: string;
64
+ }[];
65
+ /** Whose order `hunks` follow: the reviewing agent's once it recorded one, otherwise diffninja's. */
66
+ readonly order: ConnectedOrder;
67
+ /** Every hunk, in report order. */
68
+ readonly hunks: readonly ConnectedHunk[];
69
+ readonly questions: {
70
+ readonly total: number;
71
+ readonly answered: number;
72
+ };
73
+ /** Line comments the reviewing agent suggested, for the human to add to their review or not. */
74
+ /** Changed files that have call-flow diagrams, for the page's "Call flow" buttons; empty for a patch-only analysis. */
75
+ readonly callFlowFiles: readonly string[];
76
+ suggestions?: {
77
+ readonly suggestedBy: string;
78
+ readonly comments: readonly SuggestedComment[];
79
+ };
80
+ }
81
+ export type ConnectedOrder = {
82
+ readonly source: "agent";
83
+ readonly orderedBy: string;
84
+ } | {
85
+ readonly source: "diffninja";
86
+ };
87
+ export interface ConnectedAnalysisUnavailable {
88
+ readonly available: false;
89
+ readonly reason: string;
90
+ }
91
+ export type ConnectedAnalysisView = ConnectedAnalysis | ConnectedAnalysisUnavailable;
92
+ /** Where the page scrolls to for one hunk. */
93
+ interface HunkLanding {
94
+ readonly line: number;
95
+ readonly side: "LEFT" | "RIGHT";
96
+ }
97
+ /** Changed files with at least one call-flow tree, in report order. */
98
+ export declare function callFlowFilesOf(report: ReviewReport): string[];
99
+ export declare function connectedAnalysisOf(report: ReviewReport, snapshotId: string, reviewId: string, reportUrl: string, scope: ConnectedScope): ConnectedAnalysis;
100
+ export {};
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The local analysis of a connected pull request, as the connected page shows it.
3
+ *
4
+ * The connected page reviews the canonical GitHub patch; this is the same
5
+ * deterministic analysis a static review produces for that patch — status,
6
+ * priority, change facts with their cited lines, the reading agenda, and the
7
+ * questions for the agent with any answers recorded — reduced to what the page
8
+ * renders. It is bound to one snapshot id, so a page never shows analysis of a
9
+ * revision other than the one it reviews. Nothing here is model output: the
10
+ * facts are lexical, and an answer is a closed-set choice attributed to the
11
+ * MCP client that recorded it.
12
+ */
13
+ import { CHANGE_FACT_QUESTIONS } from "./change-facts.js";
14
+ import { verdictOf } from "./questions.js";
15
+ /** Most agenda entries the page lists; the full report has the rest. */
16
+ export const CONNECTED_AGENDA_LIMIT = 5;
17
+ /** Short tags: each names what to check on the line it points at. */
18
+ const FACT_LABEL = {
19
+ comparisonChanged: "Comparison",
20
+ limitChanged: "Limit",
21
+ validationChanged: "Input check",
22
+ failurePropagated: "Throws to caller",
23
+ failureDeferred: "Retry or deferral",
24
+ failureDiscarded: "Swallowed error",
25
+ contractChanged: "Public API",
26
+ dataChanged: "Schema or data",
27
+ queryChanged: "SQL query",
28
+ instructionChanged: "Instructions",
29
+ referenceChanged: "Link",
30
+ gateWeakened: "CI gate weakened",
31
+ permissionChanged: "Permissions",
32
+ pinChanged: "Version pin",
33
+ };
34
+ /** The diff line a fact cites: the first changed row on its side whose text is the evidence. */
35
+ function lineOf(item, side, text) {
36
+ const marker = side === "added" ? "+" : "-";
37
+ const truncated = text.endsWith("\u2026");
38
+ const stem = truncated ? text.slice(0, -1) : text;
39
+ let oldLine = item.oldStart;
40
+ let newLine = item.newStart;
41
+ for (const row of item.diff.split("\n").slice(1)) {
42
+ if (row.startsWith(marker)) {
43
+ const body = row.slice(1).trim();
44
+ if (truncated ? body.startsWith(stem) : body === stem)
45
+ return side === "added" ? { line: newLine, side: "RIGHT" } : { line: oldLine, side: "LEFT" };
46
+ }
47
+ if (row.startsWith("+"))
48
+ newLine += 1;
49
+ else if (row.startsWith("-"))
50
+ oldLine += 1;
51
+ else if (row.startsWith(" ")) {
52
+ oldLine += 1;
53
+ newLine += 1;
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+ /** The first line a reader should land on: the first changed line, on its own side. */
59
+ function landingOf(item) {
60
+ let oldLine = item.oldStart;
61
+ let newLine = item.newStart;
62
+ for (const text of item.diff.split("\n").slice(1)) {
63
+ if (text.startsWith("+"))
64
+ return { line: newLine, side: "RIGHT" };
65
+ if (text.startsWith("-"))
66
+ return { line: oldLine, side: "LEFT" };
67
+ if (text.startsWith(" ")) {
68
+ oldLine += 1;
69
+ newLine += 1;
70
+ }
71
+ }
72
+ return { line: item.newStart, side: "RIGHT" };
73
+ }
74
+ function noteOf(item) {
75
+ if (item.facts === undefined && item.status !== "passed") {
76
+ return "Not read by diffninja (binary, rename, mode, or other metadata-only change). Check it yourself.";
77
+ }
78
+ if (item.facts?.language === null)
79
+ return "diffninja does not read this file type. Read this hunk yourself.";
80
+ if (item.facts?.inert === true)
81
+ return "Formatting or comments only.";
82
+ if (item.facts?.importsOnly === true)
83
+ return "Imports only: read where the imported names are used.";
84
+ return undefined;
85
+ }
86
+ /** Changed files with at least one call-flow tree, in report order. */
87
+ export function callFlowFilesOf(report) {
88
+ const changed = new Set(report.items.map((item) => item.file));
89
+ return report.callFlows.filter((entry) => entry.trees.length > 0 && changed.has(entry.file)).map((entry) => entry.file);
90
+ }
91
+ export function connectedAnalysisOf(report, snapshotId, reviewId, reportUrl, scope) {
92
+ const questionsByUnit = new Map();
93
+ for (const question of report.questions) {
94
+ const view = { id: question.id, kind: question.kind, text: question.text, options: question.options };
95
+ if (question.answer !== undefined) {
96
+ view.choice = question.answer.choice;
97
+ view.answeredBy = question.answer.answeredBy;
98
+ const verdict = verdictOf(question.kind, question.answer.choice);
99
+ if (verdict !== undefined)
100
+ view.verdict = verdict;
101
+ }
102
+ const owner = question.unitIds[0];
103
+ if (owner === undefined)
104
+ continue;
105
+ const list = questionsByUnit.get(owner) ?? [];
106
+ list.push(view);
107
+ questionsByUnit.set(owner, list);
108
+ }
109
+ const counts = { attention: 0, uncertain: 0, low: 0, passed: 0 };
110
+ const hunks = report.items.map((item) => {
111
+ counts[item.status] += 1;
112
+ const facts = [];
113
+ for (const question of CHANGE_FACT_QUESTIONS) {
114
+ const evidence = item.facts?.evidence[question];
115
+ if (item.facts?.answers[question] === "yes" && evidence !== undefined) {
116
+ const fact = { label: FACT_LABEL[question], side: evidence.side, text: evidence.text };
117
+ const at = lineOf(item, evidence.side, evidence.text);
118
+ if (at !== undefined)
119
+ fact.at = at;
120
+ facts.push(fact);
121
+ }
122
+ }
123
+ const landing = landingOf(item);
124
+ const hunk = {
125
+ id: item.id,
126
+ file: item.file,
127
+ header: item.header,
128
+ added: item.added,
129
+ removed: item.removed,
130
+ line: landing.line,
131
+ side: landing.side,
132
+ status: item.status,
133
+ facts,
134
+ questions: questionsByUnit.get(item.id) ?? [],
135
+ };
136
+ const note = noteOf(item);
137
+ if (note !== undefined)
138
+ hunk.note = note;
139
+ return hunk;
140
+ });
141
+ const answered = report.questions.filter((question) => question.answer !== undefined).length;
142
+ const analysis = {
143
+ available: true,
144
+ scope,
145
+ snapshotId,
146
+ reviewId,
147
+ reportUrl,
148
+ counts,
149
+ // The hunk list already covers "read the remaining changes in <file>" entries.
150
+ agenda: (report.evidence?.agenda ?? [])
151
+ .filter((entry) => !entry.id.startsWith("agenda:hunks:"))
152
+ .slice(0, CONNECTED_AGENDA_LIMIT)
153
+ .map((entry) => ({ title: entry.title, reason: entry.reason })),
154
+ order: report.agentOrder === undefined ? { source: "diffninja" } : { source: "agent", orderedBy: report.agentOrder.orderedBy },
155
+ hunks,
156
+ questions: { total: report.questions.length, answered },
157
+ callFlowFiles: callFlowFilesOf(report),
158
+ };
159
+ if (report.agentComments !== undefined) {
160
+ analysis.suggestions = { suggestedBy: report.agentComments.suggestedBy, comments: report.agentComments.comments };
161
+ }
162
+ return analysis;
163
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The one page the connected session serves.
3
+ *
4
+ * The page owns no data: it renders whatever `GET /api/state` returns, posts a
5
+ * draft to `/api/preview`, and posts the same payload to `/api/submit`. Every
6
+ * string that comes from GitHub (paths, diff lines, the diff text itself, error
7
+ * messages) reaches the DOM through `textContent`; nothing is interpolated into
8
+ * markup or into the script. Style and script carry the session CSRF token as
9
+ * their CSP nonce, so the server can serve a `default-src 'none'` policy with
10
+ * no inline handlers and no inline styles.
11
+ *
12
+ * Drafts live in `sessionStorage`, keyed per pull request and stamped with the
13
+ * snapshot id. Restoring a draft revalidates every comment anchor against the
14
+ * snapshot on screen: a comment whose line no longer exists in the new revision
15
+ * is dropped, never silently carried over.
16
+ */
17
+ export declare function renderConnectedPage(csrf: string): string;