diffninja 0.2.0 → 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 CHANGED
@@ -11,6 +11,11 @@ submit the review to GitHub yourself.
11
11
  - **Read the important changes first.** Big PRs are hard to follow file by
12
12
  file. diffninja numbers each change, most important first, and lets you step
13
13
  through them with `j` and `k`.
14
+ - **See what the change does to the product, not just the code.** Your agent
15
+ explains the change in business terms: the processes it touches as
16
+ flowcharts, with new and changed steps highlighted, the business rules it
17
+ adds or changes (before and after), and one plain sentence on what each
18
+ function does. Call flows show that sentence above each function's name.
14
19
  - **Your agent does the first pass.** For each change, the agent answers simple
15
20
  questions: does it change behavior, is it tested, does it match the PR's
16
21
  goal. The answers sit above the code.
@@ -62,6 +67,9 @@ your machine at `127.0.0.1`). On the page:
62
67
  of what the PR is meant to do and its important limits. The full
63
68
  **Original PR description** stays one click away, with Markdown formatting.
64
69
  The goal is stated intent, not proof the code fulfills it.
70
+ Under it, **How it works** draws the business processes the change touches,
71
+ with its new and changed steps highlighted, and each change in the diff
72
+ says which step or rule it belongs to.
65
73
  `j` and `k` move to the next and previous change, and the list on the left
66
74
  shows where you are. The agent sets the reading order; the connected page
67
75
  does not label changes “Attention”.
@@ -22,7 +22,7 @@ import type { ReviewReport } from "./types.js";
22
22
  * removed, or contains a change, and plain dimmed text for an unchanged call.
23
23
  */
24
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";
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-business { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 10px; }\n.cf-business-note { font-size: 13px; color: var(--ink-soft); }\n.cf-plumbing-toggle { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--ink-soft); cursor: pointer; width: fit-content; }\n.cf:has(.cf-show-plumbing:not(:checked)) .cf-node.cf-plumbing { display: none; }\n.cf-purpose { font-family: var(--sans); font-size: 13.5px; font-weight: 550; color: var(--ink); overflow-wrap: anywhere; }\n.cf-row > .cf-purpose { flex: 1 0 100%; order: -1; padding-left: 26px; }\n.cf-fold > .cf-row > .cf-purpose { padding-left: 16px; }\n.cf-node.cf-explained > .cf-row .cf-label, .cf-node.cf-explained > .cf-fold > .cf-row .cf-label { font-size: 11.5px; }\n.cf-chip .cf-purpose { font-size: 12.5px; max-width: 34ch; }\n.cf-gpurpose { font-family: var(--sans); font-size: 12px; font-weight: 600; fill: var(--ink); }\n.cf-diagram-nav-name.cf-diagram-nav-purpose { font-family: var(--sans); white-space: normal; }\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
26
  /**
27
27
  * Progressive enhancement for the rendered call flow. Nothing here is
28
28
  * interpolated: the visited trail, mode, depth and the source text all come from
@@ -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 files = collectFiles(report.callFlows, report.items);
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
- node.description ? `<span class="cf-desc">${escapeHtml(node.description)}</span>` : "",
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 description = node.description ?? "";
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 === "" ? GRAPH_NODE_HEIGHT : GRAPH_NODE_HEIGHT + GRAPH_DESC_HEIGHT,
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
- const description = node.description ?? "";
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
- `<text class="cf-glabel" x="${box.x + 12}" y="${loc === "" && desc === "" ? box.y + 25 : box.y + 17}">${escapeHtml(label)}</text>`,
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
- node.description ? `<span class="cf-desc">${escapeHtml(node.description)}</span>` : "",
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);
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { type QuestionKind, type Verdict } from "./questions.js";
14
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 {
@@ -85,6 +102,8 @@ export interface ConnectedAnalysis {
85
102
  * author's stated intent, not a claim that the changes achieve it.
86
103
  */
87
104
  summary?: AgentSummary;
105
+ /** Present once the reviewing agent's business explanation was accepted for this report. */
106
+ explanation?: ConnectedExplanation;
88
107
  }
89
108
  export type ConnectedOrder = {
90
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;
@@ -164,5 +178,14 @@ export function connectedAnalysisOf(report, snapshotId, reviewId, reportUrl, sco
164
178
  // verbatim, attributed, and never synthesized here.
165
179
  if (report.agentSummary !== undefined)
166
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
+ }
167
190
  return analysis;
168
191
  }