diffninja 0.3.1 → 0.3.2

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.
@@ -21,7 +21,9 @@ import type { ReviewReport } from "./types.js";
21
21
  * reaches the reviewer as a glyph plus color for a call that was added,
22
22
  * removed, or contains a change, and plain dimmed text for an unchanged call.
23
23
  */
24
- export declare function renderCallFlows(report: ReviewReport): string;
24
+ export declare function renderCallFlows(report: ReviewReport, options?: {
25
+ readonly treeOnly?: boolean;
26
+ }): string;
25
27
  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
28
  /**
27
29
  * Progressive enhancement for the rendered call flow. Nothing here is
@@ -24,7 +24,8 @@ import { nodeFunctionId, purposesOf, wrapPurpose } from "./explanation.js";
24
24
  * reaches the reviewer as a glyph plus color for a call that was added,
25
25
  * removed, or contains a change, and plain dimmed text for an unchanged call.
26
26
  */
27
- export function renderCallFlows(report) {
27
+ export function renderCallFlows(report, options = {}) {
28
+ const treeOnly = options.treeOnly === true;
28
29
  const purposes = purposesOf(report);
29
30
  const files = collectFiles(report.callFlows, report.items, purposes);
30
31
  if (files.length === 0) {
@@ -35,7 +36,7 @@ export function renderCallFlows(report) {
35
36
  '<div class="cf">',
36
37
  renderSummary(files, new Set(report.items.map(item => item.file)).size),
37
38
  renderPlumbingToggle(plumbing, report.agentExplanation?.explainedBy),
38
- renderControls(),
39
+ treeOnly ? "" : renderControls(),
39
40
  renderJump(files),
40
41
  '<nav class="cf-crumbs" id="cf-crumbs" aria-label="Visited call trail" hidden></nav>',
41
42
  // One panel the script fills with the focused call's definition; without the
@@ -45,7 +46,7 @@ export function renderCallFlows(report) {
45
46
  '<div class="cf-src-panel-body" id="cf-src-panel-body"></div>',
46
47
  "</details>",
47
48
  '<div class="cf-files">',
48
- files.map((view, index) => renderFile(view, index + 1)).join("\n"),
49
+ files.map((view, index) => renderFile(view, index + 1, treeOnly)).join("\n"),
49
50
  "</div>",
50
51
  "</div>",
51
52
  ].join("\n");
@@ -267,7 +268,7 @@ function renderJump(files) {
267
268
  ].join("\n");
268
269
  }
269
270
  /** One file section: severity, path, call count and its diff link. */
270
- function renderFile(view, at) {
271
+ function renderFile(view, at, treeOnly) {
271
272
  const { dot, tone, count } = fileFacts(view);
272
273
  const status = view.status;
273
274
  const diff = `<a class="cf-diff-link" data-open-hunk href="#item-${view.rank}">View diff #${view.rank}</a>`;
@@ -284,8 +285,8 @@ function renderFile(view, at) {
284
285
  view.truncated
285
286
  ? `<p class="cf-bounds">Bounds reached for this file: ${escapeHtml(BOUNDS_TEXT)}. Calls cut at a bound are omitted.</p>`
286
287
  : "",
287
- modesNote(),
288
- renderModes(view, at),
288
+ treeOnly ? "" : modesNote(),
289
+ treeOnly ? renderTreeMode(view, at, false) : renderModes(view, at),
289
290
  "</div>",
290
291
  "</details>",
291
292
  ]
@@ -303,7 +304,7 @@ function renderFile(view, at) {
303
304
  */
304
305
  function renderModes(view, at) {
305
306
  return [
306
- renderTreeMode(view, at),
307
+ renderTreeMode(view, at, true),
307
308
  lazyMode("graph", at, renderGraphMode(view, at)),
308
309
  lazyMode("sequence", at, renderSequenceMode(view, at)),
309
310
  ].join("\n");
@@ -321,10 +322,10 @@ function modesNote() {
321
322
  `resolved definition opens on its own. The diff holds every changed line.` +
322
323
  `</p></noscript>`);
323
324
  }
324
- function modeSection(mode, at, body) {
325
+ function modeSection(mode, at, body, headed = true) {
325
326
  return [
326
327
  `<section class="cf-mode cf-mode-${mode}" id="cf-f${at}-${mode}" data-cf-mode-body="${mode}">`,
327
- `<h3 class="cf-mode-head">${MODE_LABEL[mode]}</h3>`,
328
+ headed ? `<h3 class="cf-mode-head">${MODE_LABEL[mode]}</h3>` : "",
328
329
  body,
329
330
  "</section>",
330
331
  ].join("\n");
@@ -333,11 +334,11 @@ function modeSection(mode, at, body) {
333
334
  function lazyMode(mode, at, section) {
334
335
  return `<template data-cf-lazy-mode="${mode}" data-cf-file="${at}">${section}</template>`;
335
336
  }
336
- function renderTreeMode(view, at) {
337
+ function renderTreeMode(view, at, headed) {
337
338
  const trees = view.trees
338
339
  .map((tree, root) => renderTreeNode(tree, [root], at, view.file, view.purposes))
339
340
  .join("\n");
340
- return modeSection("tree", at, `<ul class="cf-tree">${trees}</ul>`);
341
+ return modeSection("tree", at, `<ul class="cf-tree">${trees}</ul>`, headed);
341
342
  }
342
343
  /** The resolved definition of one call. It is a native disclosure, so the actual
343
344
  * source is readable without the script, and the script clones this body into
@@ -1843,7 +1843,8 @@ function script(csrf) {
1843
1843
  for (var i = 0; i < list.length; i += 1) {
1844
1844
  var s = list[i];
1845
1845
  var shaped = s && typeof s === 'object' && typeof s.path === 'string' && typeof s.line === 'number'
1846
- && (s.side === 'LEFT' || s.side === 'RIGHT') && typeof s.body === 'string' && s.body !== '';
1846
+ && (s.side === 'LEFT' || s.side === 'RIGHT') && typeof s.body === 'string' && s.body !== ''
1847
+ && SEVERITY_LABELS[s.severity] !== undefined;
1847
1848
  if (!shaped || !currentLine(s) || settled[suggestionKey(current.snapshotId, s)]) continue;
1848
1849
  out.push(s);
1849
1850
  }
@@ -1910,12 +1911,17 @@ function script(csrf) {
1910
1911
  render();
1911
1912
  }
1912
1913
 
1914
+ var SEVERITY_LABELS = { critical: 'Critical', major: 'Major', minor: 'Minor' };
1915
+
1913
1916
  function suggestionRow(suggestion, disabled) {
1914
1917
  var wrap = make('div', 'suggestion');
1915
1918
  var head = make('p', 'suggestion-by');
1916
1919
  head.appendChild(make('span', 'agent-dot', ''));
1917
1920
  head.appendChild(make('strong', '', suggestedBy()));
1918
1921
  head.appendChild(document.createTextNode(' suggests'));
1922
+ var severity = make('span', 'severity severity-' + suggestion.severity, SEVERITY_LABELS[suggestion.severity]);
1923
+ severity.title = 'How much the agent thinks this matters';
1924
+ head.appendChild(severity);
1919
1925
  wrap.appendChild(head);
1920
1926
  wrap.appendChild(make('p', 'suggestion-body', suggestion.body));
1921
1927
  var actions = make('div', 'suggestion-actions');
@@ -3071,8 +3077,14 @@ kbd {
3071
3077
  .editor { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-soft); }
3072
3078
  .editor-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
3073
3079
  .editor-input { flex: 1 1 240px; min-width: 0; }
3074
- .suggestion-by { font-size: 12px; color: var(--ink-soft); }
3080
+ .suggestion-by { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; font-size: 12px; color: var(--ink-soft); }
3075
3081
  .suggestion-by strong { color: var(--ink); font-weight: 600; }
3082
+ .severity {
3083
+ margin-left: 6px; padding: 1px 7px; border-radius: 4px; font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
3084
+ color: var(--ink-soft); background: var(--neutral-soft);
3085
+ }
3086
+ .severity-critical { color: var(--alarm); background: var(--alarm-bg); }
3087
+ .severity-major { color: var(--warn); background: var(--warn-soft); }
3076
3088
  .suggestion-body { font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
3077
3089
  .suggestion-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
3078
3090
 
@@ -35,7 +35,8 @@ export declare function renderBusinessPage(report: ReviewReport): string;
35
35
  /**
36
36
  * The call flows of one review as a page of their own, for the connected pull
37
37
  * request page to show beside its diff: every changed file with call flows, or
38
- * only `file` when given. Tree, Graph and Sequence work as in the report; links
39
- * into the report's diff are dropped, since the page showing this has its own.
38
+ * only `file` when given. Only the Tree view is offered, and the business view
39
+ * stays out of it: the page around it already shows that view. Links into the
40
+ * report's diff are dropped, since the page showing this has its own.
40
41
  */
41
42
  export declare function renderCallFlowPage(report: ReviewReport, file?: string): string;
@@ -110,7 +110,6 @@ const BUSINESS_VIEW_STYLES = `
110
110
  .business { padding-top: 14px; }
111
111
  .business-title { font-size: 20px; }
112
112
  .business-lede { margin-top: 4px; color: var(--ink-soft); font-size: 14px; max-width: 90ch; }
113
- .flow-section-title { margin: 22px 0 0; font-size: 18px; }
114
113
  `;
115
114
  /**
116
115
  * Posts the document's height to the page that frames it, so the pull request
@@ -122,7 +121,9 @@ const BUSINESS_FRAME_SCRIPT = `
122
121
  'use strict';
123
122
  function post() {
124
123
  if (window.parent === window) return;
125
- window.parent.postMessage({ type: 'diffninja-business-height', height: Math.ceil(document.documentElement.scrollHeight) }, window.location.origin);
124
+ // The html element's own box, not scrollHeight: scrollHeight never drops below the frame's
125
+ // height, so the frame could grow but never shrink back around its content.
126
+ window.parent.postMessage({ type: 'diffninja-business-height', height: Math.ceil(document.documentElement.getBoundingClientRect().height) }, window.location.origin);
126
127
  }
127
128
  window.addEventListener('load', post);
128
129
  if (typeof ResizeObserver !== 'undefined') new ResizeObserver(post).observe(document.body);
@@ -173,8 +174,9 @@ body { background: var(--bg); font-family: var(--sans); }
173
174
  /**
174
175
  * The call flows of one review as a page of their own, for the connected pull
175
176
  * request page to show beside its diff: every changed file with call flows, or
176
- * only `file` when given. Tree, Graph and Sequence work as in the report; links
177
- * into the report's diff are dropped, since the page showing this has its own.
177
+ * only `file` when given. Only the Tree view is offered, and the business view
178
+ * stays out of it: the page around it already shows that view. Links into the
179
+ * report's diff are dropped, since the page showing this has its own.
178
180
  */
179
181
  export function renderCallFlowPage(report, file) {
180
182
  const scoped = file === undefined ? report : { ...report, callFlows: report.callFlows.filter((entry) => entry.file === file) };
@@ -186,20 +188,11 @@ export function renderCallFlowPage(report, file) {
186
188
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
187
189
  '<meta name="color-scheme" content="light dark">',
188
190
  `<title>${escapeHtml(file === undefined ? "Call flows" : `Call flow: ${file}`)}</title>`,
189
- `<style>${STYLES}\n${CALL_FLOW_STYLES}\n${PALETTE_STYLES}\n${BUSINESS_STYLES}\n${BUSINESS_VIEW_STYLES}\n${EMBEDDED_FLOW_STYLES}</style>`,
191
+ `<style>${STYLES}\n${CALL_FLOW_STYLES}\n${PALETTE_STYLES}\n${EMBEDDED_FLOW_STYLES}</style>`,
190
192
  "</head>",
191
193
  "<body>",
192
- report.agentExplanation === undefined
193
- ? ""
194
- : [
195
- '<section class="flow-embed business" aria-labelledby="flow-business-title">',
196
- `<h2 class="business-title" id="flow-business-title">${file === undefined ? "How it works" : "How this file fits the process"}</h2>`,
197
- renderBusinessView(report, { file, glossary: false }),
198
- "</section>",
199
- ].join("\n"),
200
194
  `<section id="view-call-flow" class="flow-embed${file === undefined ? "" : " flow-single"}" aria-label="Call flow">`,
201
- report.agentExplanation === undefined ? "" : '<h2 class="flow-section-title">Call flow</h2>',
202
- renderCallFlows(scoped),
195
+ renderCallFlows(scoped, { treeOnly: true }),
203
196
  "</section>",
204
197
  `<script>document.documentElement.classList.add('js');\n${CALL_FLOW_SCRIPT}</script>`,
205
198
  "</body>",
@@ -9,6 +9,7 @@ import { detectPullRequest } from "./pr-input.js";
9
9
  import { renderBusinessPage, renderCallFlowPage, renderReview } from "./html.js";
10
10
  import { MAX_SUGGESTED_COMMENTS, ReportPages } from "./report-pages.js";
11
11
  import { reviewDiff } from "./service.js";
12
+ import { COMMENT_SEVERITIES } from "./types.js";
12
13
  import { MAX_BRANCH_CHARS, MAX_DETAIL_CHARS, MAX_EXPLAINED_FUNCTIONS, MAX_PROCESSES, MAX_PROCESS_STEPS, MAX_PURPOSE_CHARS, MAX_RULES, MAX_RULE_CHARS, MAX_STEP_CHARS, MAX_STEP_EXITS, MAX_TITLE_CHARS, MIN_PROCESS_STEPS, } from "./explanation.js";
13
14
  import { packageVersion } from "./version.js";
14
15
  const PR_LINK_ERROR = "A pull request review needs exactly one full github.com pull request URL, for example https://github.com/OWNER/REPO/pull/123. Ask the user for their link; do not guess, search, or invent one.";
@@ -245,8 +246,9 @@ const commentSchema = z.object({
245
246
  line: z.number().int().positive().describe("The line number on that side."),
246
247
  side: z.enum(["LEFT", "RIGHT"]).describe("RIGHT for an added or context line (new side), LEFT for a removed line (old side)."),
247
248
  body: z.string().max(1000).describe("The comment, as the reviewer would write it: one short line, no labels or formatting."),
249
+ severity: z.enum(COMMENT_SEVERITIES).describe("How much it matters, so the human can triage: critical for a bug, data loss, security or contract break that should block the merge; major for a real risk or missing case worth fixing before merge; minor for a nit, naming or style point. Give it here, never inside the body."),
248
250
  }).strict();
249
- const COMMENT_RULES = "Only comment where a maintainer would actually ask for something or point something out: a bug, a risk, a missing case, a confusing name, a missing test; never pad. Write each one as the reviewer would type it on GitHub, in their own voice: short (one line, at most 280 characters), concrete, conversational, e.g. \"This drops the error from Close(); should we return it?\" or \"nit: could this reuse parseVersion?\". No report scaffolding: no headings, bold, list markers, numbering, or labels such as Finding, Issue, Attention, Error, Severity. Each names a line of the diff: path, line, and side RIGHT for an added or context line, LEFT for a removed line; at most one per line and 30 in all.";
251
+ const COMMENT_RULES = "Only comment where a maintainer would actually ask for something or point something out: a bug, a risk, a missing case, a confusing name, a missing test; never pad. Write each one as the reviewer would type it on GitHub, in their own voice: short (one line, at most 280 characters), concrete, conversational, e.g. \"This drops the error from Close(); should we return it?\" or \"nit: could this reuse parseVersion?\". No report scaffolding: no headings, bold, list markers, numbering, or labels such as Finding, Issue, Attention, Error, Severity. Each names a line of the diff: path, line, and side RIGHT for an added or context line, LEFT for a removed line; at most one per line and 30 in all. Each also carries a severity (critical, major or minor) in its own field, which the page shows next to the comment so the human reviews the important ones first; the words critical, major and minor do not belong in the body.";
250
252
  /**
251
253
  * What the goal summary is for. It is the agent's own paragraph for the human
252
254
  * reading the pull request, written from the author's own title and
@@ -462,12 +464,7 @@ export function createReviewServer() {
462
464
  description: "Update the line comments suggested for a pull request review after finish_review, or before it. " + COMMENT_RULES + " The whole call is refused, and the previous suggestions kept, if any comment breaks these rules. A later call replaces the earlier suggestions; an empty list clears them. Nothing is posted: the page shows each suggestion under its line, attributed to this MCP client, and the human adds it to their own review, edits it, or dismisses it. This returns no page link: only finish_review does.",
463
465
  inputSchema: z.object({
464
466
  reviewId: z.string().regex(/^[a-f0-9]{32}$/).describe("The reviewId a review_diff result returned on this connection."),
465
- comments: z.array(z.object({
466
- path: z.string().min(1).max(1024).describe("The file's path in the diff."),
467
- line: z.number().int().positive().describe("The line number on that side."),
468
- side: z.enum(["LEFT", "RIGHT"]).describe("RIGHT for an added or context line (new side), LEFT for a removed line (old side)."),
469
- body: z.string().max(1000).describe("The comment, as the reviewer would write it: one short line, no labels or formatting."),
470
- }).strict()).max(MAX_SUGGESTED_COMMENTS),
467
+ comments: z.array(commentSchema).max(MAX_SUGGESTED_COMMENTS),
471
468
  }).strict(),
472
469
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
473
470
  }, async ({ reviewId, comments }) => {
@@ -22,8 +22,6 @@ import type { ReviewReport } from "./types.js";
22
22
  export interface BusinessViewOptions {
23
23
  /** Link for a hunk id, or undefined when this page has no diff to link to. */
24
24
  readonly hunkHref?: (itemId: string) => string | undefined;
25
- /** Draw only the processes and rules that touch this changed file. */
26
- readonly file?: string;
27
25
  /** Leave out the function glossary (the call-flow view shows the purposes). */
28
26
  readonly glossary?: boolean;
29
27
  /** Leave out the attribution line when the page around it already says who explained it. */
@@ -32,9 +30,7 @@ export interface BusinessViewOptions {
32
30
  readonly stepsOpen?: boolean;
33
31
  }
34
32
  /**
35
- * The whole business view, or a note saying why there is none. `options.file`
36
- * scopes it to one changed file: the processes whose steps name a hunk or a
37
- * function of that file, and the rules that name one of its hunks.
33
+ * The whole business view, or a note saying why there is none.
38
34
  */
39
35
  export declare function renderBusinessView(report: ReviewReport, options?: BusinessViewOptions): string;
40
36
  interface StepBox {
@@ -49,9 +49,7 @@ const LANE_GAP = 18;
49
49
  const CHART_PAD = 14;
50
50
  const DECISION_INSET = 16;
51
51
  /**
52
- * The whole business view, or a note saying why there is none. `options.file`
53
- * scopes it to one changed file: the processes whose steps name a hunk or a
54
- * function of that file, and the rules that name one of its hunks.
52
+ * The whole business view, or a note saying why there is none.
55
53
  */
56
54
  export function renderBusinessView(report, options = {}) {
57
55
  const explanation = report.agentExplanation;
@@ -62,11 +60,7 @@ export function renderBusinessView(report, options = {}) {
62
60
  const purposes = purposesOf(report);
63
61
  const ranks = new Map(report.items.map((item, index) => [item.id, index + 1]));
64
62
  const itemFiles = new Map(report.items.map((item) => [item.id, item.file]));
65
- const touches = (hunks, fns) => options.file === undefined ||
66
- (hunks ?? []).some((id) => itemFiles.get(id) === options.file) ||
67
- (fns ?? []).some((id) => functions.get(id)?.file === options.file);
68
- const processes = explanation.processes.filter((process) => process.steps.some((step) => touches(step.hunks, step.functions)));
69
- const rules = explanation.rules.filter((rule) => touches(rule.hunks, undefined));
63
+ const { processes, rules } = explanation;
70
64
  const context = { functions, purposes, ranks, itemFiles, hunkHref: options.hunkHref, stepsOpen: options.stepsOpen !== false };
71
65
  const parts = [
72
66
  '<div class="bp">',
@@ -77,13 +71,10 @@ export function renderBusinessView(report, options = {}) {
77
71
  renderLegend(),
78
72
  "</div>",
79
73
  ].filter((part) => part !== "");
80
- if (processes.length === 0 && rules.length === 0) {
81
- parts.push(`<p class="bp-note">No process or rule in the explanation names this file's hunks or functions.</p>`);
82
- }
83
74
  processes.forEach((process, index) => parts.push(renderProcess(process, index + 1, context)));
84
75
  if (rules.length > 0)
85
76
  parts.push(renderRules(rules, context));
86
- if (options.glossary !== false && options.file === undefined)
77
+ if (options.glossary !== false)
87
78
  parts.push(renderGlossary(report.functions ?? [], purposes));
88
79
  parts.push("</div>");
89
80
  return parts.join("\n");
@@ -117,7 +117,7 @@ function checkComments(report, comments) {
117
117
  }
118
118
  function applyComments(report, comments, suggestedBy) {
119
119
  report.agentComments = {
120
- comments: comments.map(({ path, line, side, body }) => ({ path, line, side, body: body.trim() })),
120
+ comments: comments.map(({ path, line, side, body, severity }) => ({ path, line, side, body: body.trim(), severity })),
121
121
  suggestedBy,
122
122
  suggestedAt: new Date().toISOString(),
123
123
  };
@@ -132,12 +132,16 @@ export interface AgentOrder {
132
132
  /** diffninja's own order of the same items, kept so the page can still offer it. */
133
133
  diffninjaIds: string[];
134
134
  }
135
+ /** How much a suggested comment matters, most to least: what a reviewer triages by. */
136
+ export declare const COMMENT_SEVERITIES: readonly ["critical", "major", "minor"];
137
+ export type CommentSeverity = (typeof COMMENT_SEVERITIES)[number];
135
138
  /** One line comment the reviewing agent suggests; the human adds it to their review or not. */
136
139
  export interface SuggestedComment {
137
140
  path: string;
138
141
  line: number;
139
142
  side: "LEFT" | "RIGHT";
140
143
  body: string;
144
+ severity: CommentSeverity;
141
145
  }
142
146
  export interface AgentComments {
143
147
  comments: SuggestedComment[];
@@ -1 +1,2 @@
1
- export {};
1
+ /** How much a suggested comment matters, most to least: what a reviewer triages by. */
2
+ export const COMMENT_SEVERITIES = ["critical", "major", "minor"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffninja",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Local, deterministic PR review for coding agents over MCP: call flows, change facts, and a human review workspace",
5
5
  "type": "module",
6
6
  "exports": {