diffninja 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { buildCallSitesFromInfo } from "../calltree.js";
|
|
2
|
+
import { allContextDefinitions } from "../extract.js";
|
|
3
|
+
import { resolveDispatchContext } from "../languages/typescript-dispatch.js";
|
|
4
|
+
import { resolveTypeContracts } from "../languages/typescript-contracts.js";
|
|
5
|
+
import { formatSourceLoc } from "../loc.js";
|
|
6
|
+
import { CALL_FLOW_MAX_DEPTH } from "./call-flow.js";
|
|
7
|
+
function locationKey(loc) {
|
|
8
|
+
return `${loc.file}\0${loc.line}\0${loc.endLine ?? loc.line}`;
|
|
9
|
+
}
|
|
10
|
+
/** Expand each body once, rather than building the same descendant tree per caller. */
|
|
11
|
+
function contextGraph(index) {
|
|
12
|
+
const infos = allContextDefinitions(index);
|
|
13
|
+
const definitions = infos.map(info => ({ info, outgoing: [], incoming: [] }));
|
|
14
|
+
const locations = new Map();
|
|
15
|
+
for (const definition of definitions) {
|
|
16
|
+
const { file, line, endLine } = definition.info;
|
|
17
|
+
if (line === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
const key = locationKey({ file, line, endLine });
|
|
20
|
+
// Line-only engine locations cannot distinguish two definitions on one line.
|
|
21
|
+
locations.set(key, locations.has(key) ? null : definition);
|
|
22
|
+
}
|
|
23
|
+
const edges = [];
|
|
24
|
+
for (const owner of definitions) {
|
|
25
|
+
if (owner.info.review?.kind)
|
|
26
|
+
continue;
|
|
27
|
+
for (const node of buildCallSitesFromInfo(owner.info, index)) {
|
|
28
|
+
const target = node.definition ? locations.get(locationKey(node.definition)) ?? undefined : undefined;
|
|
29
|
+
const edge = { node, owner, target };
|
|
30
|
+
owner.outgoing.push(edge);
|
|
31
|
+
target?.incoming.push(edge);
|
|
32
|
+
edges.push(edge);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const byInfo = new Map(definitions.map(definition => [definition.info, definition]));
|
|
36
|
+
for (const relation of [...resolveDispatchContext(infos), ...resolveTypeContracts(infos)]) {
|
|
37
|
+
const owner = byInfo.get(relation.owner), target = byInfo.get(relation.target);
|
|
38
|
+
if (!owner || !target || target.info.line === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
const edge = {
|
|
41
|
+
owner, target,
|
|
42
|
+
relation: { kind: relation.kind, evidence: relation.evidence },
|
|
43
|
+
node: {
|
|
44
|
+
key: target.info.key, label: target.info.label, children: [],
|
|
45
|
+
file: owner.info.file, line: relation.line,
|
|
46
|
+
definition: { file: target.info.file, line: target.info.line, endLine: target.info.endLine },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
owner.outgoing.push(edge);
|
|
50
|
+
target.incoming.push(edge);
|
|
51
|
+
edges.push(edge);
|
|
52
|
+
}
|
|
53
|
+
return { definitions, edges };
|
|
54
|
+
}
|
|
55
|
+
/** Use changed lines, not the surrounding unchanged lines in a unified hunk. */
|
|
56
|
+
function changedLines(unit, side) {
|
|
57
|
+
let line = side === "before" ? unit.oldStart : unit.newStart;
|
|
58
|
+
const changed = [];
|
|
59
|
+
for (const text of unit.diff.split("\n").slice(1)) {
|
|
60
|
+
const prefix = text[0];
|
|
61
|
+
if (prefix === (side === "before" ? "-" : "+"))
|
|
62
|
+
changed.push(line++);
|
|
63
|
+
else if (prefix === " ")
|
|
64
|
+
line++;
|
|
65
|
+
}
|
|
66
|
+
// An insertion/deletion still has a boundary on the other snapshot.
|
|
67
|
+
return changed.length ? changed : [side === "before" ? unit.oldStart : unit.newStart];
|
|
68
|
+
}
|
|
69
|
+
function hunkDefinitions(graph, unit, side) {
|
|
70
|
+
const inFile = graph.definitions.filter(({ info }) => info.file === unit.file && info.line !== undefined);
|
|
71
|
+
const selected = new Set();
|
|
72
|
+
for (const line of changedLines(unit, side)) {
|
|
73
|
+
const containing = inFile.filter(({ info }) => info.line <= line && (info.endLine ?? info.line) >= line);
|
|
74
|
+
// A nested definition's hunk should not pull in all of its enclosing body's siblings.
|
|
75
|
+
let smallest = Infinity;
|
|
76
|
+
for (const { info } of containing)
|
|
77
|
+
smallest = Math.min(smallest, info.end - info.start);
|
|
78
|
+
for (const definition of containing) {
|
|
79
|
+
if (definition.info.end - definition.info.start === smallest)
|
|
80
|
+
selected.add(definition);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return [...selected];
|
|
84
|
+
}
|
|
85
|
+
function location(node) {
|
|
86
|
+
return node.file !== undefined && node.line !== undefined
|
|
87
|
+
? formatSourceLoc({ file: node.file, line: node.line, endLine: node.endLine })
|
|
88
|
+
: "unavailable";
|
|
89
|
+
}
|
|
90
|
+
function renderEdge({ edge, side, repeated }) {
|
|
91
|
+
const { node, target } = edge;
|
|
92
|
+
if (edge.relation)
|
|
93
|
+
return [
|
|
94
|
+
`${edge.relation.kind} relation ${edge.owner.info.key} -> ${target?.info.key} @ ${location(node)}`,
|
|
95
|
+
` snapshot=${side} target=candidate mapping=unknown`,
|
|
96
|
+
` evidence=${edge.relation.evidence}`,
|
|
97
|
+
` definition=${location(node.definition ?? {})}`,
|
|
98
|
+
" static syntax relation; not a runtime call or proof of delivery",
|
|
99
|
+
].join("\n");
|
|
100
|
+
const context = node.context;
|
|
101
|
+
const lines = [
|
|
102
|
+
`call ${context?.callee ?? node.key} @ ${location(node)}`,
|
|
103
|
+
` snapshot=${side} target=${context?.target ?? "unresolved"}`,
|
|
104
|
+
` definition=${context?.parameters ?? "unavailable"}${node.definition ? ` @ ${location(node.definition)}` : ""}`,
|
|
105
|
+
` mapping=${context?.mapping ?? "unknown"}`,
|
|
106
|
+
];
|
|
107
|
+
if (context?.arguments === undefined)
|
|
108
|
+
lines.push(" arguments=unavailable");
|
|
109
|
+
else if (context.arguments.length === 0)
|
|
110
|
+
lines.push(" arguments=[]");
|
|
111
|
+
else {
|
|
112
|
+
for (const arg of context.arguments) {
|
|
113
|
+
const cut = arg.truncated ? ` truncated=true originalLength=${arg.originalLength}` : "";
|
|
114
|
+
lines.push(` arg[${arg.position}] -> ${arg.parameter ?? "?"}: ${JSON.stringify(arg.expression)}${cut}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (context?.omittedArguments)
|
|
118
|
+
lines.push(` omitted arguments=${context.omittedArguments} reason=argument-limit`);
|
|
119
|
+
if (repeated)
|
|
120
|
+
lines.push(" omitted expansion=repeated-definition (distinct call site retained)");
|
|
121
|
+
// Known bodies are expanded through the flat graph, not through this depth-one node.
|
|
122
|
+
if (!target && context?.omittedChildren)
|
|
123
|
+
lines.push(` omitted children=${context.omittedChildren} reason=unavailable-expansion`);
|
|
124
|
+
if (target && context?.omittedInlineChildren)
|
|
125
|
+
lines.push(` omitted children=${context.omittedInlineChildren} reason=inline-expansion-limit`);
|
|
126
|
+
lines.push(` completeness=${context?.completeness ?? "unavailable"} reason=${context?.reasons.join(",") || (context ? "none" : "unavailable-extraction")}`);
|
|
127
|
+
return lines.join("\n");
|
|
128
|
+
}
|
|
129
|
+
function selectContext(graph, unit, side) {
|
|
130
|
+
const seeds = hunkDefinitions(graph, unit, side);
|
|
131
|
+
if (seeds.length === 0)
|
|
132
|
+
return { selected: [], omissions: [], seeds };
|
|
133
|
+
const lines = changedLines(unit, side);
|
|
134
|
+
const seedSet = new Set(seeds);
|
|
135
|
+
const selected = new Map();
|
|
136
|
+
// Outgoing descendants and incoming callers have separate visited sets. Walking
|
|
137
|
+
// upstream must not expand unrelated sibling calls in the caller's body.
|
|
138
|
+
for (const direction of ["outgoing", "incoming"]) {
|
|
139
|
+
const seen = new Set(seeds);
|
|
140
|
+
const queue = seeds.map(definition => ({ definition, distance: 0, anchor: definition }));
|
|
141
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
142
|
+
const { definition, distance, anchor } = queue[cursor];
|
|
143
|
+
if (distance >= CALL_FLOW_MAX_DEPTH)
|
|
144
|
+
continue;
|
|
145
|
+
for (const edge of definition[direction]) {
|
|
146
|
+
const next = direction === "outgoing" ? edge.target : edge.owner;
|
|
147
|
+
const repeated = direction === "outgoing" && next !== undefined && seen.has(next) && next.outgoing.length > 0;
|
|
148
|
+
const previous = selected.get(edge);
|
|
149
|
+
if (!previous || previous.distance > distance + 1) {
|
|
150
|
+
const atHunk = edge.node.file === unit.file && edge.node.line !== undefined &&
|
|
151
|
+
lines.some(line => line >= edge.node.line && line <= (edge.node.endLine ?? edge.node.line));
|
|
152
|
+
const priority = atHunk ? 0 : edge.target && seedSet.has(edge.target) ? 1 : distance + 2;
|
|
153
|
+
selected.set(edge, { edge, side, distance: distance + 1, priority, repeated, anchor, direction });
|
|
154
|
+
}
|
|
155
|
+
else if (repeated)
|
|
156
|
+
previous.repeated = true;
|
|
157
|
+
if (next && !seen.has(next)) {
|
|
158
|
+
seen.add(next);
|
|
159
|
+
queue.push({ definition: next, distance: distance + 1, anchor });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (selected.size === 0)
|
|
165
|
+
return { selected: [], omissions: [], seeds };
|
|
166
|
+
// Count omitted components separately from connected but distant/sibling edges.
|
|
167
|
+
const connected = new Set(seeds);
|
|
168
|
+
const queue = [...seeds];
|
|
169
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
170
|
+
const definition = queue[cursor];
|
|
171
|
+
for (const edge of [...definition.outgoing, ...definition.incoming]) {
|
|
172
|
+
const next = edge.owner === definition ? edge.target : edge.owner;
|
|
173
|
+
if (next && !connected.has(next)) {
|
|
174
|
+
connected.add(next);
|
|
175
|
+
queue.push(next);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let unrelated = 0, distant = 0;
|
|
180
|
+
for (const edge of graph.edges) {
|
|
181
|
+
if (selected.has(edge))
|
|
182
|
+
continue;
|
|
183
|
+
if (!connected.has(edge.owner))
|
|
184
|
+
unrelated++;
|
|
185
|
+
else
|
|
186
|
+
distant++;
|
|
187
|
+
}
|
|
188
|
+
const omissions = [];
|
|
189
|
+
if (unrelated)
|
|
190
|
+
omissions.push(`omitted context edges=${unrelated} snapshot=${side} reason=unrelated-to-hunk`);
|
|
191
|
+
if (distant)
|
|
192
|
+
omissions.push(`omitted context edges=${distant} snapshot=${side} reason=distant-descendants-or-siblings depth-limit=${CALL_FLOW_MAX_DEPTH}`);
|
|
193
|
+
return { selected: [...selected.values()], omissions, seeds };
|
|
194
|
+
}
|
|
195
|
+
/** Distinct, readable key for one node; a repeated symbol key keeps its own location. */
|
|
196
|
+
function nodeKey(info, side, taken) {
|
|
197
|
+
const symbol = `${side}:${info.key}`;
|
|
198
|
+
if (!taken.has(symbol)) {
|
|
199
|
+
taken.add(symbol);
|
|
200
|
+
return symbol;
|
|
201
|
+
}
|
|
202
|
+
const located = `${symbol}@${info.line ?? "?"}`;
|
|
203
|
+
let key = located;
|
|
204
|
+
for (let nth = 2; taken.has(key); nth += 1)
|
|
205
|
+
key = `${located}#${nth}`;
|
|
206
|
+
taken.add(key);
|
|
207
|
+
return key;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The nodes one hunk's selection describes: the changed definition in each
|
|
211
|
+
* snapshot, the definitions that call it, then the definitions its calls
|
|
212
|
+
* resolve to, each carrying the call sites and bindings the selection already
|
|
213
|
+
* extracted. A selected edge is attached to its owner's node and, when the edge
|
|
214
|
+
* resolved into a node as well, to that node's caller evidence, so no existing
|
|
215
|
+
* evidence is dropped on the way here.
|
|
216
|
+
*/
|
|
217
|
+
function nodePlans(sides) {
|
|
218
|
+
const plans = [];
|
|
219
|
+
const byDefinition = new Map();
|
|
220
|
+
const keys = new Set();
|
|
221
|
+
const add = (definition, side, role) => {
|
|
222
|
+
const existing = byDefinition.get(definition);
|
|
223
|
+
if (existing)
|
|
224
|
+
return existing;
|
|
225
|
+
// A definition without a line has no location to address, so it never becomes a node.
|
|
226
|
+
if (definition.info.line === undefined)
|
|
227
|
+
return undefined;
|
|
228
|
+
const plan = {
|
|
229
|
+
definition, key: nodeKey(definition.info, side, keys), side, role,
|
|
230
|
+
calls: [], reachedBy: [], calleeReach: [], callerReach: [],
|
|
231
|
+
};
|
|
232
|
+
byDefinition.set(definition, plan);
|
|
233
|
+
plans.push(plan);
|
|
234
|
+
return plan;
|
|
235
|
+
};
|
|
236
|
+
// Hunk definitions of both snapshots lead — the resulting snapshot first,
|
|
237
|
+
// because it is the code under review — then the definitions that call them,
|
|
238
|
+
// which are the caller contracts a hunk alone cannot show, then the
|
|
239
|
+
// definitions their selected calls resolve to, nearest first, which are the
|
|
240
|
+
// callee bodies a call signature alone cannot show.
|
|
241
|
+
const distances = new Map();
|
|
242
|
+
const boundaries = new Set();
|
|
243
|
+
for (const { selection } of sides) {
|
|
244
|
+
for (const seed of selection.seeds)
|
|
245
|
+
distances.set(seed, 0);
|
|
246
|
+
for (const entry of selection.selected) {
|
|
247
|
+
const near = entry.direction === "incoming" ? entry.edge.target : entry.edge.owner;
|
|
248
|
+
const far = entry.direction === "incoming" ? entry.edge.owner : entry.edge.target;
|
|
249
|
+
if (near)
|
|
250
|
+
distances.set(near, Math.min(distances.get(near) ?? Infinity, entry.distance - 1));
|
|
251
|
+
if (far)
|
|
252
|
+
distances.set(far, Math.min(distances.get(far) ?? Infinity, entry.distance));
|
|
253
|
+
if (entry.edge.relation) {
|
|
254
|
+
if (entry.edge.relation.kind !== "contract")
|
|
255
|
+
boundaries.add(entry.edge.owner);
|
|
256
|
+
if (entry.edge.target)
|
|
257
|
+
boundaries.add(entry.edge.target);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const { side, selection } of sides)
|
|
262
|
+
for (const seed of selection.seeds)
|
|
263
|
+
add(seed, side, "changed-definition");
|
|
264
|
+
for (const { side, selection } of sides) {
|
|
265
|
+
for (const entry of selection.selected) {
|
|
266
|
+
if (entry.direction !== "incoming")
|
|
267
|
+
continue;
|
|
268
|
+
add(entry.edge.owner, side, "caller");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const { side, selection } of sides) {
|
|
272
|
+
// The walk already holds one entry per selected edge at the shortest
|
|
273
|
+
// distance it was reached, so a stable sort by distance orders a callee
|
|
274
|
+
// before the definitions that callee in turn reaches.
|
|
275
|
+
const callees = selection.selected
|
|
276
|
+
.filter(entry => entry.direction === "outgoing" && entry.edge.target !== undefined)
|
|
277
|
+
.sort((left, right) => left.distance - right.distance);
|
|
278
|
+
for (const entry of callees)
|
|
279
|
+
add(entry.edge.target, side, "callee");
|
|
280
|
+
}
|
|
281
|
+
for (const { selection } of sides) {
|
|
282
|
+
for (const entry of selection.selected) {
|
|
283
|
+
const owner = byDefinition.get(entry.edge.owner);
|
|
284
|
+
const target = entry.edge.target === undefined ? undefined : byDefinition.get(entry.edge.target);
|
|
285
|
+
// A selected edge is the owner's own call site and, when it resolved into
|
|
286
|
+
// a node, that node's caller evidence too: a definition whose body does
|
|
287
|
+
// not contain the call would otherwise carry no binding at all.
|
|
288
|
+
if (owner)
|
|
289
|
+
owner.calls.push(entry);
|
|
290
|
+
if (target && target !== owner)
|
|
291
|
+
target.reachedBy.push(entry);
|
|
292
|
+
if (owner || target)
|
|
293
|
+
continue;
|
|
294
|
+
// A definition the snapshot gave no line never became a node, so its
|
|
295
|
+
// edges stay with the hunk definition whose walk found them.
|
|
296
|
+
const anchor = byDefinition.get(entry.anchor);
|
|
297
|
+
if (!anchor)
|
|
298
|
+
continue;
|
|
299
|
+
if (entry.direction === "incoming")
|
|
300
|
+
anchor.callerReach.push(entry);
|
|
301
|
+
else
|
|
302
|
+
anchor.calleeReach.push(entry);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Prefer resulting-code evidence to prior-snapshot duplicates. A contract's
|
|
306
|
+
// target is the boundary, not every method that happens to mention a type:
|
|
307
|
+
// promoting both lets those methods crowd the declarations out again.
|
|
308
|
+
const tier = (plan) => plan.role === "changed-definition" ? 0
|
|
309
|
+
: boundaries.has(plan.definition) ? 1 : 2;
|
|
310
|
+
return plans.sort((a, b) => Number(a.side === "before") - Number(b.side === "before")
|
|
311
|
+
|| tier(a) - tier(b)
|
|
312
|
+
|| (distances.get(a.definition) ?? Infinity) - (distances.get(b.definition) ?? Infinity)
|
|
313
|
+
|| Number(a.role === "caller") - Number(b.role === "caller"));
|
|
314
|
+
}
|
|
315
|
+
/** Section headers of a node detail, in the order the evidence is shown. */
|
|
316
|
+
const EVIDENCE_SECTIONS = [
|
|
317
|
+
{ label: "call-sites-in-this-definition", of: "calls", depth: false },
|
|
318
|
+
{ label: "call-sites-reaching-this-definition", of: "reachedBy", depth: false },
|
|
319
|
+
{ label: "callee-reach-from-this-definition", of: "calleeReach", depth: true },
|
|
320
|
+
{ label: "caller-reach-from-this-definition", of: "callerReach", depth: true },
|
|
321
|
+
];
|
|
322
|
+
/**
|
|
323
|
+
* Whole node text: identity, the snapshot's own definition source between
|
|
324
|
+
* explicit markers, and the selected call/binding evidence for this node. The
|
|
325
|
+
* source is never re-indented or excerpted, so the text between the markers is
|
|
326
|
+
* exactly what the snapshot holds.
|
|
327
|
+
*/
|
|
328
|
+
function nodeDetail(plan, unit, source) {
|
|
329
|
+
const info = plan.definition.info;
|
|
330
|
+
const loc = formatSourceLoc({ file: info.file, line: info.line, endLine: info.endLine });
|
|
331
|
+
const lines = [
|
|
332
|
+
`context node ${plan.key}`,
|
|
333
|
+
` label=${info.label}`,
|
|
334
|
+
` location=${loc} snapshot=${plan.side} role=${plan.role} hunk=${unit.id} file=${unit.file}`,
|
|
335
|
+
info.review?.kind ? ` declaration-kind=${info.review.kind} (non-callable)` : ` declared-parameters=${info.params?.text ?? "unavailable"}`,
|
|
336
|
+
];
|
|
337
|
+
if (source === null)
|
|
338
|
+
lines.push(` source=${loc} snapshot=${plan.side} unavailable reason=whole-definition-not-readable`);
|
|
339
|
+
else
|
|
340
|
+
lines.push(` source=${loc} snapshot=${plan.side} begin`, source, " source-end");
|
|
341
|
+
let evidence = 0;
|
|
342
|
+
for (const section of EVIDENCE_SECTIONS) {
|
|
343
|
+
// With whole source present, unresolved own-call expressions are already
|
|
344
|
+
// verbatim in that body. Their unknown-binding boilerplate adds no target
|
|
345
|
+
// evidence. Keep it when source is absent, and keep every resolved relation.
|
|
346
|
+
const supplied = plan[section.of];
|
|
347
|
+
const edges = source !== null && section.of === "calls"
|
|
348
|
+
? supplied.filter(entry => entry.edge.target !== undefined || entry.edge.relation !== undefined)
|
|
349
|
+
: supplied;
|
|
350
|
+
if (edges.length < supplied.length) {
|
|
351
|
+
lines.push(` unresolved own-call evidence=${supplied.length - edges.length} source-only; targets and argument mappings unknown`);
|
|
352
|
+
}
|
|
353
|
+
if (edges.length === 0)
|
|
354
|
+
continue;
|
|
355
|
+
evidence += edges.length;
|
|
356
|
+
lines.push(` evidence=${section.label} count=${edges.length}`);
|
|
357
|
+
for (const entry of edges) {
|
|
358
|
+
if (section.depth)
|
|
359
|
+
lines.push(` depth=${entry.distance}`);
|
|
360
|
+
lines.push(renderEdge(entry));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (evidence === 0)
|
|
364
|
+
lines.push(" evidence=none-selected reason=no-selected-call-site-touches-this-definition");
|
|
365
|
+
return lines.join("\n");
|
|
366
|
+
}
|
|
367
|
+
function contextNodeOf(plan, unit, source) {
|
|
368
|
+
const info = plan.definition.info;
|
|
369
|
+
const roles = new Set();
|
|
370
|
+
for (const { edge } of plan.calls) {
|
|
371
|
+
if (edge.relation?.kind === "event")
|
|
372
|
+
roles.add("event publisher");
|
|
373
|
+
if (edge.relation?.kind === "queue")
|
|
374
|
+
roles.add("queue producer");
|
|
375
|
+
}
|
|
376
|
+
for (const { edge } of plan.reachedBy) {
|
|
377
|
+
if (edge.relation?.kind === "event")
|
|
378
|
+
roles.add("event listener");
|
|
379
|
+
if (edge.relation?.kind === "queue")
|
|
380
|
+
roles.add("queue consumer");
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
key: plan.key,
|
|
384
|
+
label: roles.size ? `${info.label} [${[...roles].join(", ")}]` : info.label,
|
|
385
|
+
file: info.file,
|
|
386
|
+
// Every node was admitted with a location, so this is the same line the
|
|
387
|
+
// descriptor and the detail report.
|
|
388
|
+
line: info.line,
|
|
389
|
+
detail: nodeDetail(plan, unit, source),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/** Exact source lines already visible on each side of the untruncated hunk. */
|
|
393
|
+
function hunkSources(unit) {
|
|
394
|
+
const before = [], after = [];
|
|
395
|
+
for (const line of unit.diff.split("\n").slice(1)) {
|
|
396
|
+
if (line[0] === " " || line[0] === "-")
|
|
397
|
+
before.push(line.slice(1));
|
|
398
|
+
if (line[0] === " " || line[0] === "+")
|
|
399
|
+
after.push(line.slice(1));
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
before: { text: before.join("\n"), start: unit.oldStart, end: unit.oldStart + before.length - 1 },
|
|
403
|
+
after: { text: after.join("\n"), start: unit.newStart, end: unit.newStart + after.length - 1 },
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Per-hunk context: the standalone report blocks, adjacent to the hunk first
|
|
408
|
+
* with snapshot-local bindings, and the structured nodes those blocks were
|
|
409
|
+
* derived from. A hunk with neither gets no entry rather than an empty one.
|
|
410
|
+
*/
|
|
411
|
+
export function buildCallContext(units, before, after, sources = {}) {
|
|
412
|
+
const beforeGraph = contextGraph(before), afterGraph = contextGraph(after);
|
|
413
|
+
const result = new Map();
|
|
414
|
+
for (const unit of units) {
|
|
415
|
+
if (unit.special || !unit.header.startsWith("@@"))
|
|
416
|
+
continue;
|
|
417
|
+
const prior = selectContext(beforeGraph, unit, "before");
|
|
418
|
+
const current = selectContext(afterGraph, unit, "after");
|
|
419
|
+
// Keep both sides of an adjacent edge before any farther descendant.
|
|
420
|
+
const selected = [...current.selected, ...prior.selected].sort((left, right) => left.priority - right.priority);
|
|
421
|
+
const blocks = selected.map(renderEdge);
|
|
422
|
+
const omissions = [...prior.omissions, ...current.omissions];
|
|
423
|
+
if (omissions.length)
|
|
424
|
+
blocks.push(omissions.join("\n"));
|
|
425
|
+
const shown = hunkSources(unit);
|
|
426
|
+
const nodes = [], redundant = [];
|
|
427
|
+
for (const plan of nodePlans([{ side: "after", selection: current }, { side: "before", selection: prior }])) {
|
|
428
|
+
const info = plan.definition.info;
|
|
429
|
+
const source = sources[plan.side]?.({ file: info.file, line: info.line, endLine: info.endLine }) ?? null;
|
|
430
|
+
const node = contextNodeOf(plan, unit, source);
|
|
431
|
+
const snapshot = shown[plan.side];
|
|
432
|
+
// Do not let whole definitions already present in the diff consume the
|
|
433
|
+
// addressable-node cap before external contracts. Keep them, with all
|
|
434
|
+
// their call evidence, after definitions whose bodies the hunk cannot show.
|
|
435
|
+
const fullyShown = info.file === unit.file && info.line >= snapshot.start &&
|
|
436
|
+
(info.endLine ?? info.line) <= snapshot.end && source !== null && source.length > 0 &&
|
|
437
|
+
snapshot.text.includes(source);
|
|
438
|
+
(fullyShown ? redundant : nodes).push(node);
|
|
439
|
+
}
|
|
440
|
+
nodes.push(...redundant);
|
|
441
|
+
if (blocks.length === 0 && nodes.length === 0)
|
|
442
|
+
continue;
|
|
443
|
+
result.set(unit.id, { entries: blocks, nodes });
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReviewReport } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Call-flow navigation for a review report: Tree, Graph and Sequence views of
|
|
4
|
+
* the syntactic call trees that touch each changed file.
|
|
5
|
+
*
|
|
6
|
+
* Everything is server-rendered. Tree mode is live markup, so calls, call
|
|
7
|
+
* sites and each resolved definition are readable with JavaScript disabled; the
|
|
8
|
+
* Graph and Sequence renderings of the same calls ship inside templates and are
|
|
9
|
+
* instantiated the first time their mode is opened, which costs no live nodes
|
|
10
|
+
* until then, and the page states that plainly where it matters. Tree nodes fold
|
|
11
|
+
* with native `<details>`, and the resolved definition of a call —
|
|
12
|
+
* line-numbered and escaped — sits in its own `<details>` under the call. The
|
|
13
|
+
* appended script switches modes, keeps an explicit trail of the functions the
|
|
14
|
+
* reviewer visited, limits the graph to a depth below the focused call, and
|
|
15
|
+
* repeats one node's server-rendered source in a single panel; it reads every
|
|
16
|
+
* label from the escaped DOM and never receives report text.
|
|
17
|
+
*
|
|
18
|
+
* Node identity is an occurrence path (`0-2-1`), never a key or a name, so
|
|
19
|
+
* duplicate and hostile keys stay distinct, and trail entries carry the file
|
|
20
|
+
* section as well, so the same path in two files stays two entries. Status
|
|
21
|
+
* reaches the reviewer as a glyph plus color for a call that was added,
|
|
22
|
+
* removed, or contains a change, and plain dimmed text for an unchanged call.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderCallFlows(report: ReviewReport): string;
|
|
25
|
+
export declare const CALL_FLOW_STYLES = "\n.cf {\n display: flex; flex-direction: column; gap: 12px; margin-top: 18px;\n --cf-add: #1a7f37; --cf-add-soft: #dafbe1;\n --cf-del: #cf222e; --cf-del-soft: #ffebe9;\n --cf-chg: #9a6700; --cf-chg-soft: #fff8c5;\n --cf-quiet: var(--ink-soft);\n}\n@media (prefers-color-scheme: dark) {\n .cf {\n --cf-add: #3fb950; --cf-add-soft: rgba(46, 160, 67, 0.15);\n --cf-del: #f85149; --cf-del-soft: rgba(248, 81, 73, 0.12);\n --cf-chg: #d29922; --cf-chg-soft: rgba(187, 128, 9, 0.15);\n }\n}\n/* Mode sections, folded files and the breadcrumb are toggled with the hidden\n attribute, so the module carries that rule itself instead of relying on the page. */\n.cf [hidden] { display: none !important; }\n.cf-head { display: flex; flex-direction: column; gap: 4px; }\n.cf-prov { font-size: 12px; color: var(--ink-soft); }\n.cf-note { font-size: 12px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.cf-absence { padding: 16px 0; }\n.cf-bounds {\n margin: 10px 0 0;\n padding: 2px 0 2px 10px;\n border-left: 2px solid var(--cf-chg);\n color: var(--ink-soft);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.cf-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }\n.cf-ready .cf-controls { position: sticky; top: var(--toolbar-height, 0px); z-index: 4; background: var(--bg); padding: 8px 0; }\n.cf, .cf-files, .cf-file-body, .cf-mode { min-width: 0; }\n.cf-modes {\n display: inline-flex;\n align-items: stretch;\n border: 1px solid var(--line);\n border-radius: 6px;\n overflow: hidden;\n background: var(--panel);\n}\n.cf-mode-link {\n display: inline-flex;\n align-items: center;\n padding: 5px 14px;\n font-size: 12px;\n color: var(--ink-soft);\n text-decoration: none;\n border-right: 1px solid var(--line);\n}\n.cf-mode-link:last-child { border-right: 0; }\n.cf-mode-link:hover, .cf-mode-link:focus-visible { color: var(--ink); background: var(--sunken); }\n.js .cf-mode-link[aria-current] { color: var(--ink); background: var(--sunken); font-weight: 600; }\n.cf-jump ul {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n max-height: 22vh;\n overflow-y: auto;\n}\n.cf-jump-link {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n max-width: 100%;\n min-width: 0;\n padding: 4px 10px 4px 9px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--panel);\n color: var(--ink);\n font-size: 12px;\n text-decoration: none;\n}\n.cf-jump-link:hover, .cf-jump-link:focus-visible { border-color: var(--cursor); }\n.cf-jump-path { min-width: 0; max-width: 32ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.cf-jump-count { color: var(--ink-soft); white-space: nowrap; }\n.cf-crumbs {\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n gap: 6px;\n font-size: 12.5px;\n padding-bottom: 3px;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n.cf-crumb {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n max-width: 34ch;\n font: inherit;\n font-size: 12px;\n padding: 3px 10px;\n border: 1px solid var(--line-strong);\n border-radius: 999px;\n background: var(--panel);\n color: var(--ink);\n cursor: pointer;\n white-space: nowrap;\n}\n.cf-crumb[aria-current] { border-color: var(--cursor); font-weight: 600; }\n.cf-crumb-label { overflow: hidden; text-overflow: ellipsis; }\n.cf-crumb-file {\n flex: 0 0 auto;\n max-width: 22ch;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 11px;\n color: var(--ink-soft);\n}\n.cf-sep { flex: 0 0 auto; color: var(--ink-soft); }\n.cf-files { display: flex; flex-direction: column; gap: 12px; }\n.cf-file {\n background: var(--panel);\n border: 1px solid var(--line);\n border-left: 3px solid var(--line-strong);\n border-radius: 6px;\n overflow: hidden;\n}\n.cf-file { --cf-color: var(--ink-soft); --cf-fill: var(--sunken); }\n.cf-file-attention { border-left-color: var(--alarm); --cf-color: var(--alarm); --cf-fill: var(--alarm-bg); }\n.cf-file-uncertain { border-left-color: var(--warn); --cf-color: var(--warn); --cf-fill: var(--warn-bg); }\n.cf-file-low { border-left-color: var(--line-strong); }\n.cf-file-passed { border-left-color: var(--teal); --cf-color: var(--teal); --cf-fill: var(--teal-bg); }\n.cf-file-head {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 10px;\n padding: 8px 12px;\n background: var(--sunken);\n cursor: pointer;\n list-style: none;\n}\n.cf-file-head::-webkit-details-marker { display: none; }\n.cf-file-head::after {\n content: \"\u25B8\";\n margin-left: auto;\n font-size: 11px;\n color: var(--ink-soft);\n transition: transform 0.15s ease;\n}\n.cf-file[open] > .cf-file-head::after { transform: rotate(90deg); }\n.cf-file-head:focus-visible { outline: 2px solid var(--cursor); outline-offset: -2px; }\n.cf-file-path { flex: 1 1 260px; min-width: 0; font-size: 12.5px; font-weight: 600; overflow-wrap: anywhere; }\n.cf-file-count { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }\n.cf-diff-link { font-size: 12px; white-space: nowrap; }\n.cf-file-body { border-top: 1px solid var(--line); padding: 0 12px 12px; }\n.cf-mode { padding-top: 12px; }\n.cf-noscript { display: block; }\n.cf-noscript .cf-note { margin: 6px 0 0; }\n.cf-mode + .cf-mode { margin-top: 12px; border-top: 1px dashed var(--line); }\n.cf-mode-head {\n margin: 0 0 8px;\n font-size: 11.5px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--ink-soft);\n}\n.cf-ready .cf-mode-head { display: none; }\n.cf-ready .cf-mode + .cf-mode { margin-top: 0; border-top: 0; }\n.cf-tree, .cf-children { list-style: none; margin: 0; padding: 0; }\n.cf-children { margin-left: 15px; padding-left: 10px; border-left: 1px dashed var(--line); }\n.cf-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0 8px; padding: 2px 6px; border-radius: 6px; min-width: 0; }\n.cf-row:hover { background: var(--sunken); }\n.cf-fold > .cf-row { cursor: pointer; list-style: none; }\n.cf-fold > .cf-row::-webkit-details-marker { display: none; }\n.cf-fold > .cf-row::before { content: \"\u25B8\"; font-size: 10px; color: var(--ink-soft); transition: transform 0.15s ease; }\n.cf-fold[open] > .cf-row::before { transform: rotate(90deg); }\n.cf-badge, .cf-chip-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n min-width: 16px;\n height: 16px;\n border: 1px solid transparent;\n border-radius: 4px;\n font-family: var(--mono);\n font-size: 11px;\n font-weight: 700;\n}\n.cf-badge, .cf-chip-badge { color: var(--cf-node-color); background: var(--cf-node-fill); }\n.cf-label {\n font: 400 12.5px/1.5 var(--mono);\n padding: 0 2px;\n border: 1px solid transparent;\n border-radius: 4px;\n background: none;\n text-align: left;\n /* Capped so a long call never pushes its row onto a second line; the\n location takes what is left and truncates first. */\n flex: 0 1 auto;\n min-width: 0;\n max-width: 62%;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.js .cf-label { cursor: pointer; }\n.js .cf-label:hover, .cf-label:focus-visible { text-decoration: underline; }\n.cf-label:focus-visible { outline: 2px solid var(--cursor); outline-offset: 1px; }\n.cf-loc { flex: 1 1 0; min-width: 0; font-size: 11px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cf-loc-none { font-style: italic; }\n.cf-node, .cf-gnode, .cf-chip { --cf-node-color: var(--cf-chg); --cf-node-fill: var(--cf-chg-soft); --cf-node-weight: 600; --cf-node-decoration: none; }\n.cf-st-added { --cf-node-color: var(--cf-add); --cf-node-fill: var(--cf-add-soft); }\n.cf-st-removed { --cf-node-color: var(--cf-del); --cf-node-fill: var(--cf-del-soft); --cf-node-decoration: line-through; }\n.cf-node.cf-st-same, .cf-gnode.cf-st-same, .cf-chip.cf-st-same { --cf-node-color: var(--ink-soft); --cf-node-fill: var(--sunken); --cf-node-weight: 400; }\n.cf-label { color: var(--cf-node-color); font-weight: var(--cf-node-weight); text-decoration: var(--cf-node-decoration); }\n.cf-node.cf-ancestor > .cf-fold > .cf-row { display: none; }\n.cf-node.cf-ancestor > .cf-fold > .cf-children { margin: 0; padding: 0; border: 0; }\n.cf-svg-wrap {\n max-width: 100%;\n overflow: auto;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--sunken);\n overscroll-behavior: contain;\n}\n.cf-svg { display: block; max-width: none; }\n.cf-ready .cf-svg-wrap { height: clamp(220px, 42vh, 400px); overflow: hidden; touch-action: none; cursor: grab; }\n.cf-diagram-btn, .cf-diagram-close {\n display: inline-flex; align-items: center; gap: 6px; height: 29px; padding: 0 12px;\n border: 1px solid var(--line); border-radius: 6px; background: var(--panel);\n font: 500 12px/1 var(--sans, inherit); color: var(--ink); cursor: pointer;\n}\n.cf-diagram-btn { color: var(--cursor); border-color: var(--cursor); }\n.cf-diagram-btn::after { content: \"\\2197\"; font-size: 12px; }\n.cf-diagram-btn:hover, .cf-diagram-close:hover { background: var(--sunken); }\n.cf-diagram-close { margin-left: auto; }\n.cf-diagram-hint { margin: 0 0 0 auto; font-size: 12px; color: var(--ink-soft); }\n.cf-diagram-hint + .cf-diagram-close { margin-left: 12px; }\n.cf.cf-diagram-open .cf-head { display: none; }\n/* The diagram view fills the browser window, and the graph gets nearly all of its height. */\n.cf.cf-diagram-open {\n position: fixed; inset: 0; z-index: 50; margin: 0; padding: 12px 20px 20px;\n background: var(--bg); overflow: auto; overscroll-behavior: contain;\n}\n.cf.cf-diagram-open .cf-controls { position: sticky; top: -12px; z-index: 5; background: var(--bg); padding: 10px 0; }\n.cf.cf-diagram-open .cf-modes, .cf.cf-diagram-open .cf-diagram-btn { display: none !important; }\n.cf.cf-diagram-open { padding-left: 292px; }\n.cf.cf-diagram-open .cf-svg-wrap { height: calc(100vh - 150px); min-height: 320px; }\n/* One canvas at a time: the entry point picked in the list, in its own file. */\n.cf.cf-diagram-open .cf-jump, .cf.cf-diagram-open .cf-bounds, .cf.cf-diagram-open .cf-omitted { display: none !important; }\n.cf.cf-diagram-open .cf-file:not(.cf-file-current) { display: none; }\n.cf.cf-diagram-open .cf-graph-frame:not(.cf-frame-current) { display: none; }\n.cf.cf-diagram-open .cf-file-body { padding-bottom: 0; }\n.cf-diagram-nav {\n position: fixed; top: 0; bottom: 0; left: 0; width: 276px; overflow-y: auto; z-index: 6;\n padding: 16px 12px; background: var(--sunken); border-right: 1px solid var(--line);\n}\n.cf-diagram-nav-head { margin: 0 0 10px; font-size: 13px; font-weight: 600; color: var(--ink); }\n.cf-diagram-nav-file {\n margin: 14px 0 4px; font: 600 12px var(--mono); color: var(--ink-soft);\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.cf-diagram-nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }\n.cf-diagram-nav button {\n display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%;\n padding: 6px 8px; border: 0; border-radius: 6px; background: none; color: var(--ink); text-align: left; cursor: pointer;\n}\n.cf-diagram-nav button:hover { background: var(--panel); }\n.cf-diagram-nav button[aria-current] { background: var(--panel); box-shadow: inset 3px 0 0 var(--cursor); }\n.cf-diagram-nav-name { max-width: 100%; font: 600 12px/1.35 var(--mono); overflow-wrap: anywhere; }\n.cf-diagram-nav-count { max-width: 100%; font-size: 11px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n@media (max-width: 760px) {\n .cf.cf-diagram-open { padding-left: 12px; padding-top: 12px; }\n .cf-diagram-nav { position: static; width: auto; max-height: 30vh; margin: 0 0 8px; border: 1px solid var(--line); border-radius: 6px; }\n}\n.cf-ready .cf-svg-wrap:focus-visible { outline: 2px solid var(--cursor); outline-offset: 2px; }\n.cf-ready .cf-svg-wrap.cf-dragging { cursor: grabbing; user-select: none; }\n.cf-ready .cf-svg { width: 100%; height: 100%; }\n.cf-graph-frame + .cf-graph-frame { margin-top: 14px; }\n.cf-camera-tools { align-items: center; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; }\n.cf-camera-tools button { min-height: 28px; padding: 0 10px; font-size: 12px; }\n.cf-scale { min-width: 4ch; text-align: center; font: 11px var(--mono); color: var(--ink-soft); }\n.cf-omitted { display: none; }\n.cf-ready .cf-omitted { display: block; }\n.cf-node.cf-selected > .cf-row, .cf-node.cf-selected > .cf-fold > .cf-row,\n.cf-chip.cf-selected { outline: 2px solid var(--cursor); outline-offset: 1px; border-radius: 5px; }\n.cf-edge { fill: none; stroke: var(--line-strong); stroke-width: 1.5; }\n.cf-gnode rect { fill: var(--panel); stroke: var(--line-strong); stroke-width: 1.5; }\n.cf-gnode:focus-visible { outline: none; }\n.cf-gzoom circle { fill: var(--panel); stroke: var(--line-strong); }\n.cf-gzoom text { fill: var(--ink); font: 14px var(--mono); }\n.cf-gzoom:hover circle, .cf-gzoom:focus-visible circle { stroke: var(--cursor); stroke-width: 2; }\n.cf-gzoom:focus-visible { outline: none; }\n.cf-glabel { font-family: var(--mono); font-size: 12px; fill: var(--ink); }\n.cf-gloc { font-family: var(--mono); font-size: 10px; fill: var(--ink-soft); }\n.cf-gnode.cf-st-same rect { stroke: var(--line); fill: transparent; }\n.cf-gnode.cf-st-same .cf-glabel { fill: var(--ink-soft); }\n.cf-gnode:not(.cf-st-same) rect { stroke: var(--cf-node-color); fill: var(--panel); }\n.cf-gnode:not(.cf-st-same) .cf-glabel { fill: var(--cf-node-color); font-weight: 600; }\n.cf-gnode:hover rect, .cf-gnode:focus-visible rect { stroke: var(--cursor); stroke-width: 2.5; }\n.cf-gnode.cf-selected rect { stroke: var(--cursor); stroke-width: 3; }\n.cf-paths { display: flex; flex-direction: column; gap: 6px; list-style: none; margin: 0; padding: 0; }\n.cf-path {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px 6px;\n padding: 6px 8px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--panel);\n}\n.cf-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n min-width: 0;\n padding: 2px 8px 2px 4px;\n border: 1px solid var(--line);\n border-left: 3px solid var(--cf-node-color);\n border-radius: 6px;\n background: var(--panel);\n color: var(--ink);\n}\n.cf-chip:hover, .cf-chip:focus-within { border-color: var(--cursor); }\n.cf-chip-zoom {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n min-width: 0;\n color: inherit;\n text-decoration: none;\n}\n.cf-chip-label { min-width: 0; max-width: 36ch; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cf-chip-loc { min-width: 0; max-width: 28ch; font-size: 10.5px; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cf-chip.cf-st-same .cf-chip-label { color: var(--ink-soft); }\n.cf-chip:not(.cf-st-same) .cf-chip-label { color: var(--cf-node-color); font-weight: 600; }\n.cf-chip .cf-desc { max-width: 28ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cf-arrow { color: var(--ink-soft); font-size: 12px; }\n/* One description line per call, only when the backend attached one. */\n.cf-desc { font-size: 11.5px; color: var(--ink-soft); font-style: italic; overflow-wrap: anywhere; }\n.cf-row > .cf-desc { flex: 1 0 100%; order: 5; padding-left: 26px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.cf-row > .cf-src-link { order: 4; }\n.cf-depth { align-items: center; gap: 4px; }\n.cf-depth-label { font-size: 12px; color: var(--ink-soft); }\n.cf-depth-btn { padding: 3px 9px; font-size: 12px; }\n.cf-depth-btn[aria-pressed=\"true\"] { border-color: var(--cursor); color: var(--cursor); font-weight: 600; }\n/* Numbered, clickable call order on the graph edges. */\n.cf-edge-num circle { fill: var(--panel); stroke: var(--line-strong); stroke-width: 1.5; }\n.cf-edge-num text { font-family: var(--mono); font-size: 10.5px; fill: var(--ink-soft); }\n.cf-edge-num:hover circle, .cf-edge-num:focus-visible circle { stroke: var(--cursor); stroke-width: 2; }\n.cf-edge-num:hover text, .cf-edge-num:focus-visible text { fill: var(--cursor); font-weight: 700; }\n.cf-edge-num:focus-visible { outline: none; }\n.cf-gdesc { font-family: var(--mono); font-size: 10px; font-style: italic; fill: var(--ink-soft); }\n/* Resolved definition of one call: a native disclosure without the script, and\n the body the script clones into its single shared panel. */\n.cf-source { margin: 3px 0 3px 24px; }\n.cf-ready .cf-source { display: none; }\n.cf-src-sum { width: fit-content; cursor: pointer; list-style: none; font-size: 11.5px; color: var(--ink-soft); }\n.cf-src-sum::-webkit-details-marker { display: none; }\n.cf-src-sum::before { content: \"\u25B8\"; margin-right: 6px; font-size: 10px; transition: transform 0.15s ease; display: inline-block; }\n.cf-source[open] > .cf-src-sum::before { transform: rotate(90deg); }\n.cf-src-sum:hover, .cf-src-sum:focus-visible { color: var(--cursor); }\n.cf-src-body { padding: 6px 0 4px; }\n.cf-src-ref { margin: 0 0 5px; font-size: 11.5px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.cf-src-code {\n margin: 0;\n padding: 6px 9px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--sunken);\n font-family: var(--mono);\n font-size: 11.5px;\n line-height: 1.5;\n overflow-x: auto;\n}\n.cf-src-line { display: block; white-space: pre; }\n.cf-src-no {\n display: inline-block;\n width: 4ch;\n margin-right: 10px;\n text-align: right;\n color: var(--ink-soft);\n user-select: none;\n}\n.cf-src-note { margin: 0; font-size: 11.5px; color: var(--ink-soft); font-style: italic; }\n.cf-src-panel {\n padding: 8px 12px 10px;\n border: 1px solid var(--line-strong);\n border-radius: 6px;\n background: var(--panel);\n}\n.cf-src-panel-sum { width: fit-content; cursor: pointer; font-size: 12.5px; font-weight: 600; }\n.cf-src-panel-body { padding-top: 6px; }\n.cf-src-panel-body { max-height: 40vh; overflow: auto; }\n.cf-src-head-line { margin: 0 0 6px; font-size: 12px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.cf-src-missing { margin: 0; }\n.cf-src-link {\n flex: 0 0 auto;\n font-size: 11px;\n color: var(--ink-soft);\n text-decoration: none;\n white-space: nowrap;\n padding: 0 6px;\n border: 1px solid var(--line);\n border-radius: 999px;\n}\n.cf-src-link:hover { text-decoration: none; }\n.cf-src-link:hover, .cf-src-link:focus-visible { color: var(--cursor); }\n@media (max-width: 680px) {\n .cf-jump ul { max-height: 38vh; }\n .cf-jump-link { width: 100%; }\n .cf-jump-path { max-width: none; flex: 1 1 auto; }\n .cf-file-body { padding: 0 11px 12px; }\n .cf-crumb { max-width: 24ch; }\n .cf-crumb-file { max-width: 12ch; }\n .cf-source { margin-left: 10px; }\n .cf-src-no { width: 3ch; margin-right: 7px; }\n .cf-chip, .cf-chip-zoom { min-width: 0; flex-wrap: wrap; }\n .cf-chip-loc { overflow-wrap: anywhere; }\n}\n";
|
|
26
|
+
/**
|
|
27
|
+
* Progressive enhancement for the rendered call flow. Nothing here is
|
|
28
|
+
* interpolated: the visited trail, mode, depth and the source text all come from
|
|
29
|
+
* attributes and text already escaped in the DOM, and the navigation state
|
|
30
|
+
* machine is the module the unit tests call (see CALL_FLOW_NAV_SOURCE).
|
|
31
|
+
*/
|
|
32
|
+
export declare const CALL_FLOW_SCRIPT: string;
|