diffninja 0.1.1 → 0.3.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/README.md +92 -228
- package/dist/review/call-flow-html.d.ts +1 -1
- package/dist/review/call-flow-html.js +115 -21
- package/dist/review/connected-analysis.d.ts +29 -2
- package/dist/review/connected-analysis.js +28 -0
- package/dist/review/connected-html.js +372 -32
- package/dist/review/connected.d.ts +17 -3
- package/dist/review/connected.js +23 -2
- package/dist/review/explanation.d.ts +143 -0
- package/dist/review/explanation.js +310 -0
- package/dist/review/html.d.ts +7 -0
- package/dist/review/html.js +98 -4
- package/dist/review/markdown.d.ts +68 -0
- package/dist/review/markdown.js +339 -0
- package/dist/review/mcp.js +102 -13
- package/dist/review/process-html.d.ts +93 -0
- package/dist/review/process-html.js +525 -0
- package/dist/review/report-pages.d.ts +37 -3
- package/dist/review/report-pages.js +69 -3
- package/dist/review/service.js +3 -1
- package/dist/review/types.d.ts +34 -0
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CALL_FLOW_MAX_CHILDREN, CALL_FLOW_MAX_CROSS_FILE_CHILDREN, CALL_FLOW_MAX_DEPTH, CALL_FLOW_MAX_NODES, CALL_FLOW_MAX_ROOTS, } from "./call-flow.js";
|
|
2
2
|
import { CALL_FLOW_DEFAULT_DEPTH, CALL_FLOW_DEPTHS, CALL_FLOW_MODES, CALL_FLOW_NAV_SOURCE, } from "./call-flow-nav.js";
|
|
3
3
|
import { escapeHtml } from "./escape-html.js";
|
|
4
|
+
import { nodeFunctionId, purposesOf, wrapPurpose } from "./explanation.js";
|
|
4
5
|
/**
|
|
5
6
|
* Call-flow navigation for a review report: Tree, Graph and Sequence views of
|
|
6
7
|
* the syntactic call trees that touch each changed file.
|
|
@@ -24,13 +25,16 @@ import { escapeHtml } from "./escape-html.js";
|
|
|
24
25
|
* removed, or contains a change, and plain dimmed text for an unchanged call.
|
|
25
26
|
*/
|
|
26
27
|
export function renderCallFlows(report) {
|
|
27
|
-
const
|
|
28
|
+
const purposes = purposesOf(report);
|
|
29
|
+
const files = collectFiles(report.callFlows, report.items, purposes);
|
|
28
30
|
if (files.length === 0) {
|
|
29
31
|
return `<p class="cf-note cf-absence">${escapeHtml(AVAILABILITY_NOTE[report.callFlowAvailability])}</p>`;
|
|
30
32
|
}
|
|
33
|
+
const plumbing = purposes.size === 0 ? 0 : files.reduce((total, view) => total + countPlumbing(view.trees, purposes), 0);
|
|
31
34
|
return [
|
|
32
35
|
'<div class="cf">',
|
|
33
36
|
renderSummary(files, new Set(report.items.map(item => item.file)).size),
|
|
37
|
+
renderPlumbingToggle(plumbing, report.agentExplanation?.explainedBy),
|
|
34
38
|
renderControls(),
|
|
35
39
|
renderJump(files),
|
|
36
40
|
'<nav class="cf-crumbs" id="cf-crumbs" aria-label="Visited call trail" hidden></nav>',
|
|
@@ -100,6 +104,47 @@ const GRAPH_CHAR = 7.4;
|
|
|
100
104
|
const GRAPH_LOC_CHAR = 6.2;
|
|
101
105
|
const GRAPH_MIN_WIDTH = 132;
|
|
102
106
|
const GRAPH_MAX_WIDTH = 300;
|
|
107
|
+
/** The agent's purpose leads an explained box: up to three lines of 12px sans text. */
|
|
108
|
+
const GRAPH_PURPOSE_LINES = 3;
|
|
109
|
+
const GRAPH_PURPOSE_HEIGHT = 16;
|
|
110
|
+
const GRAPH_PURPOSE_CHAR = 7.2;
|
|
111
|
+
/** The agent's purpose for the function a node resolved to, if it wrote one. */
|
|
112
|
+
function purposeOf(node, purposes) {
|
|
113
|
+
const id = nodeFunctionId(node);
|
|
114
|
+
return id === undefined ? undefined : purposes.get(id);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether a node and everything below it is plumbing: no call in the subtree
|
|
118
|
+
* resolved to a function the agent explained. Library and framework calls are
|
|
119
|
+
* the usual case; hiding them leaves the calls the product itself makes.
|
|
120
|
+
*/
|
|
121
|
+
function isPlumbing(node, purposes) {
|
|
122
|
+
return purposeOf(node, purposes) === undefined && node.children.every((child) => isPlumbing(child, purposes));
|
|
123
|
+
}
|
|
124
|
+
/** Plumbing subtrees that the toggle hides, counted at their topmost node. */
|
|
125
|
+
function countPlumbing(trees, purposes) {
|
|
126
|
+
let total = 0;
|
|
127
|
+
for (const tree of trees)
|
|
128
|
+
total += isPlumbing(tree, purposes) ? 1 : countPlumbing(tree.children, purposes);
|
|
129
|
+
return total;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A native checkbox, so hiding plumbing needs no script: while it is unchecked
|
|
133
|
+
* the stylesheet hides every plumbing call in Tree and Sequence. The Diagram
|
|
134
|
+
* keeps every box, because hiding one would leave its edges pointing at nothing.
|
|
135
|
+
*/
|
|
136
|
+
function renderPlumbingToggle(plumbing, explainedBy) {
|
|
137
|
+
if (explainedBy === undefined)
|
|
138
|
+
return "";
|
|
139
|
+
return [
|
|
140
|
+
'<div class="cf-business">',
|
|
141
|
+
`<p class="cf-business-note">Each call shows what it does for the product first, as ${escapeHtml(explainedBy)} explained it, and its code name under it.</p>`,
|
|
142
|
+
plumbing === 0
|
|
143
|
+
? ""
|
|
144
|
+
: `<label class="cf-plumbing-toggle"><input type="checkbox" class="cf-show-plumbing"> Show ${escapeHtml(plural(plumbing, "library or framework call"))} with no product code below</label>`,
|
|
145
|
+
"</div>",
|
|
146
|
+
].join("");
|
|
147
|
+
}
|
|
103
148
|
/** Most severe hunk per file, and the earliest hunk of that severity. */
|
|
104
149
|
function hunkRanks(items) {
|
|
105
150
|
const ranks = new Map();
|
|
@@ -113,7 +158,7 @@ function hunkRanks(items) {
|
|
|
113
158
|
return ranks;
|
|
114
159
|
}
|
|
115
160
|
/** One diagram section per changed file with structured call paths. */
|
|
116
|
-
function collectFiles(callFlows, items) {
|
|
161
|
+
function collectFiles(callFlows, items, purposes) {
|
|
117
162
|
const ranks = hunkRanks(items);
|
|
118
163
|
const views = [];
|
|
119
164
|
const seen = new Set();
|
|
@@ -129,6 +174,7 @@ function collectFiles(callFlows, items) {
|
|
|
129
174
|
rank: hunk.rank,
|
|
130
175
|
trees,
|
|
131
176
|
truncated: entry.truncated === true,
|
|
177
|
+
purposes,
|
|
132
178
|
});
|
|
133
179
|
}
|
|
134
180
|
// Most severe file first; ties keep report order.
|
|
@@ -289,7 +335,7 @@ function lazyMode(mode, at, section) {
|
|
|
289
335
|
}
|
|
290
336
|
function renderTreeMode(view, at) {
|
|
291
337
|
const trees = view.trees
|
|
292
|
-
.map((tree, root) => renderTreeNode(tree, [root], at, view.file))
|
|
338
|
+
.map((tree, root) => renderTreeNode(tree, [root], at, view.file, view.purposes))
|
|
293
339
|
.join("\n");
|
|
294
340
|
return modeSection("tree", at, `<ul class="cf-tree">${trees}</ul>`);
|
|
295
341
|
}
|
|
@@ -341,11 +387,12 @@ function nodeAttrs(node, path, at) {
|
|
|
341
387
|
node.file === undefined ? "" : ` data-cf-nodefile="${escapeHtml(node.file)}"`,
|
|
342
388
|
].join("");
|
|
343
389
|
}
|
|
344
|
-
function renderTreeNode(node, path, at, changedFile) {
|
|
390
|
+
function renderTreeNode(node, path, at, changedFile, purposes) {
|
|
345
391
|
const id = path.join("-");
|
|
346
392
|
const status = node.status;
|
|
347
393
|
const inFile = node.file === changedFile;
|
|
348
394
|
const loc = locationText(node);
|
|
395
|
+
const purpose = purposeOf(node, purposes);
|
|
349
396
|
// The label is the zoom target. It stays a plain span without JavaScript (a
|
|
350
397
|
// dead button would be worse) and the script upgrades it into a real button.
|
|
351
398
|
const zoom = `<span class="cf-label mono" data-cf-zoom data-cf-file="${at}" data-cf-path="${id}"` +
|
|
@@ -356,7 +403,9 @@ function renderTreeNode(node, path, at, changedFile) {
|
|
|
356
403
|
loc === ""
|
|
357
404
|
? '<span class="cf-loc cf-loc-none">no source location</span>'
|
|
358
405
|
: `<span class="cf-loc mono">${escapeHtml(loc)}</span>`,
|
|
359
|
-
|
|
406
|
+
purpose !== undefined
|
|
407
|
+
? `<span class="cf-purpose">${escapeHtml(purpose)}</span>`
|
|
408
|
+
: node.description ? `<span class="cf-desc">${escapeHtml(node.description)}</span>` : "",
|
|
360
409
|
// An anchor, not a button: it reaches the disclosure below without the script.
|
|
361
410
|
`<a class="cf-src-link" href="#cf-f${at}-src-${id}" data-cf-source` +
|
|
362
411
|
` data-cf-file="${at}" data-cf-path="${id}">${node.source ? "source" : "details"}</a>`,
|
|
@@ -366,13 +415,17 @@ function renderTreeNode(node, path, at, changedFile) {
|
|
|
366
415
|
const classes = ["cf-node", `cf-st-${status}`];
|
|
367
416
|
if (inFile)
|
|
368
417
|
classes.push("cf-infile");
|
|
418
|
+
if (purpose !== undefined)
|
|
419
|
+
classes.push("cf-explained");
|
|
420
|
+
else if (purposes.size > 0 && isPlumbing(node, purposes))
|
|
421
|
+
classes.push("cf-plumbing");
|
|
369
422
|
const kids = node.children;
|
|
370
423
|
const source = renderSource(node, id, at);
|
|
371
424
|
if (kids.length === 0) {
|
|
372
425
|
return `<li class="${classes.join(" ")} cf-leaf"${nodeAttrs(node, id, at)}><span class="cf-row">${row}</span>${source}</li>`;
|
|
373
426
|
}
|
|
374
427
|
const children = kids
|
|
375
|
-
.map((child, childAt) => renderTreeNode(child, path.concat([childAt]), at, changedFile))
|
|
428
|
+
.map((child, childAt) => renderTreeNode(child, path.concat([childAt]), at, changedFile, purposes))
|
|
376
429
|
.join("\n");
|
|
377
430
|
return [
|
|
378
431
|
`<li class="${classes.join(" ")}"${nodeAttrs(node, id, at)}>`,
|
|
@@ -412,7 +465,7 @@ function locationText(node) {
|
|
|
412
465
|
function renderGraphMode(view, at) {
|
|
413
466
|
const figures = view.trees
|
|
414
467
|
.map((tree, root) => {
|
|
415
|
-
const layout = layoutTree(tree, [root]);
|
|
468
|
+
const layout = layoutTree(tree, [root], view.purposes);
|
|
416
469
|
const edges = layout.edges
|
|
417
470
|
.map((edge, index) => renderGraphEdge(edge, index + 1, layout.edges.length, at))
|
|
418
471
|
.join("");
|
|
@@ -446,26 +499,29 @@ function renderGraphMode(view, at) {
|
|
|
446
499
|
* The enhanced viewport frames these coordinates without shrinking text.
|
|
447
500
|
* Without JavaScript the native-size diagram scrolls inside its container.
|
|
448
501
|
*/
|
|
449
|
-
function layoutTree(root, rootPath) {
|
|
502
|
+
function layoutTree(root, rootPath, purposes) {
|
|
450
503
|
const levels = [];
|
|
451
504
|
const boxes = [];
|
|
452
505
|
const edges = [];
|
|
453
506
|
const walk = (node, path, parent) => {
|
|
454
507
|
const loc = locationText(node);
|
|
455
|
-
const
|
|
508
|
+
const purpose = purposeOf(node, purposes);
|
|
509
|
+
const description = purpose === undefined ? node.description ?? "" : "";
|
|
456
510
|
const labelChars = Math.max(1, node.label.length + (node.status === "same" ? 0 : 2));
|
|
457
|
-
const width = Math.ceil(Math.min(GRAPH_MAX_WIDTH, Math.max(GRAPH_MIN_WIDTH, 48 +
|
|
511
|
+
const width = purpose !== undefined ? GRAPH_MAX_WIDTH : Math.ceil(Math.min(GRAPH_MAX_WIDTH, Math.max(GRAPH_MIN_WIDTH, 48 +
|
|
458
512
|
Math.max(labelChars * GRAPH_CHAR, loc.length * GRAPH_LOC_CHAR, description.length * GRAPH_LOC_CHAR))));
|
|
513
|
+
const purposeLines = purpose === undefined ? [] : wrapPurpose(purpose, Math.floor((width - 48) / GRAPH_PURPOSE_CHAR), GRAPH_PURPOSE_LINES);
|
|
459
514
|
const box = {
|
|
460
515
|
node,
|
|
461
516
|
path,
|
|
462
517
|
x: 0,
|
|
463
518
|
y: 0,
|
|
464
519
|
width,
|
|
465
|
-
height: description === "" ?
|
|
520
|
+
height: GRAPH_NODE_HEIGHT + (description === "" ? 0 : GRAPH_DESC_HEIGHT) + purposeLines.length * GRAPH_PURPOSE_HEIGHT,
|
|
466
521
|
labelChars: Math.floor((width - 48) / GRAPH_CHAR),
|
|
467
522
|
locChars: Math.floor((width - 24) / GRAPH_LOC_CHAR),
|
|
468
523
|
descChars: Math.floor((width - 24) / GRAPH_LOC_CHAR),
|
|
524
|
+
purposeLines,
|
|
469
525
|
};
|
|
470
526
|
const depth = path.length - 1;
|
|
471
527
|
const level = levels[depth];
|
|
@@ -526,12 +582,14 @@ function renderGraphNode(box, at, changedFile) {
|
|
|
526
582
|
const status = node.status;
|
|
527
583
|
const loc = locationText(node);
|
|
528
584
|
const source = node.source;
|
|
529
|
-
|
|
585
|
+
// The agent's purpose replaces the docstring line: one explanation per box.
|
|
586
|
+
const description = box.purposeLines.length > 0 ? "" : node.description ?? "";
|
|
530
587
|
const desc = description === "" ? "" : clip(description, box.descChars);
|
|
531
588
|
const inFile = node.file === changedFile;
|
|
532
589
|
const action = `href="#cf-f${at}-src-${path}" data-cf-source`;
|
|
533
590
|
const title = [
|
|
534
591
|
node.label,
|
|
592
|
+
box.purposeLines.join(" "),
|
|
535
593
|
loc === "" ? "no source location" : loc,
|
|
536
594
|
STATUS_WORD[status],
|
|
537
595
|
description,
|
|
@@ -541,17 +599,23 @@ function renderGraphNode(box, at, changedFile) {
|
|
|
541
599
|
.join(" · ");
|
|
542
600
|
const mark = STATUS_MARK[status];
|
|
543
601
|
const label = clip(mark ? `${mark} ${node.label}` : node.label, box.labelChars);
|
|
602
|
+
// An explained box leads with what the call does; its code name and place follow.
|
|
603
|
+
const lead = box.purposeLines.length * GRAPH_PURPOSE_HEIGHT;
|
|
604
|
+
const purposeLines = box.purposeLines
|
|
605
|
+
.map((line, index) => `<text class="cf-gpurpose" x="${box.x + 12}" y="${box.y + 19 + index * GRAPH_PURPOSE_HEIGHT}">${escapeHtml(line)}</text>`)
|
|
606
|
+
.join("");
|
|
544
607
|
return [
|
|
545
|
-
`<a class="cf-gnode cf-st-${status}${inFile ? " cf-infile" : ""}" ${action} data-cf-file="${at}" data-cf-path="${path}">`,
|
|
608
|
+
`<a class="cf-gnode cf-st-${status}${inFile ? " cf-infile" : ""}${lead > 0 ? " cf-explained" : ""}" ${action} data-cf-file="${at}" data-cf-path="${path}">`,
|
|
546
609
|
`<title>${escapeHtml(title)}</title>`,
|
|
547
610
|
`<rect x="${box.x}" y="${box.y}" width="${box.width}" height="${box.height}" rx="7"></rect>`,
|
|
548
|
-
|
|
611
|
+
purposeLines,
|
|
612
|
+
`<text class="cf-glabel" x="${box.x + 12}" y="${lead + (loc === "" && desc === "" ? box.y + 25 : box.y + 17)}">${escapeHtml(label)}</text>`,
|
|
549
613
|
loc === ""
|
|
550
614
|
? ""
|
|
551
|
-
: `<text class="cf-gloc" x="${box.x + 12}" y="${box.y + 31}">${escapeHtml(clip(loc, box.locChars))}</text>`,
|
|
615
|
+
: `<text class="cf-gloc" x="${box.x + 12}" y="${lead + box.y + 31}">${escapeHtml(clip(loc, box.locChars))}</text>`,
|
|
552
616
|
desc === ""
|
|
553
617
|
? ""
|
|
554
|
-
: `<text class="cf-gdesc" x="${box.x + 12}" y="${loc === "" ? box.y + 31 : box.y + 44}">${escapeHtml(desc)}</text>`,
|
|
618
|
+
: `<text class="cf-gdesc" x="${box.x + 12}" y="${lead + (loc === "" ? box.y + 31 : box.y + 44)}">${escapeHtml(desc)}</text>`,
|
|
555
619
|
"</a>",
|
|
556
620
|
`<a class="cf-gzoom" href="#cf-f${at}-t-${path}" data-cf-zoom data-cf-file="${at}" data-cf-path="${path}" aria-label="Zoom into ${escapeHtml(node.label)}">`,
|
|
557
621
|
`<circle cx="${box.x + box.width - 13}" cy="${box.y + 13}" r="10"></circle>`,
|
|
@@ -570,9 +634,15 @@ function clip(text, maxChars) {
|
|
|
570
634
|
function renderSequenceMode(view, at) {
|
|
571
635
|
const paths = [];
|
|
572
636
|
view.trees.forEach((tree, root) => collectPaths(tree, [root], [], paths));
|
|
637
|
+
// With an explanation, a path of plumbing alone says nothing about the product.
|
|
638
|
+
if (view.purposes.size > 0) {
|
|
639
|
+
const explained = paths.filter((chain) => chain.some((step) => purposeOf(step.node, view.purposes) !== undefined));
|
|
640
|
+
if (explained.length > 0)
|
|
641
|
+
paths.splice(0, paths.length, ...explained);
|
|
642
|
+
}
|
|
573
643
|
const shown = Math.min(paths.length, SEQUENCE_LIMIT);
|
|
574
644
|
const note = `<p class="cf-note cf-omitted"${paths.length <= SEQUENCE_LIMIT ? " hidden" : ""}>Showing ${shown} of ${paths.length} call paths; focus a branch to narrow.</p>`;
|
|
575
|
-
return modeSection("sequence", at, `<p class="cf-note">Static call paths, not execution order.</p><ol class="cf-paths" data-cf-path-limit="${SEQUENCE_LIMIT}">${paths.map((chain) => renderPath(chain, at)).join("\n")}</ol>${note}`);
|
|
645
|
+
return modeSection("sequence", at, `<p class="cf-note">Static call paths, not execution order.</p><ol class="cf-paths" data-cf-path-limit="${SEQUENCE_LIMIT}">${paths.map((chain) => renderPath(chain, at, view.purposes)).join("\n")}</ol>${note}`);
|
|
576
646
|
}
|
|
577
647
|
function collectPaths(node, path, chain, out) {
|
|
578
648
|
const next = chain.concat([{ node, path }]);
|
|
@@ -583,18 +653,19 @@ function collectPaths(node, path, chain, out) {
|
|
|
583
653
|
}
|
|
584
654
|
kids.forEach((child, childAt) => collectPaths(child, path.concat([childAt]), next, out));
|
|
585
655
|
}
|
|
586
|
-
function renderPath(chain, at) {
|
|
656
|
+
function renderPath(chain, at, purposes) {
|
|
587
657
|
const leaf = chain[chain.length - 1];
|
|
588
658
|
const chips = chain
|
|
589
|
-
.map((step) => renderChip(step, at))
|
|
659
|
+
.map((step) => renderChip(step, at, purposes))
|
|
590
660
|
.join('<span class="cf-arrow" aria-hidden="true">→</span>');
|
|
591
661
|
return `<li class="cf-path" data-cf-file="${at}" data-cf-path="${leaf.path.join("-")}">${chips}</li>`;
|
|
592
662
|
}
|
|
593
663
|
/** A chip is the step's zoom target plus, when the backend resolved it, its own
|
|
594
664
|
* source link: without the script both are plain anchors that reach the tree. */
|
|
595
|
-
function renderChip(step, at) {
|
|
665
|
+
function renderChip(step, at, purposes) {
|
|
596
666
|
const path = step.path.join("-");
|
|
597
667
|
const node = step.node;
|
|
668
|
+
const purpose = purposeOf(node, purposes);
|
|
598
669
|
const status = node.status;
|
|
599
670
|
const loc = locationText(node);
|
|
600
671
|
const title = loc === "" ? "" : ` title="${escapeHtml(loc)}"`;
|
|
@@ -605,7 +676,9 @@ function renderChip(step, at) {
|
|
|
605
676
|
`<span class="cf-chip-label mono">${escapeHtml(node.label)}</span>`,
|
|
606
677
|
loc === "" ? "" : `<span class="cf-chip-loc mono">${escapeHtml(loc)}</span>`,
|
|
607
678
|
"</a>",
|
|
608
|
-
|
|
679
|
+
purpose !== undefined
|
|
680
|
+
? `<span class="cf-purpose">${escapeHtml(purpose)}</span>`
|
|
681
|
+
: node.description ? `<span class="cf-desc">${escapeHtml(node.description)}</span>` : "",
|
|
609
682
|
`<a class="cf-src-link" href="#cf-f${at}-src-${path}" data-cf-source` +
|
|
610
683
|
` data-cf-file="${at}" data-cf-path="${path}">${node.source ? "source" : "details"}</a>`,
|
|
611
684
|
"</span>",
|
|
@@ -957,6 +1030,17 @@ export const CALL_FLOW_STYLES = `
|
|
|
957
1030
|
.cf-arrow { color: var(--ink-soft); font-size: 12px; }
|
|
958
1031
|
/* One description line per call, only when the backend attached one. */
|
|
959
1032
|
.cf-desc { font-size: 11.5px; color: var(--ink-soft); font-style: italic; overflow-wrap: anywhere; }
|
|
1033
|
+
.cf-business { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 10px; }
|
|
1034
|
+
.cf-business-note { font-size: 13px; color: var(--ink-soft); }
|
|
1035
|
+
.cf-plumbing-toggle { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--ink-soft); cursor: pointer; width: fit-content; }
|
|
1036
|
+
.cf:has(.cf-show-plumbing:not(:checked)) .cf-node.cf-plumbing { display: none; }
|
|
1037
|
+
.cf-purpose { font-family: var(--sans); font-size: 13.5px; font-weight: 550; color: var(--ink); overflow-wrap: anywhere; }
|
|
1038
|
+
.cf-row > .cf-purpose { flex: 1 0 100%; order: -1; padding-left: 26px; }
|
|
1039
|
+
.cf-fold > .cf-row > .cf-purpose { padding-left: 16px; }
|
|
1040
|
+
.cf-node.cf-explained > .cf-row .cf-label, .cf-node.cf-explained > .cf-fold > .cf-row .cf-label { font-size: 11.5px; }
|
|
1041
|
+
.cf-chip .cf-purpose { font-size: 12.5px; max-width: 34ch; }
|
|
1042
|
+
.cf-gpurpose { font-family: var(--sans); font-size: 12px; font-weight: 600; fill: var(--ink); }
|
|
1043
|
+
.cf-diagram-nav-name.cf-diagram-nav-purpose { font-family: var(--sans); white-space: normal; }
|
|
960
1044
|
.cf-row > .cf-desc { flex: 1 0 100%; order: 5; padding-left: 26px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
961
1045
|
.cf-row > .cf-src-link { order: 4; }
|
|
962
1046
|
.cf-depth { align-items: center; gap: 4px; }
|
|
@@ -1610,6 +1694,16 @@ export const CALL_FLOW_SCRIPT = `
|
|
|
1610
1694
|
count.className = 'cf-diagram-nav-count';
|
|
1611
1695
|
var calls = frames[i].querySelectorAll('a.cf-gnode').length;
|
|
1612
1696
|
count.textContent = (dot > 0 && label.slice(0, 4) !== 'new ' ? label.slice(0, dot) + ', ' : '') + calls + (calls === 1 ? ' call' : ' calls');
|
|
1697
|
+
// An explained entry point leads with what it does for the product; its name moves under it.
|
|
1698
|
+
var lead = frames[i].querySelector('a.cf-gnode');
|
|
1699
|
+
var said = lead ? lead.querySelectorAll('.cf-gpurpose') : [];
|
|
1700
|
+
if (said.length > 0) {
|
|
1701
|
+
var words = [];
|
|
1702
|
+
for (var w = 0; w < said.length; w++) words.push(said[w].textContent);
|
|
1703
|
+
count.textContent = name.textContent + ' \u00b7 ' + count.textContent;
|
|
1704
|
+
name.textContent = words.join(' ');
|
|
1705
|
+
name.classList.add('cf-diagram-nav-purpose');
|
|
1706
|
+
}
|
|
1613
1707
|
button.appendChild(name);
|
|
1614
1708
|
button.appendChild(count);
|
|
1615
1709
|
item.appendChild(button);
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* MCP client that recorded it.
|
|
12
12
|
*/
|
|
13
13
|
import { type QuestionKind, type Verdict } from "./questions.js";
|
|
14
|
-
import type { ReviewReport, ReviewStatus, SuggestedComment } from "./types.js";
|
|
14
|
+
import type { AgentSummary, ReviewReport, ReviewStatus, SuggestedComment } from "./types.js";
|
|
15
|
+
import type { ExplanationChange } from "./explanation.js";
|
|
15
16
|
/** Most agenda entries the page lists; the full report has the rest. */
|
|
16
17
|
export declare const CONNECTED_AGENDA_LIMIT = 5;
|
|
17
18
|
export interface ConnectedFact {
|
|
@@ -45,6 +46,22 @@ export interface ConnectedHunk {
|
|
|
45
46
|
/** One fixed sentence when the hunk was not read: a metadata unit or an unread file type. */
|
|
46
47
|
note?: string;
|
|
47
48
|
readonly questions: readonly ConnectedQuestion[];
|
|
49
|
+
/** The process steps and rules the reviewing agent tied to this hunk, in its words. */
|
|
50
|
+
business?: readonly ConnectedBusinessLink[];
|
|
51
|
+
}
|
|
52
|
+
/** One step or rule of the agent's explanation that names a hunk. */
|
|
53
|
+
export interface ConnectedBusinessLink {
|
|
54
|
+
/** The process title for a step; absent for a business rule. */
|
|
55
|
+
readonly process?: string;
|
|
56
|
+
readonly text: string;
|
|
57
|
+
readonly change: ExplanationChange;
|
|
58
|
+
}
|
|
59
|
+
/** What the page needs to show the business view under the goal; the drawing itself is served by `/flow?view=business`. */
|
|
60
|
+
export interface ConnectedExplanation {
|
|
61
|
+
readonly explainedBy: string;
|
|
62
|
+
readonly processes: readonly string[];
|
|
63
|
+
readonly rules: number;
|
|
64
|
+
readonly functions: number;
|
|
48
65
|
}
|
|
49
66
|
/** Whether the analysis had a local clone's definitions and call flows. */
|
|
50
67
|
export interface ConnectedScope {
|
|
@@ -70,13 +87,23 @@ export interface ConnectedAnalysis {
|
|
|
70
87
|
readonly total: number;
|
|
71
88
|
readonly answered: number;
|
|
72
89
|
};
|
|
73
|
-
/** Line comments the reviewing agent suggested, for the human to add to their review or not. */
|
|
74
90
|
/** Changed files that have call-flow diagrams, for the page's "Call flow" buttons; empty for a patch-only analysis. */
|
|
75
91
|
readonly callFlowFiles: readonly string[];
|
|
92
|
+
/** Line comments the reviewing agent suggested, for the human to add to their review or not. */
|
|
76
93
|
suggestions?: {
|
|
77
94
|
readonly suggestedBy: string;
|
|
78
95
|
readonly comments: readonly SuggestedComment[];
|
|
79
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* The reviewing agent's own short paragraph on what the pull request does and
|
|
99
|
+
* why, attributed to the client that wrote it. Absent when no finish_review
|
|
100
|
+
* accepted one, which the page must say instead of showing a goal of its own:
|
|
101
|
+
* diffninja generates no summary, and this one is the agent's reading of the
|
|
102
|
+
* author's stated intent, not a claim that the changes achieve it.
|
|
103
|
+
*/
|
|
104
|
+
summary?: AgentSummary;
|
|
105
|
+
/** Present once the reviewing agent's business explanation was accepted for this report. */
|
|
106
|
+
explanation?: ConnectedExplanation;
|
|
80
107
|
}
|
|
81
108
|
export type ConnectedOrder = {
|
|
82
109
|
readonly source: "agent";
|
|
@@ -106,6 +106,17 @@ export function connectedAnalysisOf(report, snapshotId, reviewId, reportUrl, sco
|
|
|
106
106
|
list.push(view);
|
|
107
107
|
questionsByUnit.set(owner, list);
|
|
108
108
|
}
|
|
109
|
+
const business = new Map();
|
|
110
|
+
const link = (hunks, entry) => {
|
|
111
|
+
for (const id of new Set(hunks ?? []))
|
|
112
|
+
business.set(id, [...(business.get(id) ?? []), entry]);
|
|
113
|
+
};
|
|
114
|
+
for (const process of report.agentExplanation?.processes ?? []) {
|
|
115
|
+
for (const step of process.steps)
|
|
116
|
+
link(step.hunks, { process: process.title, text: step.text, change: step.change });
|
|
117
|
+
}
|
|
118
|
+
for (const rule of report.agentExplanation?.rules ?? [])
|
|
119
|
+
link(rule.hunks, { text: rule.text, change: rule.change });
|
|
109
120
|
const counts = { attention: 0, uncertain: 0, low: 0, passed: 0 };
|
|
110
121
|
const hunks = report.items.map((item) => {
|
|
111
122
|
counts[item.status] += 1;
|
|
@@ -136,6 +147,9 @@ export function connectedAnalysisOf(report, snapshotId, reviewId, reportUrl, sco
|
|
|
136
147
|
const note = noteOf(item);
|
|
137
148
|
if (note !== undefined)
|
|
138
149
|
hunk.note = note;
|
|
150
|
+
const links = business.get(item.id);
|
|
151
|
+
if (links !== undefined)
|
|
152
|
+
hunk.business = links;
|
|
139
153
|
return hunk;
|
|
140
154
|
});
|
|
141
155
|
const answered = report.questions.filter((question) => question.answer !== undefined).length;
|
|
@@ -159,5 +173,19 @@ export function connectedAnalysisOf(report, snapshotId, reviewId, reportUrl, sco
|
|
|
159
173
|
if (report.agentComments !== undefined) {
|
|
160
174
|
analysis.suggestions = { suggestedBy: report.agentComments.suggestedBy, comments: report.agentComments.comments };
|
|
161
175
|
}
|
|
176
|
+
// Only finish_review stores a summary, so this is present exactly when the
|
|
177
|
+
// agent's whole reading was accepted for this snapshot's report; it is copied
|
|
178
|
+
// verbatim, attributed, and never synthesized here.
|
|
179
|
+
if (report.agentSummary !== undefined)
|
|
180
|
+
analysis.summary = report.agentSummary;
|
|
181
|
+
const explanation = report.agentExplanation;
|
|
182
|
+
if (explanation !== undefined) {
|
|
183
|
+
analysis.explanation = {
|
|
184
|
+
explainedBy: explanation.explainedBy,
|
|
185
|
+
processes: explanation.processes.map((process) => process.title),
|
|
186
|
+
rules: explanation.rules.length,
|
|
187
|
+
functions: explanation.functions.length,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
162
190
|
return analysis;
|
|
163
191
|
}
|