diffninja 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,26 @@ export function renderConnectedPage(csrf) {
36
36
  `<p class="brand">${BRAND_MARK}<span>diffninja</span></p>`,
37
37
  '<h1 id="page-title">Pull request review</h1>',
38
38
  '<p id="page-meta" class="page-meta" hidden></p>',
39
+ '<section id="pr-goal" class="pr-goal" aria-labelledby="pr-goal-heading" hidden>',
40
+ '<h2 id="pr-goal-heading" class="pr-goal-heading">Goal</h2>',
41
+ '<p id="pr-goal-text" class="pr-goal-text"></p>',
42
+ '<p id="pr-goal-by" class="pr-goal-by" hidden></p>',
43
+ "</section>",
44
+ '<section id="pr-how" class="pr-how" aria-labelledby="pr-how-heading" hidden>',
45
+ '<details class="pr-how-fold" open>',
46
+ '<summary class="pr-how-head"><h2 id="pr-how-heading" class="pr-goal-heading">How it works</h2></summary>',
47
+ '<div class="pr-how-tools">',
48
+ '<p id="pr-how-by" class="pr-goal-by"></p>',
49
+ '<button type="button" id="pr-how-open" class="btn btn-quiet btn-sm" data-action="open-how" aria-label="Open how it works beside the diff">Open beside the diff</button>',
50
+ "</div>",
51
+ '<iframe id="pr-how-frame" class="pr-how-frame" title="How it works: the business processes and rules this change touches"></iframe>',
52
+ "</details>",
53
+ "</section>",
54
+ '<details id="pr-description" class="pr-description" hidden>',
55
+ '<summary>Original PR description</summary>',
56
+ '<p class="note">The author’s own words, as written, including any generated notes. Raw HTML is shown as text, images as links, and nothing here is loaded from another host. It is the author’s claim, not evidence that the changes achieve it.</p>',
57
+ '<div id="pr-description-body" class="pr-description-body"></div>',
58
+ "</details>",
39
59
  '<p id="lede" class="lede">Load a GitHub pull request, read its diff in a recommended order, and post your own review through the <span class="mono">gh</span> CLI. Nothing is posted until you press Submit.</p>',
40
60
  '<p id="message-note" class="note" role="status" aria-live="polite" hidden></p>',
41
61
  "</header>",
@@ -840,6 +860,8 @@ function script(csrf) {
840
860
  show(el.lede, !snap);
841
861
  show(el.loadSection, !snap || showLoad);
842
862
  show(el.pageMeta, Boolean(snap));
863
+ show(el.prDescription, Boolean(snap));
864
+ renderDescription();
843
865
  el.pageMeta.textContent = '';
844
866
  el.snapshotBody.textContent = '';
845
867
  if (!snap) {
@@ -854,10 +876,6 @@ function script(csrf) {
854
876
  el.pageMeta.appendChild(make('span', 'meta-item sha', shortSha(snap.headSha)));
855
877
  var login = reviewerLogin();
856
878
  if (login !== '') el.pageMeta.appendChild(make('span', 'meta-item', 'Reviewing as ' + login));
857
- var description = make('details', '');
858
- description.appendChild(make('summary', '', 'Description (the author\u2019s claims)'));
859
- description.appendChild(make('pre', '', typeof snap.body === 'string' && snap.body ? snap.body : 'No description.'));
860
- el.snapshotBody.appendChild(description);
861
879
  var dl = make('dl', 'facts');
862
880
  addLinkFact(dl, 'Pull request', snap.url, typeof snap.url === 'string' ? snap.url : 'unknown');
863
881
  addFact(dl, 'GitHub state', typeof snap.state === 'string' ? snap.state : 'unknown');
@@ -905,9 +923,248 @@ function script(csrf) {
905
923
  return wrap;
906
924
  }
907
925
 
926
+ /* ---------------------------------------------------------- description -- */
927
+
928
+ /**
929
+ * The author's description, rendered from the node tree the server parsed out
930
+ * of their Markdown. This page may not set markup directly, so every node
931
+ * becomes a whitelisted element and every string a text node: raw HTML
932
+ * arrives as text, an image arrives as a link, and an anchor is created only
933
+ * for an http(s) URL. The body is never summarized, shortened or rewritten
934
+ * here — the snapshot's own body text stays the untouched source, and the
935
+ * goal above it is the agent's sentence about the goal, not a rewrite of
936
+ * this text.
937
+ */
938
+ function inlineNodesInto(host, nodes, depth) {
939
+ if (!Array.isArray(nodes) || depth > 16) return;
940
+ for (var i = 0; i < nodes.length; i += 1) {
941
+ var node = nodes[i];
942
+ if (!node || typeof node !== 'object') continue;
943
+ var type = typeof node.t === 'string' ? node.t : '';
944
+ if (type === 'text') {
945
+ if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
946
+ continue;
947
+ }
948
+ if (type === 'code') {
949
+ host.appendChild(make('code', 'pr-inline-code', typeof node.v === 'string' ? node.v : ''));
950
+ continue;
951
+ }
952
+ if (type === 'br') { host.appendChild(document.createElement('br')); continue; }
953
+ if (type === 'strong' || type === 'em' || type === 'del') {
954
+ var wrapper = document.createElement(type);
955
+ inlineNodesInto(wrapper, node.c, depth + 1);
956
+ host.appendChild(wrapper);
957
+ continue;
958
+ }
959
+ if (type === 'link') {
960
+ var inner = document.createDocumentFragment();
961
+ inlineNodesInto(inner, node.c, depth + 1);
962
+ // The URL is checked here too: no anchor is built for anything but http(s).
963
+ if (typeof node.href === 'string' && /^https?:\\/\\//i.test(node.href)) {
964
+ var link = document.createElement('a');
965
+ link.href = node.href;
966
+ link.target = '_blank';
967
+ link.rel = 'noopener noreferrer';
968
+ link.appendChild(inner);
969
+ host.appendChild(link);
970
+ } else {
971
+ host.appendChild(inner);
972
+ }
973
+ continue;
974
+ }
975
+ // Anything this page does not know is shown as its own text.
976
+ if (typeof node.v === 'string') host.appendChild(document.createTextNode(node.v));
977
+ }
978
+ }
979
+
980
+ function blockNodesInto(host, nodes, depth) {
981
+ if (!Array.isArray(nodes) || depth > 16) return;
982
+ for (var i = 0; i < nodes.length; i += 1) {
983
+ var node = nodes[i];
984
+ if (!node || typeof node !== 'object') continue;
985
+ var type = typeof node.t === 'string' ? node.t : '';
986
+ if (type === 'para') {
987
+ var para = document.createElement('p');
988
+ inlineNodesInto(para, node.c, depth + 1);
989
+ host.appendChild(para);
990
+ continue;
991
+ }
992
+ if (type === 'heading') {
993
+ // Demoted: the page owns h1 and h2, so a description's headings stay below them.
994
+ var level = typeof node.d === 'number' && isFinite(node.d) ? Math.min(Math.max(Math.floor(node.d) + 2, 3), 6) : 3;
995
+ var heading = document.createElement('h' + String(level));
996
+ inlineNodesInto(heading, node.c, depth + 1);
997
+ host.appendChild(heading);
998
+ continue;
999
+ }
1000
+ if (type === 'quote') {
1001
+ var quote = document.createElement('blockquote');
1002
+ blockNodesInto(quote, node.c, depth + 1);
1003
+ host.appendChild(quote);
1004
+ continue;
1005
+ }
1006
+ if (type === 'code') {
1007
+ var lang = typeof node.lang === 'string' ? node.lang : '';
1008
+ var pre = make('pre', 'pr-code', typeof node.v === 'string' ? node.v : '');
1009
+ if (lang !== '') pre.setAttribute('data-lang', lang);
1010
+ host.appendChild(pre);
1011
+ continue;
1012
+ }
1013
+ if (type === 'raw') {
1014
+ // Markup in the description is shown verbatim, as text.
1015
+ host.appendChild(make('pre', 'pr-raw', typeof node.v === 'string' ? node.v : ''));
1016
+ continue;
1017
+ }
1018
+ if (type === 'rule') { host.appendChild(document.createElement('hr')); continue; }
1019
+ if (type === 'list') {
1020
+ var ordered = node.ordered === true;
1021
+ var list = document.createElement(ordered ? 'ol' : 'ul');
1022
+ if (ordered && typeof node.start === 'number' && isFinite(node.start) && node.start > 1) list.start = Math.floor(node.start);
1023
+ var items = Array.isArray(node.items) ? node.items : [];
1024
+ for (var j = 0; j < items.length; j += 1) {
1025
+ var item = items[j] && typeof items[j] === 'object' ? items[j] : {};
1026
+ var entry = document.createElement('li');
1027
+ if (item.task === true) {
1028
+ entry.className = 'pr-task';
1029
+ var box = document.createElement('input');
1030
+ box.type = 'checkbox';
1031
+ box.disabled = true;
1032
+ box.checked = item.checked === true;
1033
+ box.setAttribute('aria-label', item.checked === true ? 'Task done' : 'Task not done');
1034
+ entry.appendChild(box);
1035
+ }
1036
+ blockNodesInto(entry, item.c, depth + 1);
1037
+ list.appendChild(entry);
1038
+ if (!entry.hasChildNodes()) entry.appendChild(document.createTextNode('(empty)'));
1039
+ }
1040
+ host.appendChild(list);
1041
+ continue;
1042
+ }
1043
+ if (type === 'table') {
1044
+ var align = Array.isArray(node.align) ? node.align : [];
1045
+ var table = make('table', 'pr-table');
1046
+ var head = Array.isArray(node.head) ? node.head : [];
1047
+ if (head.length > 0) {
1048
+ var headRow = document.createElement('tr');
1049
+ for (var h = 0; h < head.length; h += 1) {
1050
+ var th = make('th', 'pr-align-' + (align[h] === 'center' || align[h] === 'right' ? align[h] : 'left'));
1051
+ inlineNodesInto(th, head[h], depth + 1);
1052
+ headRow.appendChild(th);
1053
+ }
1054
+ var thead = document.createElement('thead');
1055
+ thead.appendChild(headRow);
1056
+ table.appendChild(thead);
1057
+ }
1058
+ var rows = Array.isArray(node.rows) ? node.rows : [];
1059
+ var body = document.createElement('tbody');
1060
+ for (var r = 0; r < rows.length; r += 1) {
1061
+ var row = Array.isArray(rows[r]) ? rows[r] : [];
1062
+ var rowElement = document.createElement('tr');
1063
+ for (var c = 0; c < row.length; c += 1) {
1064
+ var td = make('td', 'pr-align-' + (align[c] === 'center' || align[c] === 'right' ? align[c] : 'left'));
1065
+ inlineNodesInto(td, row[c], depth + 1);
1066
+ rowElement.appendChild(td);
1067
+ }
1068
+ body.appendChild(rowElement);
1069
+ }
1070
+ table.appendChild(body);
1071
+ host.appendChild(table);
1072
+ continue;
1073
+ }
1074
+ // An unknown block that carries children or text is still readable as that.
1075
+ if (Array.isArray(node.c)) { blockNodesInto(host, node.c, depth + 1); continue; }
1076
+ if (typeof node.v === 'string' && node.v !== '') host.appendChild(make('p', null, node.v));
1077
+ }
1078
+ }
1079
+
1080
+ function renderDescription() {
1081
+ var snap = snapshot();
1082
+ var host = el.prDescriptionBody;
1083
+ host.textContent = '';
1084
+ var blocks = snap && Array.isArray(snap.bodyBlocks) ? snap.bodyBlocks : null;
1085
+ var plain = snap && typeof snap.body === 'string' ? snap.body.trim() : '';
1086
+ host.className = 'pr-description-body';
1087
+ if (blocks && blocks.length > 0) { blockNodesInto(host, blocks, 0); return; }
1088
+ // No parsed form in this response, or one this server would not hold in full:
1089
+ // the author's own bytes, unwrapped, rather than a cut-off rendering.
1090
+ if (plain !== '') {
1091
+ host.appendChild(make('div', 'pr-plain', plain));
1092
+ if (snap.bodyTooLarge === true) {
1093
+ host.appendChild(make('p', 'note', 'This description is longer than this page will format, so it is shown as the author wrote it, in full and unformatted.'));
1094
+ }
1095
+ return;
1096
+ }
1097
+ setText(host, 'The author has not provided a PR description. No expected outcome was stated.');
1098
+ }
1099
+
1100
+ /**
1101
+ * The goal the reviewing agent recorded for this revision, at the top of the
1102
+ * page: the reading the review was done against, in the agent's own words. It
1103
+ * belongs to the revision on screen — an analysis of another revision shows
1104
+ * nothing rather than a stale goal — and when none was recorded the block
1105
+ * says so instead of describing the pull request itself.
1106
+ */
1107
+ function renderGoal() {
1108
+ var snap = snapshot();
1109
+ // A revision that cannot be reviewed gets no goal block: there is nothing to aim the reading at.
1110
+ var reviewable = Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== '');
1111
+ show(el.prGoal, reviewable);
1112
+ if (!reviewable) return;
1113
+ var current = currentAnalysis();
1114
+ var source = current && current.summary && typeof current.summary === 'object' ? current.summary : null;
1115
+ var text = source && typeof source.text === 'string' ? source.text.trim() : '';
1116
+ setText(el.prGoalText, '');
1117
+ setText(el.prGoalBy, '');
1118
+ show(el.prGoalBy, false);
1119
+ if (text !== '') {
1120
+ el.prGoal.className = 'pr-goal';
1121
+ el.prGoalText.className = 'pr-goal-text';
1122
+ setText(el.prGoalText, text);
1123
+ var by = typeof source.summarizedBy === 'string' ? source.summarizedBy.trim() : '';
1124
+ setText(el.prGoalBy, 'Recorded by ' + (by === '' ? 'your review agent' : by) + ' from the pull request title and description. It is the agent\u2019s reading of the goal, not a check that the changes reach it.');
1125
+ show(el.prGoalBy, true);
1126
+ return;
1127
+ }
1128
+ el.prGoal.className = 'pr-goal is-empty';
1129
+ el.prGoalText.className = 'pr-goal-note';
1130
+ setText(el.prGoalText, goalAbsentReason(snap));
1131
+ }
1132
+
1133
+ /**
1134
+ * The agent's business view under the goal: the processes and rules this
1135
+ * change touches, drawn by the server into a same-origin frame that reports
1136
+ * its own height. Shown only once the agent's explanation was accepted for the
1137
+ * revision on screen; the frame reloads only when that revision changes.
1138
+ */
1139
+ function renderHow() {
1140
+ var snap = snapshot();
1141
+ var current = currentAnalysis();
1142
+ var explanation = current && current.explanation && typeof current.explanation === 'object' ? current.explanation : null;
1143
+ var ready = Boolean(snap) && explanation !== null && current.snapshotId === snap.id;
1144
+ show(el.prHow, ready);
1145
+ if (!ready) { if (el.prHowFrame.getAttribute('src')) el.prHowFrame.removeAttribute('src'); return; }
1146
+ var by = typeof explanation.explainedBy === 'string' && explanation.explainedBy.trim() !== '' ? explanation.explainedBy.trim() : 'your review agent';
1147
+ var processes = Array.isArray(explanation.processes) ? explanation.processes.length : 0;
1148
+ setText(el.prHowBy, 'Explained by ' + by + ' from the code: ' + processes + (processes === 1 ? ' process' : ' processes') + ' and ' + (explanation.rules || 0) + ((explanation.rules || 0) === 1 ? ' business rule' : ' business rules') + '. Its reading, not a verdict; the diff below is the ground truth.');
1149
+ var src = '/flow?snapshot=' + encodeURIComponent(current.snapshotId) + '&view=business';
1150
+ if (el.prHowFrame.getAttribute('src') !== src) el.prHowFrame.setAttribute('src', src);
1151
+ }
1152
+
1153
+ /** Why no goal is on screen: the analysis is still coming, is of another revision, or recorded none. */
1154
+ function goalAbsentReason(snap) {
1155
+ if (!analysis || analysisFor !== snap.id) return 'Waiting for the local analysis of this revision\u2026';
1156
+ if (analysis.available !== true) {
1157
+ var reason = typeof analysis.reason === 'string' && analysis.reason !== '' ? analysis.reason : 'no local analysis is attached to this session.';
1158
+ return 'No agent-stated goal for this revision: ' + reason;
1159
+ }
1160
+ if (analysis.snapshotId !== snap.id) return 'The pull request changed; waiting for the analysis of this revision before any goal is shown.';
1161
+ return 'No goal was recorded for this revision. The reviewing agent records one when it finishes its review; the original description below is the author\u2019s own words.';
1162
+ }
1163
+
908
1164
  /* ------------------------------------------------------------ analysis -- */
909
1165
 
910
1166
  var STATUS_ORDER = ['attention', 'uncertain', 'low', 'passed'];
1167
+ var CHANGE_WORD = { added: 'New', changed: 'Changed', removed: 'Removed' };
911
1168
 
912
1169
  function currentAnalysis() {
913
1170
  var snap = snapshot();
@@ -915,17 +1172,12 @@ function script(csrf) {
915
1172
  return analysis;
916
1173
  }
917
1174
 
918
- function worstStatusFor(path) {
1175
+ function fileHasUncertainty(path) {
919
1176
  var current = currentAnalysis();
920
- if (!current || !Array.isArray(current.hunks)) return '';
921
- var best = STATUS_ORDER.length;
922
- for (var i = 0; i < current.hunks.length; i += 1) {
923
- var hunk = current.hunks[i];
924
- if (!hunk || hunk.file !== path) continue;
925
- var rank = STATUS_ORDER.indexOf(hunk.status);
926
- if (rank >= 0 && rank < best) best = rank;
927
- }
928
- return best < STATUS_ORDER.length ? STATUS_ORDER[best] : '';
1177
+ if (!current || !Array.isArray(current.hunks)) return false;
1178
+ return current.hunks.some(function (hunk) {
1179
+ return hunk && hunk.file === path && hunk.status === 'uncertain';
1180
+ });
929
1181
  }
930
1182
 
931
1183
  function scheduleAnalysis(delay) {
@@ -956,7 +1208,9 @@ function script(csrf) {
956
1208
  var pendingAnswers = analysis && analysis.available === true && analysis.questions && analysis.questions.answered < analysis.questions.total;
957
1209
  var pendingOrder = analysis && analysis.available === true && !(analysis.order && analysis.order.source === 'agent');
958
1210
  var pendingSuggestions = analysis && analysis.available === true && !analysis.suggestions;
959
- if (pendingAnswers || pendingOrder || pendingSuggestions) scheduleAnalysis(10000);
1211
+ // The goal of the revision is recorded when the agent finishes, which can be after its order arrived.
1212
+ var pendingGoal = analysis && analysis.available === true && !analysis.summary;
1213
+ if (pendingAnswers || pendingOrder || pendingSuggestions || pendingGoal) scheduleAnalysis(10000);
960
1214
  });
961
1215
  }
962
1216
 
@@ -979,12 +1233,8 @@ function script(csrf) {
979
1233
  return out;
980
1234
  }
981
1235
 
982
- var STATUS_LABEL = { attention: 'Attention', uncertain: 'Uncertain', low: 'Low', passed: 'Passed' };
983
-
984
- /** A status as a small tinted label; the word is the text, the tint only repeats it. */
985
- function statusTag(status) {
986
- var known = STATUS_ORDER.indexOf(status) >= 0 ? status : 'uncertain';
987
- return make('span', 'status-tag status-' + known, STATUS_LABEL[known]);
1236
+ function uncertaintyTag() {
1237
+ return make('span', 'status-tag status-uncertain', 'Uncertain');
988
1238
  }
989
1239
 
990
1240
  /** A path with its directory muted, so the file name reads first. */
@@ -1013,7 +1263,7 @@ function script(csrf) {
1013
1263
  return go;
1014
1264
  }
1015
1265
 
1016
- /** One station of the rail: where the change is and whether it asks for attention; what to look at lives beside the code. */
1266
+ /** One station of the rail: where the change is; what to look at lives beside the code. */
1017
1267
  function renderHunkEntry(hunk, rank) {
1018
1268
  var entry = make('li', 'order-item');
1019
1269
  entry.dataset.action = 'goto-stop';
@@ -1028,8 +1278,7 @@ function script(csrf) {
1028
1278
  where.appendChild(make('span', 'path-line', ':' + String(hunk.line)));
1029
1279
  main.appendChild(where);
1030
1280
  var meta = make('div', 'order-meta');
1031
- // Only a status that asks for something is shown; low and passed stay quiet.
1032
- if (hunk.status === 'attention' || hunk.status === 'uncertain') meta.appendChild(statusTag(hunk.status));
1281
+ if (hunk.status === 'uncertain') meta.appendChild(uncertaintyTag());
1033
1282
  meta.appendChild(sizeNode(hunk.added, hunk.removed));
1034
1283
  main.appendChild(meta);
1035
1284
  var jump = make('a', 'sr-only', 'Go to change ' + rank + ', ' + hunk.file + ' line ' + hunk.line);
@@ -1049,7 +1298,8 @@ function script(csrf) {
1049
1298
  var verdicts = answeredVerdicts(hunk);
1050
1299
  var facts = Array.isArray(hunk.facts) ? hunk.facts : [];
1051
1300
  var note = typeof hunk.note === 'string' ? hunk.note : '';
1052
- if (rank === 0 && verdicts.length === 0 && facts.length === 0 && note === '') return null;
1301
+ var business = Array.isArray(hunk.business) ? hunk.business : [];
1302
+ if (rank === 0 && verdicts.length === 0 && facts.length === 0 && note === '' && business.length === 0) return null;
1053
1303
  var why = make('div', 'stop-why' + (rank > 0 ? ' has-rank' : ''));
1054
1304
  if (rank > 0) {
1055
1305
  var badge = make('span', 'why-rank', String(rank));
@@ -1057,6 +1307,19 @@ function script(csrf) {
1057
1307
  why.appendChild(badge);
1058
1308
  }
1059
1309
  var main = make('div', 'why-main');
1310
+ if (business.length > 0) {
1311
+ var steps = make('div', 'tag-row business-row');
1312
+ steps.appendChild(make('span', 'tag-lead', 'In the business'));
1313
+ for (var b = 0; b < business.length; b += 1) {
1314
+ var link = business[b];
1315
+ var change = typeof link.change === 'string' ? link.change : 'unchanged';
1316
+ var step = make('span', 'business-step change-' + change);
1317
+ if (change !== 'unchanged') step.appendChild(make('span', 'business-change', CHANGE_WORD[change] || change));
1318
+ step.appendChild(make('span', 'business-text', (typeof link.process === 'string' ? link.process + ': ' : 'Rule: ') + String(link.text)));
1319
+ steps.appendChild(step);
1320
+ }
1321
+ main.appendChild(steps);
1322
+ }
1060
1323
  if (verdicts.length > 0) {
1061
1324
  var said = make('div', 'tag-row');
1062
1325
  said.appendChild(make('span', 'sr-only', 'Your agent says: '));
@@ -1117,6 +1380,9 @@ function script(csrf) {
1117
1380
 
1118
1381
  function renderAnalysis() {
1119
1382
  var snap = snapshot();
1383
+ // The goal rides on the analysis, so it re-renders wherever the analysis does — every poll included.
1384
+ renderGoal();
1385
+ renderHow();
1120
1386
  show(el.analysisSection, Boolean(snap) && !(typeof snap.unavailableReason === 'string' && snap.unavailableReason !== ''));
1121
1387
  el.analysisBody.textContent = '';
1122
1388
  el.analysisActions.textContent = '';
@@ -1506,8 +1772,9 @@ function script(csrf) {
1506
1772
  var src = '/flow?snapshot=' + encodeURIComponent(current.snapshotId) + (path === '' ? '' : '&file=' + encodeURIComponent(path));
1507
1773
  if (el.flowFrame.getAttribute('src') !== src) el.flowFrame.setAttribute('src', src);
1508
1774
  flowSnapshot = current.snapshotId;
1509
- setText(el.flowTitle, path === '' ? 'Call flows' : 'Call flow: ' + path);
1510
- el.flowFrame.title = path === '' ? 'Call flows of every changed file' : 'Call flow of ' + path;
1775
+ var explained = Boolean(current.explanation);
1776
+ setText(el.flowTitle, path === '' ? (explained ? 'How it works and call flows' : 'Call flows') : 'Call flow: ' + path);
1777
+ el.flowFrame.title = path === '' ? (explained ? 'How it works, and the call flows of every changed file' : 'Call flows of every changed file') : 'Call flow of ' + path;
1511
1778
  if (el.flowDrawer.hidden) flowReturn = opener || document.activeElement;
1512
1779
  show(el.flowDrawer, true);
1513
1780
  document.body.classList.add('flow-open');
@@ -1531,7 +1798,16 @@ function script(csrf) {
1531
1798
 
1532
1799
  /** The diagram in the drawer opened or closed: the drawer takes the whole window while it is open. */
1533
1800
  function onFlowMessage(event_) {
1534
- if (event_.origin !== window.location.origin || event_.source !== el.flowFrame.contentWindow) return;
1801
+ if (event_.origin !== window.location.origin) return;
1802
+ // The inline business view reports its height; nothing else it sends is read.
1803
+ if (event_.source === el.prHowFrame.contentWindow) {
1804
+ var sized = event_.data;
1805
+ if (sized && sized.type === 'diffninja-business-height' && typeof sized.height === 'number' && isFinite(sized.height)) {
1806
+ el.prHowFrame.style.height = Math.max(120, Math.min(20000, Math.ceil(sized.height))) + 'px';
1807
+ }
1808
+ return;
1809
+ }
1810
+ if (event_.source !== el.flowFrame.contentWindow) return;
1535
1811
  var data = event_.data;
1536
1812
  if (!data || data.type !== 'diffninja-diagram') return;
1537
1813
  document.body.classList.toggle('flow-full', data.open === true);
@@ -1867,7 +2143,7 @@ function script(csrf) {
1867
2143
  where.appendChild(make('span', 'path-line', ':' + String(lead.line)));
1868
2144
  head.appendChild(where);
1869
2145
  head.appendChild(sizeOf([stop.segment]));
1870
- if (lead.status === 'attention' || lead.status === 'uncertain') head.appendChild(statusTag(lead.status));
2146
+ if (lead.status === 'uncertain') head.appendChild(uncertaintyTag());
1871
2147
  var tools = make('span', 'file-tools');
1872
2148
  if (hasCallFlow(stop.path)) tools.appendChild(flowButton(stop.path, 'Call flow'));
1873
2149
  head.appendChild(tools);
@@ -1891,8 +2167,7 @@ function script(csrf) {
1891
2167
  var head = make('summary', 'file-head');
1892
2168
  head.appendChild(pathNode(group.path, 'file-path'));
1893
2169
  head.appendChild(sizeOf(segments));
1894
- var worst = worstStatusFor(group.path);
1895
- if (worst === 'attention' || worst === 'uncertain') head.appendChild(statusTag(worst));
2170
+ if (fileHasUncertainty(group.path)) head.appendChild(uncertaintyTag());
1896
2171
  var tools = make('span', 'file-tools');
1897
2172
  if (hasCallFlow(group.path)) tools.appendChild(flowButton(group.path, 'Call flow'));
1898
2173
  head.appendChild(tools);
@@ -2280,6 +2555,7 @@ function script(csrf) {
2280
2555
  placeAnchor = action === 'preview' || action === 'submit' || action === 'refresh' ? node : null;
2281
2556
  if (action === 'comment') { event_.preventDefault(); addComment(node); return; }
2282
2557
  if (action === 'open-flow') { event_.preventDefault(); openFlow(node.getAttribute('data-path') || '', node); return; }
2558
+ if (action === 'open-how') { event_.preventDefault(); openFlow('', node); return; }
2283
2559
  if (action === 'close-flow') { event_.preventDefault(); closeFlow(); return; }
2284
2560
  if (action === 'add-suggestion') { event_.preventDefault(); addSuggestion(node); return; }
2285
2561
  if (action === 'add-all-suggestions') { event_.preventDefault(); addAllSuggestions(); return; }
@@ -2334,6 +2610,14 @@ function script(csrf) {
2334
2610
  el.refreshButton = byId('refresh-button');
2335
2611
  el.pageTitle = byId('page-title');
2336
2612
  el.pageMeta = byId('page-meta');
2613
+ el.prGoal = byId('pr-goal');
2614
+ el.prGoalText = byId('pr-goal-text');
2615
+ el.prGoalBy = byId('pr-goal-by');
2616
+ el.prHow = byId('pr-how');
2617
+ el.prHowBy = byId('pr-how-by');
2618
+ el.prHowFrame = byId('pr-how-frame');
2619
+ el.prDescription = byId('pr-description');
2620
+ el.prDescriptionBody = byId('pr-description-body');
2337
2621
  el.lede = byId('lede');
2338
2622
  el.loadSection = byId('load-section');
2339
2623
  el.detailsSection = byId('details-section');
@@ -2451,6 +2735,63 @@ ${BRAND_MARK_STYLES}
2451
2735
  .brand .brand-mark { width: 20px; height: 20px; }
2452
2736
  .page-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 13px; color: var(--ink-soft); }
2453
2737
  .page-meta a { font-weight: 600; }
2738
+ /* The agent's stated goal, first thing under the masthead: one sentence, with its attribution. */
2739
+ .pr-goal { margin-top: 6px; padding: 14px 16px; border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: var(--radius); background: var(--panel); }
2740
+ .pr-goal.is-empty { border-left-color: var(--line-strong); background: var(--sunken); }
2741
+ .pr-goal-heading { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
2742
+ .pr-goal-text { margin: 6px 0 0; font-size: 15.5px; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; max-width: 90ch; }
2743
+ .pr-goal-note { margin: 6px 0 0; font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; max-width: 90ch; }
2744
+ .pr-goal-by { margin: 6px 0 0; font-size: 12.5px; color: var(--ink-faint); overflow-wrap: anywhere; max-width: 90ch; }
2745
+ .pr-how { margin-top: 10px; padding: 14px 16px 10px; border: 1px solid var(--line); border-left: 3px solid var(--ok); border-radius: var(--radius); background: var(--panel); }
2746
+ .pr-how-head { cursor: pointer; width: fit-content; }
2747
+ .pr-how-head h2 { display: inline; }
2748
+ .pr-how-tools { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
2749
+ .pr-how-tools .pr-goal-by { flex: 1 1 320px; }
2750
+ .pr-how-frame { display: block; width: 100%; height: 480px; margin-top: 10px; border: 0; background: transparent; color-scheme: normal; }
2751
+ .business-row { align-items: flex-start; }
2752
+ .business-step { display: inline-flex; align-items: baseline; gap: 6px; max-width: 100%; padding: 2px 8px; border: 1px solid var(--line); border-radius: 6px; font-size: 12.5px; color: var(--ink); background: var(--panel); overflow-wrap: anywhere; }
2753
+ .business-step.change-added { border-color: var(--ok); background: var(--ok-soft); }
2754
+ .business-step.change-changed { border-color: var(--warn); background: var(--warn-soft); }
2755
+ .business-step.change-removed { border-color: var(--alarm); background: var(--alarm-bg); }
2756
+ .business-change { flex: none; font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); }
2757
+ .change-added .business-change { color: var(--ok); }
2758
+ .change-changed .business-change { color: var(--warn); }
2759
+ .change-removed .business-change { color: var(--alarm); }
2760
+ .pr-description { margin-top: 6px; padding: 12px 16px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
2761
+ .pr-description > summary { cursor: pointer; font-size: 14px; font-weight: 600; }
2762
+ .pr-description > .note { margin-top: 10px; }
2763
+ .pr-description-body { margin-top: 10px; overflow-wrap: anywhere; font: 14px/1.6 var(--sans); max-width: 90ch; }
2764
+ /* No parsed form in the response, or one too large to hold: the author's own bytes, unwrapped. */
2765
+ .pr-description-body .pr-plain { white-space: pre-wrap; }
2766
+ /* The author's Markdown, laid out as the document it is; every size stays below the page's own headings. */
2767
+ .pr-description-body > *:first-child { margin-top: 0; }
2768
+ .pr-description-body > *:last-child { margin-bottom: 0; }
2769
+ .pr-description-body p, .pr-description-body blockquote, .pr-description-body table, .pr-description-body pre, .pr-description-body ul, .pr-description-body ol { margin: 0 0 10px; }
2770
+ .pr-description-body h3, .pr-description-body h4, .pr-description-body h5, .pr-description-body h6 { margin: 16px 0 6px; line-height: 1.3; }
2771
+ .pr-description-body h3 { font-size: 15px; }
2772
+ .pr-description-body h4 { font-size: 14px; }
2773
+ .pr-description-body h5, .pr-description-body h6 { font-size: 13px; color: var(--ink-soft); }
2774
+ .pr-description-body ul, .pr-description-body ol { padding-left: 22px; }
2775
+ .pr-description-body li { margin: 2px 0; }
2776
+ .pr-description-body li > ul, .pr-description-body li > ol { margin: 2px 0 0; }
2777
+ .pr-description-body li > p { margin: 0 0 6px; }
2778
+ .pr-description-body blockquote { padding: 2px 0 2px 12px; border-left: 3px solid var(--line-strong); color: var(--ink-soft); }
2779
+ .pr-description-body hr { margin: 14px 0; border: 0; border-top: 1px solid var(--line); }
2780
+ .pr-description-body a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
2781
+ .pr-description-body code { font: 12.5px var(--mono); background: var(--sunken); border: 1px solid var(--line-soft); border-radius: 4px; padding: 1px 4px; }
2782
+ .pr-description-body pre { padding: 9px 11px; background: var(--sunken); border: 1px solid var(--line); border-radius: 6px; overflow-x: auto; font: 12.5px/1.5 var(--mono); white-space: pre; }
2783
+ .pr-description-body pre code { padding: 0; border: 0; background: none; font: inherit; }
2784
+ /* Raw HTML in a description is shown, not run: dimmed, so it reads as text the author wrote. */
2785
+ .pr-description-body .pr-raw { color: var(--ink-soft); white-space: pre-wrap; }
2786
+ .pr-description-body .pr-task { list-style: none; }
2787
+ .pr-description-body .pr-task input { margin-right: 6px; vertical-align: baseline; }
2788
+ /* The marker sits beside the item's first line, not above the paragraph it labels. */
2789
+ .pr-description-body .pr-task > p:first-of-type { display: inline; margin: 0; }
2790
+ .pr-description-body table.pr-table { width: 100%; border-collapse: collapse; font-size: 13px; display: block; overflow-x: auto; }
2791
+ .pr-description-body .pr-table th, .pr-description-body .pr-table td { border: 1px solid var(--line); padding: 4px 8px; text-align: left; vertical-align: top; }
2792
+ .pr-description-body .pr-table th { background: var(--panel-head, var(--sunken)); font-weight: 650; }
2793
+ .pr-description-body .pr-align-center { text-align: center; }
2794
+ .pr-description-body .pr-align-right { text-align: right; }
2454
2795
  .meta-item.sha { font: 12px var(--mono); padding: 2px 7px; border-radius: 6px; background: var(--neutral-soft); color: var(--ink); }
2455
2796
  .state-badge {
2456
2797
  display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: 999px;
@@ -2529,7 +2870,6 @@ body.is-busy button { cursor: progress; }
2529
2870
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
2530
2871
  color: var(--ink-soft); background: var(--neutral-soft);
2531
2872
  }
2532
- .status-tag.status-attention { color: var(--alarm); background: var(--alarm-bg); }
2533
2873
  .status-tag.status-uncertain { color: var(--warn); background: var(--warn-soft); }
2534
2874
  .tag-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
2535
2875
  .tag {
@@ -1,10 +1,22 @@
1
1
  import { type Server } from "node:http";
2
2
  import { ConnectedReview } from "./github.js";
3
+ import { type MarkdownBlock } from "./markdown.js";
3
4
  import type { ConnectedAnalysisView } from "./connected-analysis.js";
4
5
  export interface ConnectedSession {
5
6
  server: Server;
6
7
  url: string;
7
8
  }
9
+ /**
10
+ * What `GET /api/state` adds to the snapshot: the author's description already
11
+ * parsed into the nodes the page renders. The snapshot's own `body` is left
12
+ * exactly as GitHub returned it; these nodes are how the page shows it.
13
+ */
14
+ export interface ConnectedDescription {
15
+ /** The description as nodes. Absent when the body is empty, or too large to hold in full. */
16
+ readonly bodyBlocks?: readonly MarkdownBlock[];
17
+ /** The body was larger than this parser will hold: the page shows it as plain text rather than cut off. */
18
+ readonly bodyTooLarge?: true;
19
+ }
8
20
  /** Read-only extras a connected session can serve beside the review itself. */
9
21
  export interface ConnectedOptions {
10
22
  /**
@@ -14,10 +26,12 @@ export interface ConnectedOptions {
14
26
  readonly analysis?: () => Promise<ConnectedAnalysisView>;
15
27
  /**
16
28
  * The call-flow page of the analysis of snapshot `snapshotId`, for one changed
17
- * file or all of them, for `GET /flow`. Undefined when that snapshot is not
18
- * the one analyzed or it has no call flows there.
29
+ * file or all of them, for `GET /flow`; with `view` "business", the business
30
+ * view alone (`GET /flow?snapshot=&view=business`), which the page shows under
31
+ * its goal. Undefined when that snapshot is not the one analyzed or it has
32
+ * nothing to show there.
19
33
  */
20
- readonly flow?: (snapshotId: string, file: string | undefined) => Promise<string | undefined>;
34
+ readonly flow?: (snapshotId: string, file: string | undefined, view: "flow" | "business") => Promise<string | undefined>;
21
35
  }
22
36
  /** A single ephemeral session; never exposes a general GitHub API proxy. */
23
37
  export declare function serveConnected(review?: ConnectedReview, options?: ConnectedOptions): Promise<ConnectedSession>;
@@ -3,6 +3,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { ConnectedReview } from "./github.js";
5
5
  import { renderConnectedPage } from "./connected-html.js";
6
+ import { markdownBlocks } from "./markdown.js";
6
7
  import { reportPolicy } from "./report-pages.js";
7
8
  const MAX_BODY = 256 * 1024;
8
9
  const loadSchema = z.object({ url: z.string() }).strict();
@@ -12,6 +13,25 @@ const reviewSchema = z.object({
12
13
  }).strict();
13
14
  const emptySchema = z.object({}).strict();
14
15
  const tokenSchema = z.string().regex(/^[a-f0-9]{64}$/);
16
+ function described(state) {
17
+ const snap = state.snapshot;
18
+ if (snap === undefined)
19
+ return state;
20
+ const body = snap.body;
21
+ if (body === undefined || body.trim() === "")
22
+ return state;
23
+ const parsed = markdownBlocks(body);
24
+ if (parsed.truncated)
25
+ return { ...state, snapshot: { ...snap, bodyTooLarge: true } };
26
+ return { ...state, snapshot: { ...snap, bodyBlocks: parsed.blocks } };
27
+ }
28
+ function withDescription(response) {
29
+ if ("status" in response)
30
+ return described(response);
31
+ if ("state" in response && response.state !== undefined)
32
+ return { ...response, state: described(response.state) };
33
+ return response;
34
+ }
15
35
  const NO_ANALYSIS = {
16
36
  available: false,
17
37
  reason: "No local analysis is attached to this session.",
@@ -52,7 +72,7 @@ export async function serveConnected(review = new ConnectedReview(), options = {
52
72
  res.setHeader("X-Frame-Options", "DENY");
53
73
  res.setHeader("Referrer-Policy", "no-referrer");
54
74
  res.setHeader("Content-Security-Policy", `default-src 'none'; script-src 'nonce-${csrf}'; style-src 'nonce-${csrf}'; connect-src 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`);
55
- const json = (code, value) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(value)); };
75
+ const json = (code, value) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(withDescription(value))); };
56
76
  if (!requestIsTrusted(req, origin)) {
57
77
  json(403, { error: "Untrusted Host or Origin." });
58
78
  return;
@@ -79,9 +99,10 @@ export async function serveConnected(review = new ConnectedReview(), options = {
79
99
  const query = new URL(req.url ?? "", origin).searchParams;
80
100
  const snapshotId = query.get("snapshot") ?? "";
81
101
  const file = query.get("file") ?? undefined;
102
+ const view = query.get("view") === "business" ? "business" : "flow";
82
103
  let html;
83
104
  try {
84
- html = options.flow === undefined ? undefined : await options.flow(snapshotId, file);
105
+ html = options.flow === undefined ? undefined : await options.flow(snapshotId, file, view);
85
106
  }
86
107
  catch {
87
108
  html = undefined;