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,521 @@
1
+ import { escapeHtml } from "./escape-html.js";
2
+ /**
3
+ * The opening view of a review report: what the pull request says it does, what
4
+ * the gathered evidence actually established, the short reading agenda, and the
5
+ * automatic findings with the source behind them.
6
+ *
7
+ * Every string is a fixed template or escaped report data. The pull request
8
+ * title, description and evidence text are untrusted input: they are escaped
9
+ * into text nodes and never reach the page script. No numeric priority, model
10
+ * probability or judgment value is printed here, and no check is described as
11
+ * passing: a check states the scope it ran over and where its certainty ends,
12
+ * and a finding carries its own limitation.
13
+ *
14
+ * The order is the reading order: outcome first, then the checks that ran and
15
+ * what they did not cover, then the agenda, then the findings the agenda points
16
+ * at. One piece of evidence is rendered once — an excerpt a finding already
17
+ * carries is linked from the agenda instead of repeated, and a definition cited
18
+ * by two agenda entries is shown in the first and linked from the second.
19
+ */
20
+ export function renderBrief(report) {
21
+ const evidence = report.evidence;
22
+ const ranks = hunkRanks(report.items);
23
+ const claimed = new Set();
24
+ // Finding id → the card that carries it, so an agenda entry links to the
25
+ // finding it cites instead of repeating its evidence.
26
+ const findingsAt = new Map();
27
+ if (evidence !== undefined) {
28
+ for (const [index, finding] of evidence.findings.entries()) {
29
+ findingsAt.set(finding.id, index + 1);
30
+ for (const excerpt of finding.evidence)
31
+ claimed.add(excerptKey(excerpt));
32
+ }
33
+ }
34
+ return [
35
+ '<h2 id="brief-outcome">Expected outcome</h2>',
36
+ '<nav class="ev-links" aria-label="Evidence navigation"><a href="#brief-agenda-h">Read these first</a> · <a href="#brief-findings-h">Automatic findings</a> · <a href="#view-diff" data-view="diff">All hunks</a></nav>',
37
+ report.pr === undefined
38
+ ? '<p class="ev-note">No pull request metadata was recorded for this report.</p>'
39
+ : renderPullRequest(report.pr),
40
+ evidence === undefined
41
+ ? '<p class="ev-note">No intent cross-check was recorded for this report.</p>'
42
+ : renderIntent(evidence.intent, ranks),
43
+ report.project === undefined ? "" : renderProject(report.project),
44
+ evidence === undefined ? "" : renderChecks(evidence.checks),
45
+ evidence === undefined ? "" : renderAgenda(evidence.agenda, ranks, claimed, findingsAt),
46
+ evidence === undefined ? "" : renderFindings(evidence.findings, ranks),
47
+ ]
48
+ .filter((part) => part !== "")
49
+ .join("\n");
50
+ }
51
+ /**
52
+ * Repository context the diff does not show: related reverts, contributor
53
+ * guidelines, and sibling-file conventions. Commit subjects and paths are
54
+ * repository text, printed escaped; they are pointers to read, not verdicts.
55
+ */
56
+ function renderProject(project) {
57
+ const rows = [];
58
+ for (const revert of project.reverts) {
59
+ const why = revert.reason.kind === "file" ? `touched ${revert.reason.file}` : `shares the word “${revert.reason.term}”`;
60
+ rows.push(`<li><span class="mono">${escapeHtml(revert.commit)}</span> ${escapeHtml(revert.date)} ${escapeHtml(revert.subject)} <span class="ev-note">(revert; ${escapeHtml(why)})</span></li>`);
61
+ }
62
+ for (const path of project.guidelines)
63
+ rows.push(`<li>Guideline: <span class="mono">${escapeHtml(path)}</span></li>`);
64
+ for (const convention of project.conventions) {
65
+ const names = convention.common.map((entry) => `${entry.name} (${entry.peers}/${convention.peers})`).join(", ");
66
+ rows.push(`<li>New <span class="mono">${escapeHtml(convention.file)}</span> uses none of what most <span class="mono">${escapeHtml(convention.pattern)}</span> files use: <span class="mono">${escapeHtml(names)}</span></li>`);
67
+ }
68
+ const shallow = project.history === "shallow"
69
+ ? '<p class="ev-note">This clone is shallow: line history and reverts before its boundary are missing.</p>'
70
+ : "";
71
+ return [
72
+ '<h2 id="brief-project">Project context</h2>',
73
+ shallow,
74
+ rows.length === 0 ? '<p class="ev-note">No related reverts, guidelines, or sibling conventions were found.</p>' : `<ul>${rows.join("")}</ul>`,
75
+ ].filter((part) => part !== "").join("\n");
76
+ }
77
+ /**
78
+ * The only external URL the report will ever emit: a github.com pull request
79
+ * link. The URL arrives from whatever API caller produced the report, so it is
80
+ * validated against this exact shape before it becomes an `href`; anything else
81
+ * is printed as escaped text, so a `javascript:`, `data:`, userinfo or
82
+ * off-host URL cannot become a link even if the report is opened locally.
83
+ */
84
+ const PULL_REQUEST_URL = /^https:\/\/github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/pull\/\d+$/;
85
+ /** The exact pull request metadata, as untrusted text under its own labels. */
86
+ function renderPullRequest(pr) {
87
+ const range = pr.baseRef === undefined || pr.headRef === undefined
88
+ ? ""
89
+ : `<dt>Range</dt><dd class="mono">${escapeHtml(`${pr.baseRef} → ${pr.headRef}`)}</dd>`;
90
+ const url = pr.url ?? "";
91
+ const source = url === ""
92
+ ? ""
93
+ : `<dt>Source</dt><dd class="mono">${PULL_REQUEST_URL.test(url)
94
+ ? `<a href="${escapeHtml(url)}" rel="noreferrer noopener">${escapeHtml(url)}</a>`
95
+ : escapeHtml(url)}</dd>`;
96
+ return [
97
+ '<h3 id="brief-pr">Pull request</h3>',
98
+ '<dl class="pr-facts">',
99
+ '<dt>Title</dt>',
100
+ `<dd class="pr-title">${pr.title === "" ? '<span class="muted">The pull request has no title.</span>' : escapeHtml(pr.title)}</dd>`,
101
+ source,
102
+ range,
103
+ "</dl>",
104
+ pr.body === ""
105
+ ? '<p class="ev-note">The pull request has no description. Nothing in it states an expected outcome.</p>'
106
+ : [
107
+ '<details class="pr-body">',
108
+ '<summary>PR description — untrusted claims, including any generated notes</summary>',
109
+ `<pre class="pr-text">${escapeHtml(pr.body)}</pre>`,
110
+ "</details>",
111
+ ].join("\n"),
112
+ ].join("\n");
113
+ }
114
+ /** Verdict, the statements it is about, and the requirements nothing answered. */
115
+ function renderIntent(intent, ranks) {
116
+ const verdict = VERDICT[intent.verdict];
117
+ const claims = intent.claims.map((claim) => renderClaim(claim, ranks)).join("\n");
118
+ return [
119
+ '<h3 id="brief-intent-h">Intent cross-check</h3>',
120
+ `<div class="intent intent-${verdict.tone}">`,
121
+ `<p class="intent-verdict">${badge(verdict.label, verdict.tone, verdict.hint)}</p>`,
122
+ `<p class="intent-summary">${escapeHtml(intent.summary)}</p>`,
123
+ claims === "" ? '<p class="ev-note">No intent statements were extracted; inspect the exact description above.</p>' : `<details class="intent-claims"><summary>Compare ${intent.claims.length} intent statements with changed code</summary><ul class="claims">${claims}</ul></details>`,
124
+ renderObligations(intent.obligations),
125
+ "</div>",
126
+ ]
127
+ .filter((part) => part !== "")
128
+ .join("\n");
129
+ }
130
+ function renderClaim(claim, ranks) {
131
+ const status = CLAIM_STATUS[claim.status];
132
+ const links = claim.unitIds.length === 0 ? "" : `<p class="claim-links">${hunkLinks(claim.unitIds, "", ranks)}</p>`;
133
+ return [
134
+ '<li class="claim">',
135
+ '<p class="claim-head">',
136
+ `<span class="claim-origin">${escapeHtml(ORIGIN_LABEL[claim.origin])}</span>`,
137
+ badge(status.label, status.tone, status.hint),
138
+ "</p>",
139
+ `<p class="claim-text">${escapeHtml(claim.text)}</p>`,
140
+ claim.explanation === "" ? "" : `<p class="claim-note">${escapeHtml(claim.explanation)}</p>`,
141
+ links,
142
+ "</li>",
143
+ ]
144
+ .filter((part) => part !== "")
145
+ .join("");
146
+ }
147
+ /** Review decisions and proof obligations still open after static inspection. */
148
+ function renderObligations(obligations) {
149
+ if (obligations.length === 0)
150
+ return "";
151
+ return [
152
+ '<details class="obligations">',
153
+ `<summary>${obligations.length} open review decision${obligations.length === 1 ? "" : "s"} and proof obligations</summary>`,
154
+ `<ul>${obligations.map((text) => `<li>${escapeHtml(text)}</li>`).join("")}</ul>`,
155
+ "</details>",
156
+ ].join("");
157
+ }
158
+ /** What each check ran over, in the words of the check itself. */
159
+ function renderChecks(checks) {
160
+ const rows = checks
161
+ .map((check) => {
162
+ const status = CHECK_STATUS[check.status];
163
+ return [
164
+ '<li class="check">',
165
+ '<details><summary class="check-head">',
166
+ `<span class="check-kind">${escapeHtml(KIND_LABEL[check.kind])}</span>`,
167
+ badge(status.label, status.tone, status.hint),
168
+ "</summary>",
169
+ `<p class="check-detail">${escapeHtml(check.detail)}</p>`,
170
+ "</details></li>",
171
+ ].join("");
172
+ })
173
+ .join("\n");
174
+ return [
175
+ '<h3 id="brief-checks-h">Checks and their limits</h3>',
176
+ rows === "" ? '<p class="ev-note">No check reported a result for this pull request.</p>' : `<ul class="checks">${rows}</ul>`,
177
+ // Silence is not a result: a kind that ran is listed above, whatever it found.
178
+ '<p class="ev-note">Each line states the scope its check ran over. A check that is not listed here produced no result for this pull request.</p>',
179
+ ].join("\n");
180
+ }
181
+ /** The reading agenda, in the order the report ranked it. */
182
+ function renderAgenda(entries, ranks, claimed, findingsAt) {
183
+ if (entries.length === 0) {
184
+ return [
185
+ '<h3 id="brief-agenda-h">Read these first</h3>',
186
+ '<p class="ev-note">No reading agenda was produced for this pull request.</p>',
187
+ ].join("\n");
188
+ }
189
+ // A definition two agenda entries cite is shown once and linked after that.
190
+ const contexts = new Map();
191
+ const cards = entries.map((entry, index) => renderAgendaEntry(entry, index + 1, ranks, claimed, findingsAt, contexts));
192
+ return [
193
+ '<h3 id="brief-agenda-h">Read these first</h3>',
194
+ `<ol class="agenda">${cards.slice(0, 5).join("\n")}</ol>`,
195
+ cards.length > 5 ? `<details class="supporting-agenda"><summary>${cards.length - 5} more review entries and supporting changes</summary><ol class="agenda" start="6">${cards.slice(5).join("\n")}</ol></details>` : "",
196
+ '<p class="ev-note">Evidence cards are places to read — changed code, direct caller, contract, related code or test — not results.</p>',
197
+ ].join("\n");
198
+ }
199
+ function renderAgendaEntry(entry, at, ranks, claimed, findingsAt, contexts) {
200
+ // One excerpt is rendered once; what a finding already carries is linked.
201
+ const shown = entry.evidence.filter((excerpt) => !claimed.has(excerptKey(excerpt)));
202
+ const file = entry.evidence[0]?.file ?? "";
203
+ const links = entry.unitIds.length === 0 ? "" : `<p class="ag-links">${hunkLinks(entry.unitIds, file, ranks)}</p>`;
204
+ const findings = entry.findingIds
205
+ .map((id) => {
206
+ const where = findingsAt.get(id);
207
+ return where === undefined ? "" : `<a class="ag-finding" href="#finding-${where}">Finding ${where}</a>`;
208
+ })
209
+ .filter((link) => link !== "")
210
+ .join("");
211
+ const context = entry.context.map((node) => renderContextNode(node, contexts)).filter((part) => part !== "").join("");
212
+ const linked = linkedRanks(entry.unitIds, file, ranks);
213
+ const evidence = shown.map((excerpt) => renderEvidence(excerpt, ranks, linked)).join("");
214
+ return [
215
+ `<li class="ag-card" id="agenda-${at}">`,
216
+ '<p class="ag-head">',
217
+ `<span class="ag-rank mono" aria-hidden="true">${at}</span>`,
218
+ `<span class="ag-title">${escapeHtml(entry.title)}</span>`,
219
+ findings,
220
+ "</p>",
221
+ entry.reason === "" ? "" : `<p class="ag-reason">${escapeHtml(entry.reason)}</p>`,
222
+ links,
223
+ context === "" ? "" : `<div class="ag-ctx-list">${context}</div>`,
224
+ evidence === "" ? "" : `<ul class="evidence">${evidence}</ul>`,
225
+ "</li>",
226
+ ]
227
+ .filter((part) => part !== "")
228
+ .join("");
229
+ }
230
+ /** One definition an agenda entry reads: shown once, linked when cited again. */
231
+ function renderContextNode(node, seen) {
232
+ const key = `${node.key}\u0000${node.file}\u0000${node.line}`;
233
+ const shown = seen.get(key);
234
+ if (shown !== undefined) {
235
+ return `<a class="ag-ctx-again" href="#ctx-${shown}">${escapeHtml(locationOf(node.file, node.line))} shown above</a>`;
236
+ }
237
+ const at = seen.size + 1;
238
+ seen.set(key, at);
239
+ return [
240
+ `<details class="ag-ctx" id="ctx-${at}">`,
241
+ `<summary class="ag-ctx-sum"><span class="mono">${escapeHtml(locationOf(node.file, node.line))}</span> ${escapeHtml(node.label)}</summary>`,
242
+ node.detail === ""
243
+ ? '<p class="ev-note">No text was captured for this definition.</p>'
244
+ : `<pre class="ev-code ag-ctx-code">${escapeHtml(node.detail)}</pre>`,
245
+ "</details>",
246
+ ].join("");
247
+ }
248
+ /** The automatic findings, each with the scope it ran over and its own limit. */
249
+ function renderFindings(findings, ranks) {
250
+ if (findings.length === 0) {
251
+ return [
252
+ '<h3 id="brief-findings-h">Automatic findings</h3>',
253
+ '<p class="ev-note">No automatic finding was reported. The checks above state the scope that ran, and an empty list is not a statement about the code.</p>',
254
+ ].join("\n");
255
+ }
256
+ const rendered = new Set();
257
+ const cards = findings
258
+ .map((finding, index) => {
259
+ const linked = linkedRanks(finding.unitIds, finding.evidence[0]?.file ?? "", ranks);
260
+ const evidence = finding.evidence
261
+ .filter((excerpt) => {
262
+ const key = excerptKey(excerpt);
263
+ if (rendered.has(key))
264
+ return false;
265
+ rendered.add(key);
266
+ return true;
267
+ })
268
+ .map((excerpt) => renderEvidence(excerpt, ranks, linked))
269
+ .join("");
270
+ return [
271
+ `<li class="finding" id="finding-${index + 1}">`,
272
+ '<p class="finding-head">',
273
+ `<span class="finding-kind">${escapeHtml(KIND_LABEL[finding.kind])}</span>`,
274
+ `<span class="finding-title">${escapeHtml(finding.title)}</span>`,
275
+ "</p>",
276
+ `<p class="finding-scope"><span class="finding-label">Checked</span> ${escapeHtml(finding.scope)}</p>`,
277
+ `<p class="finding-limit"><span class="finding-label">Limit</span> ${escapeHtml(finding.limitation)}</p>`,
278
+ finding.unitIds.length === 0 ? "" : `<p class="finding-hunks">${hunkLinks(finding.unitIds, finding.evidence[0]?.file ?? "", ranks)}</p>`,
279
+ evidence === "" ? "" : `<ul class="evidence">${evidence}</ul>`,
280
+ "</li>",
281
+ ]
282
+ .filter((part) => part !== "")
283
+ .join("");
284
+ })
285
+ .join("\n");
286
+ return ['<h3 id="brief-findings-h">Automatic findings</h3>', `<ul class="findings">${cards}</ul>`].join("\n");
287
+ }
288
+ /** One change/caller/contract card: where it is, how to reach it, and its text. */
289
+ function renderEvidence(excerpt, ranks, linked) {
290
+ return [
291
+ '<li class="ev-card"><details>',
292
+ '<summary class="ev-head">',
293
+ `<span class="ev-role">${escapeHtml(ROLE_LABEL[excerpt.role])}</span>`,
294
+ `<span class="ev-where mono">${escapeHtml(locationOf(excerpt.file, excerpt.line, excerpt.endLine))}</span>`,
295
+ excerpt.ref === "" ? "" : `<span class="ev-ref mono">from ${escapeHtml(/^[0-9a-f]{40,64}$/iu.test(excerpt.ref) ? excerpt.ref.slice(0, 12) : excerpt.ref)}</span>`,
296
+ hunkLinks([], excerpt.file, ranks, linked),
297
+ "</summary>",
298
+ excerpt.label === "" ? "" : `<p class="ev-label">${escapeHtml(excerpt.label)}</p>`,
299
+ excerpt.text === ""
300
+ ? '<p class="ev-note">No source text was captured for this excerpt.</p>'
301
+ : `<pre class="ev-code">${escapeHtml(excerpt.text)}</pre>`,
302
+ "</details></li>",
303
+ ]
304
+ .filter((part) => part !== "")
305
+ .join("");
306
+ }
307
+ function hunkRanks(items) {
308
+ const unit = new Map();
309
+ const file = new Map();
310
+ for (const [index, item] of items.entries()) {
311
+ const rank = index + 1;
312
+ if (!unit.has(item.id))
313
+ unit.set(item.id, rank);
314
+ if (!file.has(item.file))
315
+ file.set(item.file, rank);
316
+ }
317
+ return { unit, file };
318
+ }
319
+ /**
320
+ * The report ranks a card's links resolve to, in order: the hunks it names by
321
+ * unit id, or the first hunk of its file when it names none. A rank another link
322
+ * on the same card already points at is left out, so one card never offers the
323
+ * same hunk twice.
324
+ */
325
+ function linksToRanks(unitIds, file, ranks, skip = new Set()) {
326
+ const found = [];
327
+ for (const id of unitIds) {
328
+ const rank = ranks.unit.get(id);
329
+ if (rank !== undefined && !found.includes(rank))
330
+ found.push(rank);
331
+ }
332
+ if (found.length === 0) {
333
+ const rank = ranks.file.get(file);
334
+ if (rank !== undefined)
335
+ found.push(rank);
336
+ }
337
+ return found.filter((rank) => !skip.has(rank));
338
+ }
339
+ /**
340
+ * Links that open a hunk in the diff. The link carries a flag the page script
341
+ * acts on: it clears any hiding filter and focus mode first, so a hunk the
342
+ * current view hides is still reachable.
343
+ */
344
+ function hunkLinks(unitIds, file, ranks, skip = new Set()) {
345
+ return linksToRanks(unitIds, file, ranks, skip)
346
+ .map((rank) => `<a class="ev-hunk" data-open-hunk href="#item-${rank}">Open hunk #${rank}</a>`)
347
+ .join(" ");
348
+ }
349
+ /** The ranks a card's own hunk links resolve to, for the evidence inside it. */
350
+ function linkedRanks(unitIds, file, ranks) {
351
+ return new Set(linksToRanks(unitIds, file, ranks));
352
+ }
353
+ /** Evidence is identified by its own id, or by where it was read from. */
354
+ function excerptKey(excerpt) {
355
+ return excerpt.id === "" ? `${excerpt.file}:${excerpt.line}:${excerpt.ref}` : excerpt.id;
356
+ }
357
+ function locationOf(file, line, endLine) {
358
+ if (!Number.isFinite(line) || line <= 0)
359
+ return file;
360
+ if (endLine === undefined || !Number.isFinite(endLine) || endLine <= line)
361
+ return `${file}:${line}`;
362
+ return `${file}:${line}-${endLine}`;
363
+ }
364
+ /** One status word, with the reason for it as the tooltip and no color alone. */
365
+ function badge(label, tone, hint) {
366
+ return `<span class="ev-badge ev-badge-${tone}" title="${escapeHtml(hint)}">${escapeHtml(label)}</span>`;
367
+ }
368
+ const ROLE_LABEL = {
369
+ change: "Changed code",
370
+ caller: "Direct caller",
371
+ contract: "Contract",
372
+ related: "Related code",
373
+ test: "Test",
374
+ };
375
+ /** Kind names, shared by a check's coverage line and the finding it produces. */
376
+ const KIND_LABEL = {
377
+ "unused-error-result": "Unused failure result",
378
+ "duplicate-body": "Duplicate function body",
379
+ "broken-reference": "Broken reference",
380
+ };
381
+ const CHECK_STATUS = {
382
+ checked: { label: "Checked", tone: "done", hint: "the check ran over the whole change" },
383
+ partial: { label: "Partly checked", tone: "warn", hint: "the check ran over part of the change" },
384
+ "not-checked": { label: "Not checked", tone: "warn", hint: "no trusted result is available" },
385
+ };
386
+ const VERDICT = {
387
+ "supported-within-checked-scope": {
388
+ label: "Supported within the checked scope",
389
+ tone: "done",
390
+ hint: "the changed code carries the statement, within the scope the checks covered",
391
+ },
392
+ "not-established": {
393
+ label: "Not established",
394
+ tone: "warn",
395
+ hint: "the gathered evidence neither carries nor contradicts the statement",
396
+ },
397
+ contradicted: {
398
+ label: "Contradicted by a check",
399
+ tone: "warn",
400
+ hint: "a check reports the opposite of the statement",
401
+ },
402
+ };
403
+ const CLAIM_STATUS = {
404
+ "evidence-linked": {
405
+ label: "Evidence linked",
406
+ tone: "done",
407
+ hint: "changed code carries this statement",
408
+ },
409
+ "not-established": {
410
+ label: "Not established",
411
+ tone: "warn",
412
+ hint: "no changed code was linked to this statement",
413
+ },
414
+ };
415
+ const ORIGIN_LABEL = {
416
+ title: "PR title",
417
+ author: "Author description",
418
+ "generated-summary": "Generated summary",
419
+ };
420
+ export const BRIEF_STYLES = `
421
+ .brief { display: flex; flex-direction: column; gap: 7px; }
422
+ .brief > h2 { font-size: 13.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); margin-top: 7px; }
423
+ .brief > h2:first-child { margin-top: 0; }
424
+ .brief h3 { font-size: 13px; color: var(--ink-soft); font-weight: 600; }
425
+ .pr-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; }
426
+ .pr-facts dt { font-size: 12px; color: var(--ink-soft); }
427
+ .pr-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
428
+ .pr-title { font-size: 17px; font-weight: 700; }
429
+ .pr-body > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }
430
+ .pr-text {
431
+ margin: 6px 0 0;
432
+ padding: 9px 11px;
433
+ border: 1px solid var(--line);
434
+ border-radius: 6px;
435
+ background: var(--sunken);
436
+ font-family: var(--mono);
437
+ font-size: 12.5px;
438
+ line-height: 1.5;
439
+ white-space: pre-wrap;
440
+ overflow-wrap: anywhere;
441
+ max-height: 40vh;
442
+ overflow: auto;
443
+ }
444
+ .intent { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 10px 14px; background: var(--panel); }
445
+ .intent-done { border-left-color: var(--teal); }
446
+ .intent-warn { border-left-color: var(--warn); }
447
+ .intent-summary { font-size: 14.5px; overflow-wrap: anywhere; }
448
+ .intent-verdict { margin-bottom: 4px; }
449
+ .ev-badge {
450
+ display: inline-flex;
451
+ align-items: center;
452
+ gap: 6px;
453
+ border: 1px solid var(--line-strong);
454
+ border-radius: 999px;
455
+ padding: 2px 10px;
456
+ font-size: 11.5px;
457
+ font-weight: 600;
458
+ white-space: nowrap;
459
+ }
460
+ .ev-badge-done { border-color: var(--teal); color: var(--teal); background: var(--teal-bg); }
461
+ .ev-badge-warn { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
462
+ .claims, .checks, .findings, .evidence { list-style: none; margin: 5px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
463
+ .claim, .check, .finding, .ev-card { border: 1px solid var(--line); border-radius: 6px; padding: 6px 9px; background: var(--panel); }
464
+ .claim-head, .check-head, .finding-head, .ev-head, .ag-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
465
+ summary.ev-head, summary.check-head { cursor: pointer; }
466
+ summary.ev-head::before, summary.check-head::before { content: "+"; font-family: var(--mono); }
467
+ details[open] > summary.ev-head::before, details[open] > summary.check-head::before { content: "-"; font-family: var(--mono); }
468
+ .claim-origin, .ev-role, .finding-kind, .check-kind { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
469
+ .claim-text, .ag-title, .finding-title { font-size: 14.5px; font-weight: 600; overflow-wrap: anywhere; }
470
+ .claim-note, .ag-reason { font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; }
471
+ .check-detail, .finding-scope, .finding-limit, .ev-label, .ev-note, .ag-links, .finding-hunks, .claim-links {
472
+ font-size: 12.5px;
473
+ color: var(--ink-soft);
474
+ overflow-wrap: anywhere;
475
+ margin-top: 3px;
476
+ }
477
+ .finding-label { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
478
+ .ev-where, .ev-ref { font-size: 12px; color: var(--ink-soft); overflow-wrap: anywhere; }
479
+ .ev-card { background: var(--sunken); }
480
+ .ev-code {
481
+ margin: 6px 0 0;
482
+ padding: 7px 9px;
483
+ border: 1px solid var(--line);
484
+ border-radius: 6px;
485
+ background: var(--panel);
486
+ font-family: var(--mono);
487
+ font-size: 12px;
488
+ line-height: 1.5;
489
+ max-height: 26vh;
490
+ overflow: auto;
491
+ tab-size: 4;
492
+ }
493
+ .ev-hunk { font-size: 12px; white-space: nowrap; }
494
+ .agenda { list-style: none; counter-reset: agenda; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
495
+ .ag-card { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 9px 12px; background: var(--panel); }
496
+ .ag-rank {
497
+ display: inline-flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ min-width: 20px;
501
+ height: 20px;
502
+ border-radius: 999px;
503
+ background: var(--sunken);
504
+ border: 1px solid var(--line);
505
+ font-size: 11.5px;
506
+ font-weight: 700;
507
+ color: var(--ink-soft);
508
+ }
509
+ .ag-ctx-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
510
+ .ag-ctx > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
511
+ .ag-ctx { border: 1px dashed var(--line-strong); border-radius: 6px; padding: 6px 9px; }
512
+ .ag-ctx-again { font-size: 12.5px; }
513
+ .obligations { margin-top: 8px; }
514
+ .obligations > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }
515
+ .obligations ul { margin: 6px 0 0; padding-left: 20px; font-size: 13px; }
516
+ .obligations li { overflow-wrap: anywhere; }
517
+ @media (max-width: 680px) {
518
+ .pr-facts { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
519
+ .pr-facts dd { margin-bottom: 6px; }
520
+ }
521
+ `;
@@ -0,0 +1,132 @@
1
+ import { type SyntaxNode, type Tree } from "../languages/types.js";
2
+ /** A fragment parsed without syntax errors, and the row shift a wrapper added. */
3
+ export interface FragmentParse {
4
+ tree: Tree;
5
+ /**
6
+ * Rows of the parse above the fragment's first line: 1 after the container
7
+ * wrapper, 0 for the text as written. Callers convert a node row to a source
8
+ * line by subtracting this from the row and adding the definition's line.
9
+ */
10
+ lineOffset: number;
11
+ }
12
+ /**
13
+ * Parse one definition fragment, trying the text as written first and then the
14
+ * two wrappers above. The first attempt whose whole tree is error-free wins, so
15
+ * the same fragment always yields the same tree.
16
+ */
17
+ export declare function parseFragment(file: string, text: string): FragmentParse | null;
18
+ /** Imports in a module prefix; an unfinished following class does not invalidate preceding imports. */
19
+ export declare function moduleImports(file: string, prefix: string): Map<string, string> | null;
20
+ /** A function-like node and the body its grammar gives it. */
21
+ export interface DefinitionSyntax {
22
+ definition: SyntaxNode;
23
+ body: SyntaxNode;
24
+ }
25
+ /**
26
+ * The definition's own body. The fragment holds either the definition itself or
27
+ * one wrapper (a class the member was written in, or a binding the function
28
+ * value was assigned to), so the outermost node with a callable body is the
29
+ * definition being read. Breadth-first order picks the shallowest one, which
30
+ * keeps a large nested callback inside a default parameter from being taken for
31
+ * the definition's body, and ties are broken by position, so the choice is
32
+ * stable.
33
+ */
34
+ export declare function definitionSyntax(fragment: FragmentParse): DefinitionSyntax | null;
35
+ /**
36
+ * Comment-free token stream of a body: leaf text in source order, one space
37
+ * between tokens. Formatting and comments are gone; every literal and operator
38
+ * is kept exactly as written, so two bodies match only when their tokens do.
39
+ */
40
+ export declare function bodyTokenSignature(body: SyntaxNode): string;
41
+ /** Declared response type name of a definition's return annotation, if any. */
42
+ export declare function returnedResponseType(definition: SyntaxNode): string | null;
43
+ /** One field declared in an interface or type-alias object. */
44
+ export interface ContractField {
45
+ name: string;
46
+ type: string;
47
+ }
48
+ /**
49
+ * The declared response type name of a type annotation, or null when the
50
+ * annotation declares something else. Only a plain name (`CreateResult`) or a
51
+ * known async/readonly wrapper of one (`Promise<CreateResult>`) is a response:
52
+ * an array of them (`CreateResult[]`), an arbitrary generic
53
+ * (`Wrapper<CreateResult>`), a union, or a function type is a different thing,
54
+ * and this check must not treat it as holding one response.
55
+ */
56
+ export declare function declaredResponseType(annotation: SyntaxNode | null | undefined, awaited?: boolean): string | null;
57
+ /**
58
+ * Declared fields of the named interface or type alias in a fragment, or null
59
+ * when that declaration is not the one the fragment holds. The name is required
60
+ * because one line can carry several declarations: a fragment read for `B` that
61
+ * holds both `A` and `B` must never report `A`'s fields as `B`'s. `extends` and
62
+ * intersections are not followed, so this is only ever the fields written in the
63
+ * declaration itself; an empty array means it writes no field, which is a
64
+ * different answer from a declaration nobody could read.
65
+ */
66
+ export declare function contractFields(fragment: FragmentParse, name: string): ContractField[] | null;
67
+ /** A name a response value is bound to, and where the binding was written. */
68
+ export interface TypedBinding {
69
+ name: string;
70
+ /** 0-based row inside the parsed fragment. */
71
+ row: number;
72
+ }
73
+ /**
74
+ * Names declared with a type annotation that names `typeName`: a parameter, a
75
+ * local, or a class field. Only a plainly named binding is returned; a
76
+ * destructuring pattern is left out, because its fields are read positions
77
+ * rather than one value the checks can follow.
78
+ */
79
+ export declare function typedBindings(definition: SyntaxNode, typeName: string): TypedBinding[];
80
+ /** Every node in the fragment, in pre-order. */
81
+ export declare function walkSyntax(root: SyntaxNode, visit: (node: SyntaxNode) => void): void;
82
+ /**
83
+ * The value one expression counts, when the expression is a count-named read.
84
+ * `users.length` counts the binding `users`; `result.users.length` counts the
85
+ * field `users` read off the binding `result`. Both shapes report a number, and
86
+ * nothing else does: a plain `.length` on a call or an element access is not
87
+ * attributed to a binding, because the checks only follow named values.
88
+ */
89
+ export interface CountRead {
90
+ /** Expression that reports the count, e.g. `result.users.length`. */
91
+ read: SyntaxNode;
92
+ /** Name of the value counted, e.g. `users`. */
93
+ value: string;
94
+ /** Whether the value is a field read off another name rather than a binding. */
95
+ ofField: boolean;
96
+ }
97
+ export declare function countReadOf(node: SyntaxNode): CountRead | null;
98
+ /**
99
+ * First count-named read inside a `return`, which is the number a receiver hands
100
+ * back to its own caller. That is the count a partial-failure question is about,
101
+ * more than any diagnostic log written after it.
102
+ */
103
+ export declare function returnedCountRead(definition: SyntaxNode): CountRead | null;
104
+ /**
105
+ * State/status properties in a call payload. A queue routing constant or a
106
+ * state-looking word inside a string is not by itself a state assignment.
107
+ */
108
+ export declare function stateWritesAt(call: SyntaxNode): string[];
109
+ /** Call expressions written in one definition, in source order. */
110
+ export declare function callsIn(definition: SyntaxNode): SyntaxNode[];
111
+ /**
112
+ * Count-named reads in a body, in source order: the numbers a receiver reports,
113
+ * which are what a partial-failure question compares with the failures it never
114
+ * read.
115
+ */
116
+ export declare function countReads(definition: SyntaxNode): CountRead[];
117
+ /**
118
+ * Whether two node handles denote the same syntax node. Tree-sitter hands back
119
+ * a fresh wrapper per field access, so `===` on handles is not a stable test:
120
+ * two handles for one node can differ between calls. Node ids are stable within
121
+ * one tree, which is what every structural question here compares.
122
+ */
123
+ export declare function sameNode(left: SyntaxNode | null | undefined, right: SyntaxNode | null | undefined): boolean;
124
+ /**
125
+ * Names one definition declares for itself: its parameters, its locals, and any
126
+ * nested declaration. A call written under one of these names reaches the local
127
+ * value, not whatever definition elsewhere carries the same bare key, so a
128
+ * caller-side check must not resolve through the shadow.
129
+ */
130
+ export declare function declaredValueNames(definition: SyntaxNode): Set<string>;
131
+ /** Value of a string literal node, or the raw text of any other node. */
132
+ export declare function staticStringValue(node: SyntaxNode): string;