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,478 @@
1
+ /**
2
+ * Tree-sitter views of one definition's own source, used by the automatic
3
+ * evidence checks.
4
+ *
5
+ * The checks read definitions from snapshot indexes rather than whole files, so
6
+ * every parse starts from a single definition's text. A member definition is
7
+ * not a valid module on its own, so a container and a binding wrapper are
8
+ * retried before a fragment is reported unsupported: a fragment no attempt
9
+ * parses contributes no body, no return type, and no shape, and the caller says
10
+ * which definitions were left out instead of guessing.
11
+ *
12
+ * Every helper here is syntax-only. Nothing resolves a name to a value: a
13
+ * return type, a declared field, or a parameter binding is what the grammar
14
+ * wrote, and call/argument answers stay with the extractor indexes the checks
15
+ * already read.
16
+ */
17
+ import Parser from "tree-sitter";
18
+ import { loadGrammarPackage, resolveLanguage, } from "../languages/grammars.js";
19
+ import { detectLanguage } from "../languages/registry.js";
20
+ import { collapseWs } from "../languages/types.js";
21
+ const parser = new Parser();
22
+ /** One grammar handle, or a confirmed failure that must not be retried. */
23
+ const grammars = new Map();
24
+ function languageFor(file) {
25
+ const extractor = detectLanguage(file);
26
+ if (!extractor)
27
+ return null;
28
+ const key = extractor.grammarExport
29
+ ? `${extractor.grammarPackage}:${extractor.grammarExport}`
30
+ : extractor.grammarPackage;
31
+ const cached = grammars.get(key);
32
+ if (cached !== undefined)
33
+ return cached;
34
+ let language = null;
35
+ try {
36
+ language = resolveLanguage(loadGrammarPackage(extractor.grammarPackage), extractor.grammarExport);
37
+ }
38
+ catch {
39
+ // A grammar that cannot be installed or loaded leaves its definitions
40
+ // unchecked; the checks report them as unsupported rather than failing.
41
+ language = null;
42
+ }
43
+ grammars.set(key, language);
44
+ return language;
45
+ }
46
+ /**
47
+ * Literal text that only parses inside a container, so a class member or a
48
+ * bare function value can be parsed at all. The wrapper names cannot collide
49
+ * with an extracted definition: no definition span contains them.
50
+ */
51
+ const CONTAINER_WRAPPER = (source) => `class __diffninjaEvidenceContainer {\n${source}\n}`;
52
+ const BINDING_WRAPPER = (source) => `const __diffninjaEvidenceBinding = ${source};`;
53
+ /**
54
+ * Parse one definition fragment, trying the text as written first and then the
55
+ * two wrappers above. The first attempt whose whole tree is error-free wins, so
56
+ * the same fragment always yields the same tree.
57
+ */
58
+ export function parseFragment(file, text) {
59
+ const language = languageFor(file);
60
+ if (language === null)
61
+ return null;
62
+ const source = text.trim();
63
+ if (source === "")
64
+ return null;
65
+ const attempts = [
66
+ { source, lineOffset: 0 },
67
+ { source: CONTAINER_WRAPPER(source), lineOffset: 1 },
68
+ { source: BINDING_WRAPPER(source), lineOffset: 0 },
69
+ ];
70
+ for (const attempt of attempts) {
71
+ let tree = null;
72
+ try {
73
+ // @ts-expect-error tree-sitter Language under-specifies grammar module exports
74
+ parser.setLanguage(language);
75
+ tree = parser.parse(attempt.source);
76
+ }
77
+ catch {
78
+ // A grammar that rejects this fragment leaves nothing to read here.
79
+ tree = null;
80
+ }
81
+ if (tree === null || tree.rootNode.hasError)
82
+ continue;
83
+ return { tree, lineOffset: attempt.lineOffset };
84
+ }
85
+ return null;
86
+ }
87
+ /** Imports in a module prefix; an unfinished following class does not invalidate preceding imports. */
88
+ export function moduleImports(file, prefix) {
89
+ const language = languageFor(file);
90
+ if (language === null)
91
+ return null;
92
+ let tree;
93
+ try {
94
+ // @ts-expect-error tree-sitter Language under-specifies grammar module exports
95
+ parser.setLanguage(language);
96
+ tree = parser.parse(prefix);
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ const imports = new Map();
102
+ for (const node of tree.rootNode.namedChildren) {
103
+ if (node.type !== "import_statement" || node.hasError)
104
+ continue;
105
+ const clause = node.namedChildren.find(child => child.type === "import_clause");
106
+ const literal = node.childForFieldName("source");
107
+ const module = literal?.namedChildren.find(child => child.type === "string_fragment")?.text;
108
+ if (!clause || module === undefined)
109
+ continue;
110
+ for (const child of clause.namedChildren) {
111
+ if (child.type === "identifier")
112
+ imports.set(child.text, module);
113
+ if (child.type === "namespace_import") {
114
+ const local = child.namedChildren.find(entry => entry.type === "identifier");
115
+ if (local)
116
+ imports.set(local.text, module);
117
+ }
118
+ if (child.type !== "named_imports")
119
+ continue;
120
+ for (const specifier of child.namedChildren) {
121
+ if (specifier.type !== "import_specifier")
122
+ continue;
123
+ const local = specifier.childForFieldName("alias") ?? specifier.childForFieldName("name");
124
+ if (local)
125
+ imports.set(local.text, module);
126
+ }
127
+ }
128
+ }
129
+ return imports;
130
+ }
131
+ /* ------------------------------------------------------------------ bodies */
132
+ /**
133
+ * Bodies that belong to a container rather than to a definition. `block` is
134
+ * deliberately absent: it is a function body in grammars that have no
135
+ * `statement_block`, and a nested control-flow block is always smaller.
136
+ */
137
+ const CONTAINER_BODIES = {
138
+ class_body: true,
139
+ interface_body: true,
140
+ enum_body: true,
141
+ declaration_list: true,
142
+ field_declaration_list: true,
143
+ struct_body: true,
144
+ trait_body: true,
145
+ impl_body: true,
146
+ namespace_body: true,
147
+ module_body: true,
148
+ object_type: true,
149
+ program: true,
150
+ translation_unit: true,
151
+ source_file: true,
152
+ compilation_unit: true,
153
+ script: true,
154
+ };
155
+ /**
156
+ * The definition's own body. The fragment holds either the definition itself or
157
+ * one wrapper (a class the member was written in, or a binding the function
158
+ * value was assigned to), so the outermost node with a callable body is the
159
+ * definition being read. Breadth-first order picks the shallowest one, which
160
+ * keeps a large nested callback inside a default parameter from being taken for
161
+ * the definition's body, and ties are broken by position, so the choice is
162
+ * stable.
163
+ */
164
+ export function definitionSyntax(fragment) {
165
+ let level = [fragment.tree.rootNode];
166
+ while (level.length > 0) {
167
+ const next = [];
168
+ for (const node of level) {
169
+ const body = node.childForFieldName("body");
170
+ if (body && !Object.hasOwn(CONTAINER_BODIES, body.type))
171
+ return { definition: node, body };
172
+ next.push(...node.children);
173
+ }
174
+ level = next;
175
+ }
176
+ return null;
177
+ }
178
+ /**
179
+ * Comment-free token stream of a body: leaf text in source order, one space
180
+ * between tokens. Formatting and comments are gone; every literal and operator
181
+ * is kept exactly as written, so two bodies match only when their tokens do.
182
+ */
183
+ export function bodyTokenSignature(body) {
184
+ const tokens = [];
185
+ const walk = (node) => {
186
+ if (node.childCount === 0) {
187
+ const type = node.type;
188
+ if (type !== "comment" && !type.endsWith("_comment"))
189
+ tokens.push(node.text);
190
+ return;
191
+ }
192
+ for (const child of node.children)
193
+ walk(child);
194
+ };
195
+ walk(body);
196
+ return tokens.join(" ");
197
+ }
198
+ /* ------------------------------------------------------------- annotations */
199
+ /** Declared response type name of a definition's return annotation, if any. */
200
+ export function returnedResponseType(definition) {
201
+ return declaredResponseType(definition.childForFieldName("return_type"), true);
202
+ }
203
+ /** Declared fields written in one object literal type; empty when there are none. */
204
+ function objectFields(objectNode) {
205
+ const fields = [];
206
+ for (const member of objectNode.namedChildren) {
207
+ if (member.type !== "property_signature")
208
+ continue;
209
+ const name = member.childForFieldName("name");
210
+ const annotation = member.childForFieldName("type");
211
+ if (!name || !annotation)
212
+ continue;
213
+ // A property signature's `type` field is the annotation node, which still
214
+ // carries its colon; the declared type is the node inside it.
215
+ const type = annotation.type === "type_annotation"
216
+ ? annotation.namedChildren[0] ?? annotation
217
+ : annotation;
218
+ fields.push({ name: name.text, type: collapseWs(type.text) });
219
+ }
220
+ return fields;
221
+ }
222
+ /** Wrapper type names whose single type argument is the declared response. */
223
+ const RESPONSE_WRAPPERS = { Promise: true, Awaited: true, Readonly: true };
224
+ /**
225
+ * The declared response type name of a type annotation, or null when the
226
+ * annotation declares something else. Only a plain name (`CreateResult`) or a
227
+ * known async/readonly wrapper of one (`Promise<CreateResult>`) is a response:
228
+ * an array of them (`CreateResult[]`), an arbitrary generic
229
+ * (`Wrapper<CreateResult>`), a union, or a function type is a different thing,
230
+ * and this check must not treat it as holding one response.
231
+ */
232
+ export function declaredResponseType(annotation, awaited = false) {
233
+ if (!annotation)
234
+ return null;
235
+ const inner = annotation.type === "type_annotation" ? annotation.namedChildren[0] : annotation;
236
+ if (!inner)
237
+ return null;
238
+ if (inner.type === "type_identifier" || inner.type === "identifier")
239
+ return inner.text;
240
+ // `Promise<CreateResult>` is the response only where the code awaits it: an
241
+ // async definition's declared return, and a call whose result is awaited.
242
+ // A parameter or local declared as a Promise holds the pending value itself.
243
+ if (inner.type === "generic_type") {
244
+ if (!awaited)
245
+ return null;
246
+ const name = inner.childForFieldName("name");
247
+ const args = inner.childForFieldName("type_arguments");
248
+ if (!name || !args || !Object.hasOwn(RESPONSE_WRAPPERS, name.text))
249
+ return null;
250
+ const parameters = args.namedChildren;
251
+ if (parameters.length !== 1)
252
+ return null;
253
+ return declaredResponseType(parameters[0], true);
254
+ }
255
+ if (inner.type === "type_annotation")
256
+ return declaredResponseType(inner.namedChildren[0], awaited);
257
+ return null;
258
+ }
259
+ /** Name a declaration node declares, or null when it names none. */
260
+ function declarationName(node) {
261
+ return (node.childForFieldName("name")?.text ??
262
+ node.namedChildren.find(child => child.type === "type_identifier")?.text ??
263
+ null);
264
+ }
265
+ /**
266
+ * Declared fields of the named interface or type alias in a fragment, or null
267
+ * when that declaration is not the one the fragment holds. The name is required
268
+ * because one line can carry several declarations: a fragment read for `B` that
269
+ * holds both `A` and `B` must never report `A`'s fields as `B`'s. `extends` and
270
+ * intersections are not followed, so this is only ever the fields written in the
271
+ * declaration itself; an empty array means it writes no field, which is a
272
+ * different answer from a declaration nobody could read.
273
+ */
274
+ export function contractFields(fragment, name) {
275
+ const declarations = [];
276
+ walkSyntax(fragment.tree.rootNode, node => {
277
+ if (node.type === "interface_declaration" || node.type === "type_alias_declaration") {
278
+ declarations.push(node);
279
+ }
280
+ });
281
+ const matches = declarations.filter(declaration => declarationName(declaration) === name);
282
+ if (matches.length !== 1)
283
+ return null;
284
+ const declaration = matches[0];
285
+ if (declaration.type === "interface_declaration") {
286
+ const body = declaration.childForFieldName("body");
287
+ return body ? objectFields(body) : null;
288
+ }
289
+ const value = declaration.childForFieldName("value");
290
+ return value && value.type === "object_type" ? objectFields(value) : null;
291
+ }
292
+ /** Nodes that declare a named value with an explicit type annotation. */
293
+ const TYPED_BINDING_NODES = {
294
+ required_parameter: true,
295
+ optional_parameter: true,
296
+ parameter: true,
297
+ typed_parameter: true,
298
+ variable_declarator: true,
299
+ public_field_definition: true,
300
+ property_declaration: true,
301
+ };
302
+ /**
303
+ * Names declared with a type annotation that names `typeName`: a parameter, a
304
+ * local, or a class field. Only a plainly named binding is returned; a
305
+ * destructuring pattern is left out, because its fields are read positions
306
+ * rather than one value the checks can follow.
307
+ */
308
+ export function typedBindings(definition, typeName) {
309
+ const bindings = [];
310
+ const walk = (node) => {
311
+ if (Object.hasOwn(TYPED_BINDING_NODES, node.type)) {
312
+ const annotation = node.childForFieldName("type");
313
+ if (declaredResponseType(annotation) === typeName) {
314
+ const pattern = node.childForFieldName("pattern") ??
315
+ node.childForFieldName("name") ??
316
+ node.childForFieldName("left");
317
+ if (pattern?.type === "identifier") {
318
+ bindings.push({ name: pattern.text, row: pattern.startPosition.row });
319
+ }
320
+ }
321
+ }
322
+ for (const child of node.children)
323
+ walk(child);
324
+ };
325
+ walk(definition);
326
+ return bindings;
327
+ }
328
+ /** Every node in the fragment, in pre-order. */
329
+ export function walkSyntax(root, visit) {
330
+ visit(root);
331
+ for (const child of root.children)
332
+ walkSyntax(child, visit);
333
+ }
334
+ /** Properties whose read reports how many items a value holds. */
335
+ const COUNT_PROPERTIES = { length: true, size: true, count: true };
336
+ export function countReadOf(node) {
337
+ if (node.type !== "member_expression")
338
+ return null;
339
+ const property = node.childForFieldName("property");
340
+ if (property?.type !== "property_identifier" || !Object.hasOwn(COUNT_PROPERTIES, property.text))
341
+ return null;
342
+ const object = node.childForFieldName("object");
343
+ if (!object)
344
+ return null;
345
+ if (object.type === "identifier")
346
+ return { read: node, value: object.text, ofField: false };
347
+ if (object.type !== "member_expression")
348
+ return null;
349
+ const owner = object.childForFieldName("object");
350
+ const field = object.childForFieldName("property");
351
+ if (owner?.type !== "identifier" || field?.type !== "property_identifier")
352
+ return null;
353
+ return { read: node, value: field.text, ofField: true };
354
+ }
355
+ /**
356
+ * First count-named read inside a `return`, which is the number a receiver hands
357
+ * back to its own caller. That is the count a partial-failure question is about,
358
+ * more than any diagnostic log written after it.
359
+ */
360
+ export function returnedCountRead(definition) {
361
+ let found = null;
362
+ walkSyntax(definition, node => {
363
+ if (found || node.type !== "return_statement")
364
+ return;
365
+ walkSyntax(node, inner => {
366
+ if (found)
367
+ return;
368
+ const count = countReadOf(inner);
369
+ if (count)
370
+ found = count;
371
+ });
372
+ });
373
+ return found;
374
+ }
375
+ /** Whole camel-case words that name a lifecycle or state value, never a part. */
376
+ const STATE_WORDS = { status: true, state: true, stage: true, phase: true, lifecycle: true, mode: true };
377
+ /** Whether one identifier or property name says the value is state. */
378
+ function namesState(text) {
379
+ // `model` must not match `mode`, so words are compared whole at camel
380
+ // boundaries: `leaseStatus` and `status` name state; `model` does not.
381
+ const words = text.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+/g) ?? [text];
382
+ return words.some(word => Object.hasOwn(STATE_WORDS, word.toLowerCase()));
383
+ }
384
+ /**
385
+ * State/status properties in a call payload. A queue routing constant or a
386
+ * state-looking word inside a string is not by itself a state assignment.
387
+ */
388
+ export function stateWritesAt(call) {
389
+ const values = new Set();
390
+ const record = (key, value) => {
391
+ if (!namesState(key.text))
392
+ return;
393
+ values.add(value ? `${key.text}: ${collapseWs(value.text)}` : key.text);
394
+ };
395
+ walkSyntax(call, node => {
396
+ if (node.type !== "pair")
397
+ return;
398
+ record(node.childForFieldName("key") ?? node.namedChildren[0] ?? node, node.childForFieldName("value"));
399
+ });
400
+ return [...values];
401
+ }
402
+ /** Call expressions written in one definition, in source order. */
403
+ export function callsIn(definition) {
404
+ const calls = [];
405
+ walkSyntax(definition, node => {
406
+ if (node.type === "call_expression")
407
+ calls.push(node);
408
+ });
409
+ return calls;
410
+ }
411
+ /**
412
+ * Count-named reads in a body, in source order: the numbers a receiver reports,
413
+ * which are what a partial-failure question compares with the failures it never
414
+ * read.
415
+ */
416
+ export function countReads(definition) {
417
+ const reads = [];
418
+ walkSyntax(definition, node => {
419
+ const count = countReadOf(node);
420
+ if (count)
421
+ reads.push(count);
422
+ });
423
+ return reads;
424
+ }
425
+ /**
426
+ * Whether two node handles denote the same syntax node. Tree-sitter hands back
427
+ * a fresh wrapper per field access, so `===` on handles is not a stable test:
428
+ * two handles for one node can differ between calls. Node ids are stable within
429
+ * one tree, which is what every structural question here compares.
430
+ */
431
+ export function sameNode(left, right) {
432
+ return left !== null && left !== undefined && right !== null && right !== undefined && left.id === right.id;
433
+ }
434
+ /** Node types that declare a name local to one definition's body. */
435
+ const VALUE_DECLARATIONS = {
436
+ variable_declarator: true,
437
+ required_parameter: true,
438
+ optional_parameter: true,
439
+ parameter: true,
440
+ typed_parameter: true,
441
+ rest_pattern: true,
442
+ function_declaration: true,
443
+ class_declaration: true,
444
+ import_specifier: true,
445
+ };
446
+ /**
447
+ * Names one definition declares for itself: its parameters, its locals, and any
448
+ * nested declaration. A call written under one of these names reaches the local
449
+ * value, not whatever definition elsewhere carries the same bare key, so a
450
+ * caller-side check must not resolve through the shadow.
451
+ */
452
+ export function declaredValueNames(definition) {
453
+ const names = new Set();
454
+ walkSyntax(definition, node => {
455
+ if (!Object.hasOwn(VALUE_DECLARATIONS, node.type))
456
+ return;
457
+ const declared = node.childForFieldName("name") ??
458
+ node.childForFieldName("pattern") ??
459
+ node.childForFieldName("left") ??
460
+ node.childForFieldName("declarator");
461
+ if (declared?.type === "identifier")
462
+ names.add(declared.text);
463
+ if (node.type === "rest_pattern") {
464
+ const inner = node.namedChildren.find(child => child.type === "identifier");
465
+ if (inner)
466
+ names.add(inner.text);
467
+ }
468
+ });
469
+ return names;
470
+ }
471
+ /** Value of a string literal node, or the raw text of any other node. */
472
+ export function staticStringValue(node) {
473
+ if (node.type === "string") {
474
+ const fragment = node.namedChildren.find((c) => c.type === "string_fragment");
475
+ return fragment ? fragment.text : "";
476
+ }
477
+ return node.text;
478
+ }
@@ -0,0 +1,62 @@
1
+ import type { ReviewContextNode } from "./types.js";
2
+ /** Source evidence is snapshot-bound; syntactic relationships are not runtime proof. */
3
+ export interface EvidenceExcerpt {
4
+ id: string;
5
+ label: string;
6
+ file: string;
7
+ line: number;
8
+ endLine?: number;
9
+ ref: string;
10
+ text: string;
11
+ role: "change" | "caller" | "contract" | "related" | "test";
12
+ }
13
+ export interface AutomaticFinding {
14
+ id: string;
15
+ kind: "unused-error-result" | "duplicate-body" | "broken-reference";
16
+ title: string;
17
+ scope: string;
18
+ limitation: string;
19
+ unitIds: string[];
20
+ evidence: EvidenceExcerpt[];
21
+ }
22
+ export interface CheckCoverage {
23
+ kind: AutomaticFinding["kind"];
24
+ status: "checked" | "partial" | "not-checked";
25
+ detail: string;
26
+ }
27
+ export interface ReviewAgendaEntry {
28
+ id: string;
29
+ title: string;
30
+ reason: string;
31
+ priority: number;
32
+ unitIds: string[];
33
+ findingIds: string[];
34
+ evidence: EvidenceExcerpt[];
35
+ context: ReviewContextNode[];
36
+ }
37
+ export interface PullRequestIntent {
38
+ title: string;
39
+ body: string;
40
+ url?: string;
41
+ baseRef?: string;
42
+ headRef?: string;
43
+ }
44
+ export interface IntentClaim {
45
+ text: string;
46
+ origin: "title" | "author" | "generated-summary";
47
+ status: "evidence-linked" | "not-established";
48
+ unitIds: string[];
49
+ explanation: string;
50
+ }
51
+ export interface IntentCrossCheck {
52
+ verdict: "not-established" | "contradicted" | "supported-within-checked-scope";
53
+ summary: string;
54
+ claims: IntentClaim[];
55
+ obligations: string[];
56
+ }
57
+ export interface ReviewEvidence {
58
+ findings: AutomaticFinding[];
59
+ checks: CheckCoverage[];
60
+ agenda: ReviewAgendaEntry[];
61
+ intent: IntentCrossCheck;
62
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { type FunctionIndex } from "../extract.js";
2
+ import type { ContextSources } from "./call-context.js";
3
+ import type { AutomaticFinding, CheckCoverage, ReviewAgendaEntry } from "./evidence-types.js";
4
+ import type { ReviewUnit } from "./types.js";
5
+ export interface ReviewEvidenceOptions {
6
+ /** Prior-snapshot index; absent for patch-only input. */
7
+ before?: FunctionIndex;
8
+ /** Resulting-snapshot index; absent for patch-only input. */
9
+ after?: FunctionIndex;
10
+ /** Per-snapshot definition source readers; absent when nothing can be read. */
11
+ sources?: ContextSources;
12
+ /** Provenance label for prior-snapshot evidence, e.g. the base commit. */
13
+ baseRef?: string;
14
+ /** Provenance label for resulting-snapshot evidence, e.g. the head commit. */
15
+ headRef?: string;
16
+ /** Local module binding resolver over the resulting immutable snapshot. */
17
+ resolveImport?: (importer: string, specifier: string) => string | undefined;
18
+ }
19
+ export interface ReviewEvidenceResult {
20
+ findings: AutomaticFinding[];
21
+ checks: CheckCoverage[];
22
+ agenda: ReviewAgendaEntry[];
23
+ }
24
+ /**
25
+ * Automatic findings, check coverage, and the review agenda for one diff. Main
26
+ * calls this inside the index callback with the same snapshots it renders, so
27
+ * every excerpt names the revision it came from; a call without indexes or
28
+ * without source readers still returns an agenda over the hunks, with each
29
+ * check honestly not checked.
30
+ */
31
+ export declare function buildReviewEvidence(units: readonly ReviewUnit[], options?: ReviewEvidenceOptions): ReviewEvidenceResult;