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
+ import { type LanguageExtractor } from "./types.js";
2
+ export declare const zigExtractor: LanguageExtractor;
@@ -0,0 +1,243 @@
1
+ import { heuristicCallSyntax } from "./call-syntax.js";
2
+ import { childByType, collapseWs, locFromNode, namedChildren, sameNode, } from "./types.js";
3
+ function getParamsLabel(params) {
4
+ if (!params || params.type !== "parameters")
5
+ return "()";
6
+ const names = [];
7
+ for (const p of namedChildren(params)) {
8
+ if (p.type !== "parameter")
9
+ continue;
10
+ const id = childByType(p, "identifier");
11
+ names.push(id?.text ?? "_");
12
+ }
13
+ return names.length === 0 ? "()" : `(${names.join(", ")})`;
14
+ }
15
+ function calleeKey(node, typeName) {
16
+ if (node.type === "identifier")
17
+ return node.text;
18
+ if (node.type === "field_expression") {
19
+ const object = node.namedChild(0);
20
+ const field = node.namedChild(1);
21
+ if (!object || !field)
22
+ return null;
23
+ const prop = field.text;
24
+ if (object.type === "identifier") {
25
+ if ((object.text === "self" || object.text === "this") && typeName) {
26
+ return `${typeName}.${prop}`;
27
+ }
28
+ return `${object.text}.${prop}`;
29
+ }
30
+ if (typeName)
31
+ return `${typeName}.${prop}`;
32
+ return prop;
33
+ }
34
+ // builtin @call etc.
35
+ if (node.type === "builtin_type" || node.type === "builtin_function") {
36
+ return null;
37
+ }
38
+ return null;
39
+ }
40
+ function statementsOf(block) {
41
+ if (!block)
42
+ return [];
43
+ if (block.type === "block" || block.type === "block_expression") {
44
+ const inner = block.type === "block_expression" ? childByType(block, "block") : block;
45
+ return inner ? namedChildren(inner) : namedChildren(block);
46
+ }
47
+ return [block];
48
+ }
49
+ function collectStatements(file, statements, typeName) {
50
+ const steps = [];
51
+ const seen = new Set();
52
+ const addCall = (key, node) => {
53
+ const mark = `${key}:${node.startIndex}`;
54
+ if (seen.has(mark))
55
+ return;
56
+ seen.add(mark);
57
+ const step = { type: "call", key, ...locFromNode(file, node) };
58
+ step.syntax = heuristicCallSyntax(node, key);
59
+ steps.push(step);
60
+ };
61
+ const walk = (node) => {
62
+ if (node.type === "function_declaration" ||
63
+ node.type === "struct_declaration" ||
64
+ node.type === "enum_declaration" ||
65
+ node.type === "union_declaration" ||
66
+ node.type === "opaque_declaration") {
67
+ return;
68
+ }
69
+ if (node.type === "if_statement") {
70
+ const kids = namedChildren(node);
71
+ const cond = kids.find((c) => c.type !== "block" &&
72
+ c.type !== "block_expression" &&
73
+ c.type !== "else_clause" &&
74
+ c.type !== "labeled_statement") ?? null;
75
+ const thenBlock = kids.find((c) => c.type === "block" || c.type === "block_expression") ?? null;
76
+ const elseClause = childByType(node, "else_clause");
77
+ const condText = cond ? collapseWs(cond.text) : "";
78
+ steps.push({
79
+ type: "branch",
80
+ key: condText ? `if:${condText}` : "if",
81
+ label: condText ? `if ${condText}` : "if",
82
+ ...locFromNode(file, cond ?? node),
83
+ children: collectStatements(file, statementsOf(thenBlock), typeName),
84
+ });
85
+ if (elseClause) {
86
+ // else_clause → block | labeled_statement → block | if_statement
87
+ const elseBody = childByType(elseClause, "block") ??
88
+ childByType(elseClause, "block_expression") ??
89
+ childByType(elseClause, "if_statement") ??
90
+ childByType(elseClause, "labeled_statement") ??
91
+ elseClause.namedChild(0) ??
92
+ null;
93
+ if (elseBody?.type === "if_statement") {
94
+ // else if — flatten by walking as nested if into else-if style
95
+ const nested = collectStatements(file, [elseBody], typeName);
96
+ for (const s of nested) {
97
+ if (s.type === "branch" && s.key.startsWith("if:")) {
98
+ steps.push({
99
+ ...s,
100
+ key: s.key.replace(/^if:/, "else-if:"),
101
+ label: s.label.replace(/^if /, "else if "),
102
+ });
103
+ }
104
+ else if (s.type === "branch" && s.key === "if") {
105
+ steps.push({ ...s, key: "else-if", label: "else if" });
106
+ }
107
+ else {
108
+ steps.push(s);
109
+ }
110
+ }
111
+ }
112
+ else if (elseBody?.type === "labeled_statement") {
113
+ const blk = childByType(elseBody, "block") ?? elseBody.namedChild(0) ?? null;
114
+ steps.push({
115
+ type: "branch",
116
+ key: "else",
117
+ label: "else",
118
+ ...locFromNode(file, elseClause),
119
+ children: collectStatements(file, statementsOf(blk), typeName),
120
+ });
121
+ }
122
+ else {
123
+ steps.push({
124
+ type: "branch",
125
+ key: "else",
126
+ label: "else",
127
+ ...locFromNode(file, elseClause),
128
+ children: collectStatements(file, statementsOf(elseBody), typeName),
129
+ });
130
+ }
131
+ }
132
+ return;
133
+ }
134
+ if (node.type === "try_expression") {
135
+ steps.push({
136
+ type: "branch",
137
+ key: "try",
138
+ label: "try",
139
+ ...locFromNode(file, node),
140
+ children: collectStatements(file, namedChildren(node), typeName),
141
+ });
142
+ return;
143
+ }
144
+ if (node.type === "defer_statement") {
145
+ steps.push({
146
+ type: "branch",
147
+ key: "defer",
148
+ label: "defer",
149
+ ...locFromNode(file, node),
150
+ children: collectStatements(file, namedChildren(node), typeName),
151
+ });
152
+ return;
153
+ }
154
+ if (node.type === "switch_expression") {
155
+ for (const clause of namedChildren(node)) {
156
+ if (clause.type !== "switch_case")
157
+ continue;
158
+ const kids = namedChildren(clause);
159
+ const bodyNode = kids.find((c) => c.type === "block" ||
160
+ c.type === "block_expression" ||
161
+ c.type === "call_expression" ||
162
+ c.type === "identifier") ?? kids[kids.length - 1] ?? null;
163
+ const pattern = kids.find((c) => !sameNode(c, bodyNode)) ?? null;
164
+ const isElse = !pattern;
165
+ const text = pattern ? collapseWs(pattern.text) : "";
166
+ steps.push({
167
+ type: "branch",
168
+ key: isElse ? "else" : text ? `case:${text}` : "case",
169
+ label: isElse ? "else" : text ? `case ${text}` : "case",
170
+ ...locFromNode(file, pattern ?? clause),
171
+ children: bodyNode
172
+ ? collectStatements(file, [bodyNode], typeName)
173
+ : [],
174
+ });
175
+ }
176
+ return;
177
+ }
178
+ if (node.type === "call_expression") {
179
+ const callee = node.namedChild(0);
180
+ if (callee) {
181
+ const key = calleeKey(callee, typeName);
182
+ if (key)
183
+ addCall(key, node);
184
+ }
185
+ for (const child of namedChildren(node).slice(1))
186
+ walk(child);
187
+ return;
188
+ }
189
+ for (const child of namedChildren(node))
190
+ walk(child);
191
+ };
192
+ for (const stmt of statements)
193
+ walk(stmt);
194
+ return steps;
195
+ }
196
+ function handleFunction(file, node, typeName, functions) {
197
+ const name = childByType(node, "identifier")?.text ?? null;
198
+ if (!name)
199
+ return;
200
+ const params = childByType(node, "parameters");
201
+ const body = childByType(node, "block");
202
+ const key = typeName ? `${typeName}.${name}` : name;
203
+ functions.push({
204
+ key,
205
+ label: `${key}${getParamsLabel(params)}`,
206
+ file,
207
+ steps: body ? collectStatements(file, statementsOf(body), typeName) : [],
208
+ exported: true,
209
+ start: node.startIndex,
210
+ end: node.endIndex,
211
+ });
212
+ }
213
+ function handleStruct(file, name, struct, functions) {
214
+ for (const child of namedChildren(struct)) {
215
+ if (child.type === "function_declaration") {
216
+ handleFunction(file, child, name, functions);
217
+ }
218
+ }
219
+ }
220
+ function extractFromTree(file, _source, tree) {
221
+ const functions = [];
222
+ for (const stmt of namedChildren(tree.rootNode)) {
223
+ if (stmt.type === "function_declaration") {
224
+ handleFunction(file, stmt, null, functions);
225
+ continue;
226
+ }
227
+ if (stmt.type === "variable_declaration") {
228
+ const name = childByType(stmt, "identifier")?.text ?? null;
229
+ const struct = childByType(stmt, "struct_declaration");
230
+ if (name && struct) {
231
+ handleStruct(file, name, struct, functions);
232
+ }
233
+ }
234
+ }
235
+ return functions;
236
+ }
237
+ export const zigExtractor = {
238
+ id: "zig",
239
+ extensions: [".zig"],
240
+ // Modern grammar; npm `tree-sitter-zig` is nan-era and won't load on tree-sitter 0.25+
241
+ grammarPackage: "@tree-sitter-grammars/tree-sitter-zig",
242
+ extract: extractFromTree,
243
+ };
package/dist/loc.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { SourceLoc } from "./types.js";
2
+ /** Pick optional loc fields for spreading onto a node/step. */
3
+ export declare function pickLoc(loc: {
4
+ file?: string;
5
+ line?: number;
6
+ endLine?: number;
7
+ } | null | undefined): Partial<SourceLoc>;
8
+ /** Format as `file:line` or `file:line-line`. */
9
+ export declare function formatSourceLoc(loc: SourceLoc): string;
10
+ /**
11
+ * Convert byte offsets in `source` to 1-based line numbers.
12
+ * `end` is exclusive (tree-sitter endIndex).
13
+ */
14
+ export declare function linesFromOffsets(source: string, start: number, end: number): {
15
+ line: number;
16
+ endLine?: number;
17
+ };
package/dist/loc.js ADDED
@@ -0,0 +1,34 @@
1
+ /** Pick optional loc fields for spreading onto a node/step. */
2
+ export function pickLoc(loc) {
3
+ if (!loc?.file || loc.line == null)
4
+ return {};
5
+ if (loc.endLine != null && loc.endLine !== loc.line) {
6
+ return { file: loc.file, line: loc.line, endLine: loc.endLine };
7
+ }
8
+ return { file: loc.file, line: loc.line };
9
+ }
10
+ /** Format as `file:line` or `file:line-line`. */
11
+ export function formatSourceLoc(loc) {
12
+ const range = loc.endLine != null && loc.endLine !== loc.line
13
+ ? `${loc.line}-${loc.endLine}`
14
+ : String(loc.line);
15
+ return `${loc.file}:${range}`;
16
+ }
17
+ /**
18
+ * Convert byte offsets in `source` to 1-based line numbers.
19
+ * `end` is exclusive (tree-sitter endIndex).
20
+ */
21
+ export function linesFromOffsets(source, start, end) {
22
+ let line = 1;
23
+ for (let i = 0; i < start && i < source.length; i++) {
24
+ if (source.charCodeAt(i) === 10)
25
+ line += 1;
26
+ }
27
+ let endLine = line;
28
+ const last = Math.max(start, end - 1);
29
+ for (let i = start; i < last && i < source.length; i++) {
30
+ if (source.charCodeAt(i) === 10)
31
+ endLine += 1;
32
+ }
33
+ return endLine > line ? { line, endLine } : { line };
34
+ }
@@ -0,0 +1,17 @@
1
+ import type { FunctionIndex } from "./extract.js";
2
+ import type { CallNode } from "./types.js";
3
+ /** Collapse a root→target node list into a linear single-child tree. */
4
+ export declare function pathToTree(nodes: CallNode[]): CallNode;
5
+ /**
6
+ * Collect every root-to-node path in `tree` that ends at `target`
7
+ * (first hit on a path; does not continue below the target).
8
+ */
9
+ export declare function collectPathsTo(tree: CallNode, resolvedTarget: string, rawTarget: string): CallNode[];
10
+ /**
11
+ * Find all call paths from `fromEntry` to `toEntry` in the indexed call graph.
12
+ * Expands via the same rules as `buildCallTree` (including branch forks).
13
+ *
14
+ * When several definitions share the entry name, every definition is walked
15
+ * (reach's contract is completeness). Order is stable across file reorderings.
16
+ */
17
+ export declare function findReachPaths(fromEntry: string, toEntry: string, index: FunctionIndex, maxDepth: number): CallNode[];
package/dist/reach.js ADDED
@@ -0,0 +1,65 @@
1
+ import { buildCallTree, buildCallTreeFromInfo, resolveAllEntries, resolveEntry, } from "./calltree.js";
2
+ import { assignOptionalTreeFields } from "./types.js";
3
+ function nodeMatchesTarget(nodeKey, resolvedTarget, rawTarget) {
4
+ if (nodeKey === resolvedTarget || nodeKey === rawTarget)
5
+ return true;
6
+ const stripped = nodeKey.replace(/\(\)$/, "");
7
+ if (stripped === resolvedTarget || stripped === rawTarget)
8
+ return true;
9
+ return (nodeKey.endsWith(`.${rawTarget}`) ||
10
+ nodeKey.endsWith(`.${resolvedTarget}`));
11
+ }
12
+ /** Collapse a root→target node list into a linear single-child tree. */
13
+ export function pathToTree(nodes) {
14
+ if (nodes.length === 0) {
15
+ throw new Error("pathToTree requires at least one node");
16
+ }
17
+ const [head, ...rest] = nodes;
18
+ const tree = {
19
+ key: head.key,
20
+ label: head.label,
21
+ children: rest.length > 0 ? [pathToTree(rest)] : [],
22
+ };
23
+ return assignOptionalTreeFields(tree, head);
24
+ }
25
+ /**
26
+ * Collect every root-to-node path in `tree` that ends at `target`
27
+ * (first hit on a path; does not continue below the target).
28
+ */
29
+ export function collectPathsTo(tree, resolvedTarget, rawTarget) {
30
+ const paths = [];
31
+ const walk = (node, ancestors) => {
32
+ const chain = [...ancestors, node];
33
+ if (nodeMatchesTarget(node.key, resolvedTarget, rawTarget)) {
34
+ paths.push(pathToTree(chain));
35
+ return;
36
+ }
37
+ for (const child of node.children) {
38
+ walk(child, chain);
39
+ }
40
+ };
41
+ walk(tree, []);
42
+ return paths;
43
+ }
44
+ /**
45
+ * Find all call paths from `fromEntry` to `toEntry` in the indexed call graph.
46
+ * Expands via the same rules as `buildCallTree` (including branch forks).
47
+ *
48
+ * When several definitions share the entry name, every definition is walked
49
+ * (reach's contract is completeness). Order is stable across file reorderings.
50
+ */
51
+ export function findReachPaths(fromEntry, toEntry, index, maxDepth) {
52
+ const toKey = resolveEntry(toEntry, index) ?? toEntry;
53
+ const fromInfos = resolveAllEntries(fromEntry, index);
54
+ if (fromInfos.length === 0) {
55
+ const fromKey = resolveEntry(fromEntry, index) ?? fromEntry;
56
+ const tree = buildCallTree(fromKey, index, maxDepth);
57
+ return collectPathsTo(tree, toKey, toEntry);
58
+ }
59
+ const paths = [];
60
+ for (const info of fromInfos) {
61
+ const tree = buildCallTreeFromInfo(info, index, maxDepth);
62
+ paths.push(...collectPathsTo(tree, toKey, toEntry));
63
+ }
64
+ return paths;
65
+ }
@@ -0,0 +1,18 @@
1
+ import type { CallNode, DiffNode } from "./types.js";
2
+ export interface RenderOptions {
3
+ /** When false, skip ANSI colors (useful for tests). Default: true */
4
+ color?: boolean;
5
+ /**
6
+ * Append `file:line` (or `file:line-line`) after each label.
7
+ * Default: false.
8
+ */
9
+ locs?: boolean;
10
+ }
11
+ /**
12
+ * Render a callstack diff as an ASCII tree with +/- markers.
13
+ */
14
+ export declare function renderDiff(root: DiffNode, options?: RenderOptions): string;
15
+ /**
16
+ * Render a single call tree as an ASCII tree (no +/- markers).
17
+ */
18
+ export declare function renderTree(root: CallNode, options?: RenderOptions): string;
package/dist/render.js ADDED
@@ -0,0 +1,83 @@
1
+ import pc from "picocolors";
2
+ import { formatSourceLoc, pickLoc } from "./loc.js";
3
+ function paint(status, text) {
4
+ switch (status) {
5
+ case "added":
6
+ return pc.blue(text);
7
+ case "removed":
8
+ return pc.magenta(text);
9
+ default:
10
+ return text;
11
+ }
12
+ }
13
+ function prefix(status) {
14
+ switch (status) {
15
+ case "added":
16
+ return pc.blue("+");
17
+ case "removed":
18
+ return pc.magenta("-");
19
+ default:
20
+ return " ";
21
+ }
22
+ }
23
+ function renderAsciiTree(root, options, withStatus) {
24
+ const useColor = options.color !== false;
25
+ const showLocs = options.locs === true;
26
+ const paintStatus = (status, text) => useColor ? paint(status, text) : text;
27
+ const statusPrefix = (status) => {
28
+ if (!useColor) {
29
+ switch (status) {
30
+ case "added":
31
+ return "+";
32
+ case "removed":
33
+ return "-";
34
+ default:
35
+ return " ";
36
+ }
37
+ }
38
+ return prefix(status);
39
+ };
40
+ const locSuffix = (node) => {
41
+ if (!showLocs)
42
+ return "";
43
+ const loc = pickLoc(node);
44
+ if (!loc.file || loc.line == null)
45
+ return "";
46
+ // SAFETY: file and line were just validated as present above.
47
+ const text = ` ${formatSourceLoc(loc)}`;
48
+ return useColor ? pc.dim(text) : text;
49
+ };
50
+ const lines = [];
51
+ const walk = (node, indent, isLast, isRoot) => {
52
+ const branch = isRoot ? "" : isLast ? "└─ " : "├─ ";
53
+ const label = withStatus
54
+ ? paintStatus(node.status ?? "same", node.label)
55
+ : node.label;
56
+ const suffix = locSuffix(node);
57
+ const line = withStatus
58
+ ? `${statusPrefix(node.status ?? "same")} ${indent}${branch}${label}${suffix}`
59
+ : `${indent}${branch}${label}${suffix}`;
60
+ lines.push(line);
61
+ // Conditional arms omit the continuing │ rail — they are alternate paths,
62
+ // not a nested stack that continues past the branch.
63
+ const rail = node.kind === "branch" ? " " : isLast || isRoot ? " " : "│ ";
64
+ const childIndent = isRoot ? "" : `${indent}${rail}`;
65
+ node.children.forEach((child, index) => {
66
+ walk(child, childIndent, index === node.children.length - 1, false);
67
+ });
68
+ };
69
+ walk(root, "", true, true);
70
+ return lines.join("\n");
71
+ }
72
+ /**
73
+ * Render a callstack diff as an ASCII tree with +/- markers.
74
+ */
75
+ export function renderDiff(root, options = {}) {
76
+ return renderAsciiTree(root, options, true);
77
+ }
78
+ /**
79
+ * Render a single call tree as an ASCII tree (no +/- markers).
80
+ */
81
+ export function renderTree(root, options = {}) {
82
+ return renderAsciiTree(root, options, false);
83
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The diffninja mark: a ninja's head in its hood, eyes showing through the
3
+ * slit, a red bandana tied at the back with its tails flying. Inline SVG, so
4
+ * both pages carry it under their no-external-resources policies.
5
+ */
6
+ export declare const BRAND_MARK: string;
7
+ /** Colors for {@link BRAND_MARK}; the hood stays dark, outlined so it reads on a dark page too. */
8
+ export declare const BRAND_MARK_STYLES = "\n.brand-mark { width: 26px; height: 26px; flex: 0 0 auto; }\n.brand-mark .nj-hood { fill: #1f2328; stroke: var(--line-strong); stroke-width: 2; }\n.brand-mark .nj-band, .brand-mark .nj-tail { fill: #d1242f; }\n.brand-mark .nj-skin { fill: #f2c9a0; }\n.brand-mark .nj-eye { fill: #1f2328; }\n";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The diffninja mark: a ninja's head in its hood, eyes showing through the
3
+ * slit, a red bandana tied at the back with its tails flying. Inline SVG, so
4
+ * both pages carry it under their no-external-resources policies.
5
+ */
6
+ export const BRAND_MARK = [
7
+ '<svg class="brand-mark" viewBox="0 0 64 64" width="26" height="26" aria-hidden="true" focusable="false">',
8
+ '<path class="nj-tail" d="M49 19c5-1 9-4 13-9-1 6-4 10-9 13z"/>',
9
+ '<path class="nj-tail" d="M50 24c5 1 8 4 10 10-4-3-7-4-12-5z"/>',
10
+ '<circle class="nj-hood" cx="28" cy="34" r="24"/>',
11
+ '<path class="nj-band" d="M5 25c15-5 30-5 46 0l-1 7c-14-5-29-5-44 0z"/>',
12
+ '<circle class="nj-band" cx="50" cy="24" r="4"/>',
13
+ '<rect class="nj-skin" x="10" y="34" width="36" height="11" rx="5"/>',
14
+ '<ellipse class="nj-eye" cx="21" cy="39" rx="3" ry="3"/>',
15
+ '<ellipse class="nj-eye" cx="35" cy="39" rx="3" ry="3"/>',
16
+ "</svg>",
17
+ ].join("");
18
+ /** Colors for {@link BRAND_MARK}; the hood stays dark, outlined so it reads on a dark page too. */
19
+ export const BRAND_MARK_STYLES = `
20
+ .brand-mark { width: 26px; height: 26px; flex: 0 0 auto; }
21
+ .brand-mark .nj-hood { fill: #1f2328; stroke: var(--line-strong); stroke-width: 2; }
22
+ .brand-mark .nj-band, .brand-mark .nj-tail { fill: #d1242f; }
23
+ .brand-mark .nj-skin { fill: #f2c9a0; }
24
+ .brand-mark .nj-eye { fill: #1f2328; }
25
+ `;
@@ -0,0 +1,27 @@
1
+ import { type FunctionIndex } from "../extract.js";
2
+ import type { SourceLoc } from "../types.js";
3
+ import type { ReviewContextNode, ReviewUnit } from "./types.js";
4
+ /**
5
+ * Whole definition text for one snapshot, or `null` when the snapshot cannot
6
+ * confirm a definition at that span. Supplied by the caller because reading a
7
+ * snapshot belongs to the service, not to a pure selection function.
8
+ */
9
+ export type ContextSourceReader = (definition: SourceLoc) => string | null;
10
+ /** Per-snapshot readers for the node source text; absent means none is read. */
11
+ export interface ContextSources {
12
+ readonly before?: ContextSourceReader;
13
+ readonly after?: ContextSourceReader;
14
+ }
15
+ /** Report blocks and addressable nodes for one hunk. */
16
+ export interface UnitContext {
17
+ /** Report text in report order: both snapshots, the hunk-adjacent call first. */
18
+ entries: string[];
19
+ /** Structured context, highest retention priority first. */
20
+ nodes: ReviewContextNode[];
21
+ }
22
+ /**
23
+ * Per-hunk context: the standalone report blocks, adjacent to the hunk first
24
+ * with snapshot-local bindings, and the structured nodes those blocks were
25
+ * derived from. A hunk with neither gets no entry rather than an empty one.
26
+ */
27
+ export declare function buildCallContext(units: readonly ReviewUnit[], before: FunctionIndex, after: FunctionIndex, sources?: ContextSources): Map<string, UnitContext>;