diffninja 0.2.0 → 0.3.1
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 +14 -3
- 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 +19 -0
- package/dist/review/connected-analysis.js +23 -0
- package/dist/review/connected-html.js +79 -4
- package/dist/review/connected.d.ts +5 -3
- package/dist/review/connected.js +2 -1
- 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/mcp.js +77 -11
- package/dist/review/process-html.d.ts +93 -0
- package/dist/review/process-html.js +525 -0
- package/dist/review/report-pages.d.ts +16 -0
- package/dist/review/report-pages.js +22 -1
- package/dist/review/service.js +3 -1
- package/dist/review/setup.d.ts +24 -6
- package/dist/review/setup.js +98 -30
- package/dist/review/types.d.ts +15 -0
- package/dist/review/version.d.ts +14 -0
- package/dist/review/version.js +44 -0
- package/package.json +1 -1
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.
|
|
@@ -43,9 +48,12 @@ npx -y diffninja@latest setup
|
|
|
43
48
|
It installs diffninja and adds it to every agent CLI it finds on your machine.
|
|
44
49
|
Then restart your agent CLI.
|
|
45
50
|
|
|
46
|
-
To update later, run the same command again
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
To update later, run the same command again: setup brings an older global
|
|
52
|
+
install up to its own version, then restart your agent CLI. (Setup from
|
|
53
|
+
0.3.0 or earlier does not update an existing install; if yours reports an
|
|
54
|
+
older version, run `npm install -g diffninja@latest` once.) To remove
|
|
55
|
+
diffninja from your agent CLIs, run `npx -y diffninja@latest setup --uninstall`.
|
|
56
|
+
Other options: `npx -y diffninja@latest setup --help`.
|
|
49
57
|
|
|
50
58
|
## How to use it
|
|
51
59
|
|
|
@@ -62,6 +70,9 @@ your machine at `127.0.0.1`). On the page:
|
|
|
62
70
|
of what the PR is meant to do and its important limits. The full
|
|
63
71
|
**Original PR description** stays one click away, with Markdown formatting.
|
|
64
72
|
The goal is stated intent, not proof the code fulfills it.
|
|
73
|
+
Under it, **How it works** draws the business processes the change touches,
|
|
74
|
+
with its new and changed steps highlighted, and each change in the diff
|
|
75
|
+
says which step or rule it belongs to.
|
|
65
76
|
`j` and `k` move to the next and previous change, and the list on the left
|
|
66
77
|
shows where you are. The agent sets the reading order; the connected page
|
|
67
78
|
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
|
|
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);
|
|
@@ -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
|
}
|