report-shell.html 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Agent Analysis Report</title>
  7. <style>
  8. :root {
  9. --bg: #0f1b2d;
  10. --panel: #16263d;
  11. --panel-2: #1d3250;
  12. --ink: #e9eef6;
  13. --ink-dim: #9fb0c7;
  14. --line: #294366;
  15. --accent: #b66d46;
  16. --accent-ink: #f4d9c8;
  17. --critical: #e05656;
  18. --high: #e0904a;
  19. --medium: #d8c24a;
  20. --low: #5aa0d0;
  21. --ok: #4caf72;
  22. }
  23. * { box-sizing: border-box; }
  24. body {
  25. margin: 0;
  26. background: var(--bg);
  27. color: var(--ink);
  28. font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  29. }
  30. .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 80px; }
  31. header h1 { font-size: 22px; margin: 0 0 4px; }
  32. header .meta { color: var(--ink-dim); font-size: 13px; }
  33. header .meta b { color: var(--ink); font-weight: 600; }
  34. .banner {
  35. display: none;
  36. background: #3a1414;
  37. border: 1px solid var(--critical);
  38. color: #ffd9d9;
  39. padding: 14px 16px;
  40. border-radius: 8px;
  41. margin: 16px 0;
  42. white-space: pre-wrap;
  43. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  44. font-size: 13px;
  45. }
  46. .banner.show { display: block; }
  47. .overview {
  48. background: var(--panel);
  49. border: 1px solid var(--line);
  50. border-radius: 10px;
  51. padding: 18px;
  52. margin: 18px 0;
  53. }
  54. .grade {
  55. font-size: 34px;
  56. font-weight: 800;
  57. margin: 0 0 8px;
  58. text-transform: capitalize;
  59. }
  60. .grade.g-excellent { color: var(--ok); }
  61. .grade.g-good { color: var(--low); }
  62. .grade.g-fair { color: var(--medium); }
  63. .grade.g-poor { color: var(--critical); }
  64. .verdict { font-size: 16px; font-weight: 600; margin: 0 0 14px; }
  65. .summary { color: var(--ink-dim); margin: 0 0 14px; }
  66. .counts { display: flex; flex-wrap: wrap; gap: 10px; }
  67. .pill {
  68. display: inline-flex;
  69. align-items: center;
  70. gap: 8px;
  71. padding: 6px 12px;
  72. border-radius: 999px;
  73. background: var(--panel-2);
  74. border: 1px solid var(--line);
  75. font-size: 13px;
  76. }
  77. .pill .dot { width: 10px; height: 10px; border-radius: 50%; }
  78. .pill .n { font-weight: 700; }
  79. .dot.critical { background: var(--critical); }
  80. .dot.high { background: var(--high); }
  81. .dot.medium { background: var(--medium); }
  82. .dot.low { background: var(--low); }
  83. /* Agent portrait (agent_profile block) */
  84. .portrait {
  85. display: flex;
  86. align-items: center;
  87. gap: 16px;
  88. background: var(--panel);
  89. border: 1px solid var(--line);
  90. border-radius: 10px;
  91. padding: 18px;
  92. margin: 18px 0;
  93. }
  94. .portrait .icon {
  95. flex: 0 0 auto;
  96. width: 56px;
  97. height: 56px;
  98. border-radius: 12px;
  99. background: var(--panel-2);
  100. border: 1px solid var(--line);
  101. display: flex;
  102. align-items: center;
  103. justify-content: center;
  104. font-size: 30px;
  105. }
  106. .portrait .who { flex: 1 1 auto; min-width: 0; }
  107. .portrait .who .name { font-size: 18px; font-weight: 700; }
  108. .portrait .who .title { color: var(--ink-dim); font-size: 13px; margin-top: 1px; }
  109. .portrait .who .mission { margin-top: 8px; }
  110. .portrait .who .type {
  111. display: inline-block;
  112. margin-top: 8px;
  113. font-size: 11px;
  114. text-transform: uppercase;
  115. letter-spacing: 0.04em;
  116. padding: 3px 8px;
  117. border-radius: 6px;
  118. background: var(--panel-2);
  119. border: 1px solid var(--accent);
  120. color: var(--accent-ink);
  121. }
  122. /* Generic agent-block / synthesis panel */
  123. .block {
  124. background: var(--panel);
  125. border: 1px solid var(--line);
  126. border-radius: 10px;
  127. padding: 18px;
  128. margin: 18px 0;
  129. }
  130. .block > h2 {
  131. font-size: 13px;
  132. text-transform: uppercase;
  133. letter-spacing: 0.06em;
  134. color: var(--ink-dim);
  135. margin: 0 0 12px;
  136. }
  137. .cap-list { list-style: none; margin: 0; padding: 0; }
  138. .cap-list li {
  139. display: flex;
  140. align-items: baseline;
  141. gap: 10px;
  142. padding: 8px 0;
  143. border-top: 1px solid var(--line);
  144. }
  145. .cap-list li:first-child { border-top: none; }
  146. .cap-list .cap-name { font-weight: 600; flex: 0 0 auto; }
  147. .cap-list .cap-kind {
  148. flex: 0 0 auto;
  149. font-size: 11px;
  150. text-transform: uppercase;
  151. letter-spacing: 0.04em;
  152. padding: 2px 7px;
  153. border-radius: 6px;
  154. background: var(--panel-2);
  155. border: 1px solid var(--line);
  156. color: var(--ink-dim);
  157. }
  158. .cap-list .cap-note { color: var(--ink-dim); flex: 1 1 auto; min-width: 0; }
  159. .kv { margin: 0; display: grid; grid-template-columns: 150px 1fr; gap: 6px 14px; }
  160. .kv dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
  161. .kv dd { margin: 0; }
  162. .block .journey { padding: 8px 0; border-top: 1px solid var(--line); }
  163. .block .journey:first-of-type { border-top: none; padding-top: 0; }
  164. .block .journey .j-name { font-weight: 600; }
  165. .block .journey .j-steps { color: var(--ink-dim); margin-top: 2px; }
  166. .block .mono, .block code {
  167. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  168. font-size: 13px;
  169. background: var(--panel-2);
  170. padding: 1px 5px;
  171. border-radius: 4px;
  172. }
  173. /* Themes */
  174. .theme { padding: 12px 0; border-top: 1px solid var(--line); }
  175. .theme:first-of-type { border-top: none; padding-top: 0; }
  176. .theme .t-head { display: flex; align-items: center; gap: 10px; }
  177. .theme .t-title { font-weight: 600; flex: 1 1 auto; min-width: 0; }
  178. .theme .t-cause { color: var(--ink-dim); margin-top: 4px; }
  179. .theme .t-action { margin-top: 4px; }
  180. .theme .t-findings { margin-top: 8px; padding-left: 12px; border-left: 2px solid var(--line); }
  181. .theme .t-finding { font-size: 13px; color: var(--ink-dim); padding: 2px 0; }
  182. /* Strengths */
  183. .strength-list { margin: 0; padding-left: 20px; }
  184. .strength-list li { padding: 2px 0; }
  185. /* Recommendations */
  186. .rec { padding: 8px 0; border-top: 1px solid var(--line); }
  187. .rec:first-of-type { border-top: none; padding-top: 0; }
  188. .rec .rank { font-weight: 700; color: var(--accent-ink); margin-right: 8px; }
  189. .rec .resolves { color: var(--ink-dim); font-size: 12px; margin-left: 8px; }
  190. .toolbar {
  191. display: flex;
  192. align-items: center;
  193. gap: 12px;
  194. margin: 18px 0 10px;
  195. flex-wrap: wrap;
  196. }
  197. .toolbar .sel-count { color: var(--ink-dim); font-size: 13px; }
  198. button {
  199. font: inherit;
  200. cursor: pointer;
  201. border-radius: 8px;
  202. border: 1px solid var(--line);
  203. background: var(--panel-2);
  204. color: var(--ink);
  205. padding: 8px 14px;
  206. }
  207. button.primary {
  208. background: var(--accent);
  209. border-color: var(--accent);
  210. color: #1a0e07;
  211. font-weight: 600;
  212. }
  213. button:disabled { opacity: 0.5; cursor: default; }
  214. button.link {
  215. background: none;
  216. border: none;
  217. color: var(--accent-ink);
  218. padding: 4px 6px;
  219. font-size: 13px;
  220. }
  221. button.small { padding: 5px 10px; font-size: 13px; flex: 0 0 auto; }
  222. .no-findings {
  223. background: var(--panel);
  224. border: 1px dashed var(--line);
  225. border-radius: 10px;
  226. padding: 28px;
  227. text-align: center;
  228. color: var(--ink-dim);
  229. }
  230. .no-findings .big { font-size: 18px; color: var(--ok); margin-bottom: 6px; }
  231. .group { margin: 18px 0; }
  232. .group > h2 {
  233. font-size: 13px;
  234. text-transform: uppercase;
  235. letter-spacing: 0.06em;
  236. color: var(--ink-dim);
  237. margin: 0 0 8px;
  238. display: flex;
  239. align-items: center;
  240. gap: 8px;
  241. }
  242. .finding {
  243. background: var(--panel);
  244. border: 1px solid var(--line);
  245. border-left: 4px solid var(--line);
  246. border-radius: 8px;
  247. margin: 8px 0;
  248. overflow: hidden;
  249. }
  250. .finding.sev-critical { border-left-color: var(--critical); }
  251. .finding.sev-high { border-left-color: var(--high); }
  252. .finding.sev-medium { border-left-color: var(--medium); }
  253. .finding.sev-low { border-left-color: var(--low); }
  254. .finding .row {
  255. display: flex;
  256. align-items: center;
  257. gap: 12px;
  258. padding: 12px 14px;
  259. }
  260. .finding .row .chk { width: 16px; height: 16px; flex: 0 0 auto; cursor: pointer; }
  261. .finding .row .head { flex: 1 1 auto; cursor: pointer; min-width: 0; }
  262. .finding .row .title { font-weight: 600; }
  263. .finding .row .sub { color: var(--ink-dim); font-size: 12px; margin-top: 2px; }
  264. .finding .tag {
  265. flex: 0 0 auto;
  266. font-size: 11px;
  267. text-transform: uppercase;
  268. letter-spacing: 0.04em;
  269. padding: 3px 8px;
  270. border-radius: 6px;
  271. background: var(--panel-2);
  272. border: 1px solid var(--line);
  273. color: var(--ink-dim);
  274. }
  275. .finding .caret { flex: 0 0 auto; color: var(--ink-dim); transition: transform 0.15s; cursor: pointer; }
  276. .finding.open .caret { transform: rotate(90deg); }
  277. .finding .body {
  278. display: none;
  279. padding: 0 14px 14px 42px;
  280. border-top: 1px solid var(--line);
  281. }
  282. .finding.open .body { display: block; }
  283. .finding .body dl { margin: 12px 0 0; display: grid; grid-template-columns: 130px 1fr; gap: 6px 14px; }
  284. .finding .body dt { color: var(--ink-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
  285. .finding .body dd { margin: 0; }
  286. .finding .body code, .finding .body .mono {
  287. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  288. font-size: 13px;
  289. background: var(--panel-2);
  290. padding: 1px 5px;
  291. border-radius: 4px;
  292. }
  293. .toast {
  294. position: fixed;
  295. left: 50%;
  296. bottom: 28px;
  297. transform: translateX(-50%);
  298. background: var(--ok);
  299. color: #06160c;
  300. padding: 10px 18px;
  301. border-radius: 8px;
  302. font-weight: 600;
  303. opacity: 0;
  304. transition: opacity 0.2s;
  305. pointer-events: none;
  306. }
  307. .toast.show { opacity: 1; }
  308. .fallback-area { margin-top: 12px; display: none; }
  309. .fallback-area.show { display: block; }
  310. .fallback-area textarea {
  311. width: 100%;
  312. min-height: 160px;
  313. background: var(--panel-2);
  314. color: var(--ink);
  315. border: 1px solid var(--line);
  316. border-radius: 8px;
  317. padding: 10px;
  318. font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  319. font-size: 13px;
  320. }
  321. </style>
  322. </head>
  323. <body>
  324. <div class="wrap">
  325. <header>
  326. <h1>Agent Analysis Report</h1>
  327. <div class="meta">
  328. <span>Subject: <b id="m-subject">—</b></span> &nbsp;·&nbsp;
  329. <span>Generated: <b id="m-generated">—</b></span> &nbsp;·&nbsp;
  330. <span>Schema: <b id="m-schema">—</b></span>
  331. </div>
  332. </header>
  333. <div id="parse-banner" class="banner"></div>
  334. <!-- Agent portrait (agent_profile). Hidden when the block is absent. -->
  335. <section id="portrait" class="portrait" hidden></section>
  336. <section id="overview" class="overview" hidden>
  337. <div id="grade" class="grade" hidden></div>
  338. <p id="verdict" class="verdict"></p>
  339. <p id="summary-text" class="summary" hidden></p>
  340. <div id="counts" class="counts"></div>
  341. </section>
  342. <!-- Capability dashboard (capabilities). Hidden when absent. -->
  343. <section id="capabilities" class="block" hidden></section>
  344. <!-- Per-lens verdicts (detailed_analysis). Hidden when absent. -->
  345. <section id="lens-verdicts" class="block" hidden></section>
  346. <!-- Sanctum block (sanctum). Conditional; hidden for stateless agents. -->
  347. <section id="sanctum" class="block" hidden></section>
  348. <!-- Experience: journeys plus headless (experience). Hidden when absent. -->
  349. <section id="experience" class="block" hidden></section>
  350. <!-- Synthesis layer (themes, strengths, recommendations). Hidden when absent. -->
  351. <section id="themes" class="block" hidden></section>
  352. <section id="strengths" class="block" hidden></section>
  353. <section id="recommendations" class="block" hidden></section>
  354. <div id="toolbar" class="toolbar" hidden>
  355. <button id="btn-copy" class="primary" disabled>Copy selected as paste-back prompt</button>
  356. <span id="sel-count" class="sel-count">0 selected</span>
  357. <button id="btn-select-all" class="link">Select all</button>
  358. <button id="btn-clear" class="link">Clear</button>
  359. <button id="btn-expand-all" class="link">Expand all</button>
  360. <button id="btn-collapse-all" class="link">Collapse all</button>
  361. </div>
  362. <div id="fallback" class="fallback-area">
  363. <p class="sel-count">Clipboard was unavailable. Copy the text below manually:</p>
  364. <textarea id="fallback-text" readonly></textarea>
  365. </div>
  366. <div id="findings-root"></div>
  367. </div>
  368. <div id="toast" class="toast">Copied</div>
  369. <!-- scripts/render_report.py replaces the contents of this island per run.
  370. The placeholder below is intentionally unusable: the shell refuses to
  371. render it, so a failed injection can never look like real findings. -->
  372. <script type="application/json" id="report-data">
  373. {
  374. "schema_version": 2,
  375. "subject": "__PLACEHOLDER__",
  376. "generated": "",
  377. "verdict": "",
  378. "findings": []
  379. }
  380. </script>
  381. <script>
  382. (function () {
  383. "use strict";
  384. var SEVERITIES = ["critical", "high", "medium", "low"];
  385. var SEV_LABEL = { critical: "Critical", high: "High", medium: "Medium", low: "Low" };
  386. var GRADES = ["excellent", "good", "fair", "poor"];
  387. var PLACEHOLDER_SUBJECT = "__PLACEHOLDER__";
  388. var els = {
  389. banner: document.getElementById("parse-banner"),
  390. portrait: document.getElementById("portrait"),
  391. overview: document.getElementById("overview"),
  392. grade: document.getElementById("grade"),
  393. verdict: document.getElementById("verdict"),
  394. summaryText: document.getElementById("summary-text"),
  395. counts: document.getElementById("counts"),
  396. capabilities: document.getElementById("capabilities"),
  397. lensVerdicts: document.getElementById("lens-verdicts"),
  398. sanctum: document.getElementById("sanctum"),
  399. experience: document.getElementById("experience"),
  400. themes: document.getElementById("themes"),
  401. strengths: document.getElementById("strengths"),
  402. recommendations: document.getElementById("recommendations"),
  403. toolbar: document.getElementById("toolbar"),
  404. root: document.getElementById("findings-root"),
  405. subject: document.getElementById("m-subject"),
  406. generated: document.getElementById("m-generated"),
  407. schema: document.getElementById("m-schema"),
  408. selCount: document.getElementById("sel-count"),
  409. btnCopy: document.getElementById("btn-copy"),
  410. btnSelectAll: document.getElementById("btn-select-all"),
  411. btnClear: document.getElementById("btn-clear"),
  412. btnExpandAll: document.getElementById("btn-expand-all"),
  413. btnCollapseAll: document.getElementById("btn-collapse-all"),
  414. fallback: document.getElementById("fallback"),
  415. fallbackText: document.getElementById("fallback-text"),
  416. toast: document.getElementById("toast")
  417. };
  418. var selected = Object.create(null);
  419. var findings = [];
  420. var findingsById = Object.create(null);
  421. var subjectPath = "";
  422. var standards = null;
  423. function showBanner(message) {
  424. els.banner.textContent = message;
  425. els.banner.classList.add("show");
  426. }
  427. function esc(value) {
  428. var s = value == null ? "" : String(value);
  429. return s.replace(/[&<>"']/g, function (c) {
  430. return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
  431. });
  432. }
  433. // Normalize an arbitrary parsed object against schema_version 2, supplying
  434. // defaults so a partial or future island still renders. Unknown fields are
  435. // ignored, not fatal. Severity counts are always derived from the findings
  436. // array, never read from the island, so they cannot disagree with it. The
  437. // agent blocks (agent_profile, capabilities, detailed_analysis, sanctum,
  438. // experience) and the synthesis blocks (grade, summary, themes, strengths,
  439. // recommendations) are OPTIONAL: each normalizes to an empty value that
  440. // renders nothing rather than an empty panel or an error.
  441. function normalize(raw) {
  442. var obj = raw && typeof raw === "object" ? raw : {};
  443. var rawFindings = Array.isArray(obj.findings) ? obj.findings : [];
  444. var norm = {
  445. schema_version: typeof obj.schema_version === "number" ? obj.schema_version : 2,
  446. subject: obj.subject != null ? String(obj.subject) : "(unspecified)",
  447. generated: obj.generated != null ? String(obj.generated) : "(unspecified)",
  448. verdict: obj.verdict != null ? String(obj.verdict) : "(no verdict supplied)",
  449. grade: GRADES.indexOf(String(obj.grade || "").toLowerCase()) >= 0
  450. ? String(obj.grade).toLowerCase() : "",
  451. summary: typeof obj.summary === "string" ? obj.summary : "",
  452. standards: (obj.standards && typeof obj.standards === "object") ? {
  453. canon: obj.standards.canon != null ? String(obj.standards.canon) : "",
  454. principles: obj.standards.principles != null ? String(obj.standards.principles) : "",
  455. scripts: obj.standards.scripts != null ? String(obj.standards.scripts) : ""
  456. } : null,
  457. themes: normalizeThemes(obj.themes),
  458. strengths: normalizeStrengths(obj.strengths),
  459. recommendations: normalizeRecommendations(obj.recommendations),
  460. counts: { critical: 0, high: 0, medium: 0, low: 0 },
  461. findings: [],
  462. agent_profile: normalizeProfile(obj.agent_profile),
  463. capabilities: normalizeCapabilities(obj.capabilities),
  464. detailed_analysis: normalizeDetailed(obj.detailed_analysis),
  465. sanctum: normalizeSanctum(obj.sanctum),
  466. experience: normalizeExperience(obj.experience)
  467. };
  468. rawFindings.forEach(function (f, i) {
  469. if (!f || typeof f !== "object") { return; }
  470. var sev = SEVERITIES.indexOf(f.severity) >= 0 ? f.severity : "low";
  471. norm.findings.push({
  472. id: f.id != null ? String(f.id) : "finding-" + (i + 1),
  473. lens: f.lens != null ? String(f.lens) : "(unknown)",
  474. severity: sev,
  475. title: f.title != null ? String(f.title) : "(untitled finding)",
  476. location: f.location != null ? String(f.location) : "",
  477. evidence: f.evidence != null ? String(f.evidence) : "",
  478. recommendation: f.recommendation != null ? String(f.recommendation) : "",
  479. proposed_smallest: f.proposed_smallest != null ? String(f.proposed_smallest) : "",
  480. predicted_delta: f.predicted_delta != null ? String(f.predicted_delta) : ""
  481. });
  482. norm.counts[sev] += 1;
  483. });
  484. return norm;
  485. }
  486. function normalizeThemes(raw) {
  487. if (!Array.isArray(raw)) { return []; }
  488. var list = [];
  489. raw.forEach(function (t) {
  490. if (!t || typeof t !== "object") { return; }
  491. var ids = [];
  492. if (Array.isArray(t.finding_ids)) {
  493. t.finding_ids.forEach(function (id) { if (id != null) { ids.push(String(id)); } });
  494. }
  495. var title = t.title != null ? String(t.title) : "";
  496. if (!title && !ids.length) { return; }
  497. list.push({
  498. title: title || "(untitled theme)",
  499. root_cause: t.root_cause != null ? String(t.root_cause) : "",
  500. action: t.action != null ? String(t.action) : "",
  501. finding_ids: ids
  502. });
  503. });
  504. return list;
  505. }
  506. function normalizeStrengths(raw) {
  507. if (!Array.isArray(raw)) { return []; }
  508. var list = [];
  509. raw.forEach(function (s) {
  510. if (typeof s === "string" && s) { list.push(s); }
  511. else if (s && typeof s === "object" && s.title) {
  512. list.push(String(s.title) + (s.detail ? " — " + String(s.detail) : ""));
  513. }
  514. });
  515. return list;
  516. }
  517. function normalizeRecommendations(raw) {
  518. if (!Array.isArray(raw)) { return []; }
  519. var list = [];
  520. raw.forEach(function (r, i) {
  521. if (!r || typeof r !== "object") { return; }
  522. var action = r.action != null ? String(r.action) : "";
  523. if (!action) { return; }
  524. var resolves = "";
  525. if (Array.isArray(r.resolves)) { resolves = r.resolves.map(String).join(", "); }
  526. else if (typeof r.resolves === "number") { resolves = r.resolves + " findings"; }
  527. else if (r.resolves != null) { resolves = String(r.resolves); }
  528. list.push({
  529. rank: typeof r.rank === "number" ? r.rank : i + 1,
  530. action: action,
  531. resolves: resolves
  532. });
  533. });
  534. list.sort(function (a, b) { return a.rank - b.rank; });
  535. return list;
  536. }
  537. // Optional agent_profile portrait: name/title/icon/agent_type/mission.
  538. // Returns null when nothing usable is present, so the portrait stays hidden.
  539. function normalizeProfile(raw) {
  540. if (!raw || typeof raw !== "object") { return null; }
  541. var p = {
  542. name: raw.name != null ? String(raw.name) : "",
  543. title: raw.title != null ? String(raw.title) : "",
  544. icon: raw.icon != null ? String(raw.icon) : "",
  545. agent_type: raw.agent_type != null ? String(raw.agent_type) : "",
  546. mission: raw.mission != null ? String(raw.mission) : ""
  547. };
  548. if (!p.name && !p.title && !p.mission && !p.agent_type) { return null; }
  549. return p;
  550. }
  551. // Optional capability dashboard: a list of { name, kind, note }.
  552. // Returns null when empty or absent.
  553. function normalizeCapabilities(raw) {
  554. if (!Array.isArray(raw)) { return null; }
  555. var list = [];
  556. raw.forEach(function (c) {
  557. if (!c || typeof c !== "object") { return; }
  558. var name = c.name != null ? String(c.name) : "";
  559. if (!name) { return; }
  560. list.push({
  561. name: name,
  562. kind: c.kind != null ? String(c.kind) : "",
  563. note: c.note != null ? String(c.note) : ""
  564. });
  565. });
  566. return list.length ? list : null;
  567. }
  568. // Optional detailed_analysis: a map of lens name to one-line verdict.
  569. // Returns null when empty or absent.
  570. function normalizeDetailed(raw) {
  571. if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return null; }
  572. var entries = [];
  573. Object.keys(raw).forEach(function (key) {
  574. var value = raw[key];
  575. if (value == null || typeof value === "object") { return; }
  576. entries.push({ lens: key, verdict: String(value) });
  577. });
  578. return entries.length ? entries : null;
  579. }
  580. // Optional sanctum block, shown only for memory/autonomous agents.
  581. // Returns null when absent or explicitly marked not present.
  582. function normalizeSanctum(raw) {
  583. if (!raw || typeof raw !== "object") { return null; }
  584. if (raw.present === false) { return null; }
  585. var files = [];
  586. if (Array.isArray(raw.files)) {
  587. raw.files.forEach(function (f) { if (f != null) { files.push(String(f)); } });
  588. }
  589. var s = {
  590. location: raw.location != null ? String(raw.location) : "",
  591. files: files,
  592. note: raw.note != null ? String(raw.note) : ""
  593. };
  594. if (!s.location && !s.files.length && !s.note) { return null; }
  595. return s;
  596. }
  597. // Optional experience block: journeys plus a headless note. Returns null when
  598. // neither is usable.
  599. function normalizeExperience(raw) {
  600. if (!raw || typeof raw !== "object") { return null; }
  601. var journeys = [];
  602. if (Array.isArray(raw.journeys)) {
  603. raw.journeys.forEach(function (j) {
  604. if (!j || typeof j !== "object") { return; }
  605. var name = j.name != null ? String(j.name) : "";
  606. var steps = j.steps != null ? String(j.steps) : "";
  607. if (!name && !steps) { return; }
  608. journeys.push({ name: name, steps: steps });
  609. });
  610. }
  611. var headless = raw.headless != null ? String(raw.headless) : "";
  612. if (!journeys.length && !headless) { return null; }
  613. return { journeys: journeys, headless: headless };
  614. }
  615. function renderOverview(data) {
  616. els.subject.textContent = data.subject;
  617. els.generated.textContent = data.generated;
  618. els.schema.textContent = String(data.schema_version);
  619. els.verdict.textContent = data.verdict;
  620. if (data.grade) {
  621. els.grade.textContent = data.grade;
  622. els.grade.className = "grade g-" + data.grade;
  623. els.grade.hidden = false;
  624. }
  625. if (data.summary) {
  626. els.summaryText.textContent = data.summary;
  627. els.summaryText.hidden = false;
  628. }
  629. els.counts.innerHTML = "";
  630. SEVERITIES.forEach(function (s) {
  631. var pill = document.createElement("span");
  632. pill.className = "pill";
  633. pill.innerHTML =
  634. '<span class="dot ' + s + '"></span>' +
  635. '<span class="lbl">' + SEV_LABEL[s] + '</span>' +
  636. '<span class="n">' + data.counts[s] + "</span>";
  637. els.counts.appendChild(pill);
  638. });
  639. els.overview.hidden = false;
  640. }
  641. // Each agent-block renderer leaves its section hidden when its data is null,
  642. // so a stateless agent (no sanctum) or a minimal island never shows a blank
  643. // panel and never throws.
  644. function renderProfile(profile) {
  645. if (!profile) { els.portrait.hidden = true; return; }
  646. var typeTag = profile.agent_type
  647. ? '<span class="type">' + esc(profile.agent_type) + "</span>"
  648. : "";
  649. var mission = profile.mission
  650. ? '<div class="mission">' + esc(profile.mission) + "</div>"
  651. : "";
  652. els.portrait.innerHTML =
  653. '<div class="icon">' + (profile.icon ? esc(profile.icon) : "🤖") + "</div>" +
  654. '<div class="who">' +
  655. '<div class="name">' + esc(profile.name || "(unnamed agent)") + "</div>" +
  656. (profile.title ? '<div class="title">' + esc(profile.title) + "</div>" : "") +
  657. mission +
  658. typeTag +
  659. "</div>";
  660. els.portrait.hidden = false;
  661. }
  662. function renderCapabilities(list) {
  663. if (!list) { els.capabilities.hidden = true; return; }
  664. var items = list.map(function (c) {
  665. return "<li>" +
  666. '<span class="cap-name">' + esc(c.name) + "</span>" +
  667. (c.kind ? '<span class="cap-kind">' + esc(c.kind) + "</span>" : "") +
  668. (c.note ? '<span class="cap-note">' + esc(c.note) + "</span>" : "") +
  669. "</li>";
  670. }).join("");
  671. els.capabilities.innerHTML =
  672. "<h2>Capabilities</h2><ul class=\"cap-list\">" + items + "</ul>";
  673. els.capabilities.hidden = false;
  674. }
  675. function renderLensVerdicts(entries) {
  676. if (!entries) { els.lensVerdicts.hidden = true; return; }
  677. var rows = entries.map(function (e) {
  678. return "<dt>" + esc(e.lens) + "</dt><dd>" + esc(e.verdict) + "</dd>";
  679. }).join("");
  680. els.lensVerdicts.innerHTML =
  681. "<h2>Per-lens verdicts</h2><dl class=\"kv\">" + rows + "</dl>";
  682. els.lensVerdicts.hidden = false;
  683. }
  684. function renderSanctum(s) {
  685. if (!s) { els.sanctum.hidden = true; return; }
  686. var rows = "";
  687. if (s.location) {
  688. rows += "<dt>Location</dt><dd><code>" + esc(s.location) + "</code></dd>";
  689. }
  690. if (s.files.length) {
  691. rows += "<dt>Sanctum files</dt><dd>" +
  692. s.files.map(function (f) { return '<span class="mono">' + esc(f) + "</span>"; }).join(" ") +
  693. "</dd>";
  694. }
  695. if (s.note) {
  696. rows += "<dt>Note</dt><dd>" + esc(s.note) + "</dd>";
  697. }
  698. els.sanctum.innerHTML = "<h2>Sanctum (runtime memory)</h2><dl class=\"kv\">" + rows + "</dl>";
  699. els.sanctum.hidden = false;
  700. }
  701. function renderExperience(exp) {
  702. if (!exp) { els.experience.hidden = true; return; }
  703. var html = "<h2>Experience</h2>";
  704. if (exp.journeys.length) {
  705. html += exp.journeys.map(function (j) {
  706. return '<div class="journey">' +
  707. '<div class="j-name">' + esc(j.name || "(unnamed journey)") + "</div>" +
  708. (j.steps ? '<div class="j-steps">' + esc(j.steps) + "</div>" : "") +
  709. "</div>";
  710. }).join("");
  711. }
  712. if (exp.headless) {
  713. html += '<dl class="kv" style="margin-top:12px"><dt>Headless</dt><dd>' +
  714. esc(exp.headless) + "</dd></dl>";
  715. }
  716. els.experience.innerHTML = html;
  717. els.experience.hidden = false;
  718. }
  719. // Every copied fix prompt opens by anchoring the fixing session to the same
  720. // standards that produced the findings, so the fix is held to the bar too.
  721. function standardsPreamble() {
  722. if (!standards || !standards.canon) { return []; }
  723. var bar = standards.canon + (standards.principles ? " and " + standards.principles : "");
  724. var lines = [
  725. "Hold " + bar + " as the bar for every line you change — a fix that adds ceremony is a new finding, not a fix."
  726. ];
  727. if (standards.scripts) {
  728. lines.push("If the fix adds or changes scripts, follow " + standards.scripts + ".");
  729. }
  730. lines.push("");
  731. return lines;
  732. }
  733. function composeThemePrompt(theme, resolved) {
  734. var lines = standardsPreamble();
  735. lines.push("Fix the following theme in " + subjectPath + ": " + theme.title);
  736. lines.push("");
  737. if (theme.root_cause) { lines.push("Root cause: " + theme.root_cause); }
  738. if (theme.action) { lines.push("Fix: " + theme.action); }
  739. if (resolved.length) {
  740. lines.push("");
  741. lines.push("Findings to address:");
  742. resolved.forEach(function (f, i) {
  743. lines.push((i + 1) + ". " + f.title);
  744. if (f.location) { lines.push(" Location: " + f.location); }
  745. if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
  746. if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
  747. });
  748. }
  749. return lines.join("\n") + "\n";
  750. }
  751. function renderThemes(themes) {
  752. if (!themes.length) { els.themes.hidden = true; return; }
  753. els.themes.innerHTML = "<h2>Themes</h2>";
  754. themes.forEach(function (t) {
  755. var resolved = t.finding_ids
  756. .map(function (id) { return findingsById[id]; })
  757. .filter(function (f) { return !!f; });
  758. var items = resolved.map(function (f) {
  759. return '<div class="t-finding"><span class="mono">' + esc(f.id) + "</span> " +
  760. esc(f.title) +
  761. (f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "") +
  762. "</div>";
  763. }).join("");
  764. var node = document.createElement("div");
  765. node.className = "theme";
  766. node.innerHTML =
  767. '<div class="t-head"><span class="t-title">' + esc(t.title) + "</span>" +
  768. '<button class="small t-fix">Fix This Theme</button></div>' +
  769. (t.root_cause ? '<div class="t-cause">Root cause: ' + esc(t.root_cause) + "</div>" : "") +
  770. (t.action ? '<div class="t-action"><b>Fix:</b> ' + esc(t.action) + "</div>" : "") +
  771. (items ? '<div class="t-findings">' + items + "</div>" : "");
  772. node.querySelector(".t-fix").addEventListener("click", function () {
  773. copyText(composeThemePrompt(t, resolved));
  774. });
  775. els.themes.appendChild(node);
  776. });
  777. els.themes.hidden = false;
  778. }
  779. function renderStrengths(list) {
  780. if (!list.length) { els.strengths.hidden = true; return; }
  781. els.strengths.innerHTML =
  782. "<h2>Strengths</h2><ul class=\"strength-list\">" +
  783. list.map(function (s) { return "<li>" + esc(s) + "</li>"; }).join("") +
  784. "</ul>";
  785. els.strengths.hidden = false;
  786. }
  787. function renderRecommendations(recs) {
  788. if (!recs.length) { els.recommendations.hidden = true; return; }
  789. var html = "<h2>Recommendations</h2>";
  790. recs.forEach(function (r) {
  791. html += '<div class="rec"><span class="rank">#' + esc(String(r.rank)) + "</span>" +
  792. esc(r.action) +
  793. (r.resolves ? '<span class="resolves">resolves: ' + esc(r.resolves) + "</span>" : "") +
  794. "</div>";
  795. });
  796. els.recommendations.innerHTML = html;
  797. els.recommendations.hidden = false;
  798. }
  799. function renderNoFindings() {
  800. els.root.innerHTML =
  801. '<div class="no-findings">' +
  802. '<div class="big">No findings</div>' +
  803. "<div>The scanners returned a clean pass for this subject.</div>" +
  804. "</div>";
  805. }
  806. function findingNode(f) {
  807. var node = document.createElement("div");
  808. node.className = "finding sev-" + f.severity;
  809. node.setAttribute("data-id", f.id);
  810. var sub =
  811. esc(f.lens) +
  812. (f.location ? ' · <span class="mono">' + esc(f.location) + "</span>" : "");
  813. var rows =
  814. "<dt>Lens</dt><dd>" + esc(f.lens) + "</dd>" +
  815. (f.location ? "<dt>Location</dt><dd><code>" + esc(f.location) + "</code></dd>" : "") +
  816. (f.evidence ? "<dt>Evidence</dt><dd>" + esc(f.evidence) + "</dd>" : "") +
  817. (f.recommendation ? "<dt>Recommendation</dt><dd>" + esc(f.recommendation) + "</dd>" : "") +
  818. (f.proposed_smallest ? "<dt>Proposed smallest</dt><dd>" + esc(f.proposed_smallest) + "</dd>" : "") +
  819. (f.predicted_delta ? "<dt>Predicted delta</dt><dd>" + esc(f.predicted_delta) + "</dd>" : "");
  820. node.innerHTML =
  821. '<div class="row">' +
  822. '<input type="checkbox" class="chk" aria-label="Select finding">' +
  823. '<div class="head">' +
  824. '<div class="title">' + esc(f.title) + "</div>" +
  825. '<div class="sub">' + sub + "</div>" +
  826. "</div>" +
  827. '<span class="tag">' + SEV_LABEL[f.severity] + "</span>" +
  828. '<span class="caret">▸</span>' +
  829. "</div>" +
  830. '<div class="body"><dl>' + rows + "</dl></div>";
  831. var chk = node.querySelector(".chk");
  832. chk.checked = !!selected[f.id];
  833. chk.addEventListener("change", function () {
  834. if (chk.checked) { selected[f.id] = true; } else { delete selected[f.id]; }
  835. updateSelection();
  836. });
  837. var head = node.querySelector(".head");
  838. var caret = node.querySelector(".caret");
  839. function toggle() { node.classList.toggle("open"); }
  840. head.addEventListener("click", toggle);
  841. caret.addEventListener("click", toggle);
  842. return node;
  843. }
  844. function renderFindings(list) {
  845. els.root.innerHTML = "";
  846. if (list.length === 0) {
  847. renderNoFindings();
  848. els.toolbar.hidden = true;
  849. return;
  850. }
  851. els.toolbar.hidden = false;
  852. SEVERITIES.forEach(function (sev) {
  853. var group = list.filter(function (f) { return f.severity === sev; });
  854. if (group.length === 0) { return; }
  855. var wrap = document.createElement("div");
  856. wrap.className = "group";
  857. var h = document.createElement("h2");
  858. h.innerHTML = '<span class="dot ' + sev + '"></span>' + SEV_LABEL[sev] + " (" + group.length + ")";
  859. wrap.appendChild(h);
  860. group.forEach(function (f) { wrap.appendChild(findingNode(f)); });
  861. els.root.appendChild(wrap);
  862. });
  863. }
  864. function updateSelection() {
  865. var n = Object.keys(selected).length;
  866. els.selCount.textContent = n + " selected";
  867. els.btnCopy.disabled = n === 0;
  868. }
  869. function composePrompt() {
  870. var picked = findings.filter(function (f) { return selected[f.id]; });
  871. if (picked.length === 0) { return ""; }
  872. var lines = standardsPreamble();
  873. lines.push("Fix the following issues in " + subjectPath + ":");
  874. lines.push("");
  875. picked.forEach(function (f, i) {
  876. lines.push((i + 1) + ". " + f.title);
  877. if (f.location) { lines.push(" Location: " + f.location); }
  878. if (f.evidence) { lines.push(" Evidence: " + f.evidence); }
  879. if (f.recommendation) { lines.push(" Recommendation: " + f.recommendation); }
  880. if (f.proposed_smallest) { lines.push(" Proposed smallest: " + f.proposed_smallest); }
  881. lines.push("");
  882. });
  883. return lines.join("\n").replace(/\n+$/, "\n");
  884. }
  885. function showToast(text) {
  886. els.toast.textContent = text;
  887. els.toast.classList.add("show");
  888. setTimeout(function () { els.toast.classList.remove("show"); }, 1600);
  889. }
  890. function fallbackCopy(text) {
  891. els.fallbackText.value = text;
  892. els.fallback.classList.add("show");
  893. els.fallbackText.focus();
  894. els.fallbackText.select();
  895. try {
  896. var ok = document.execCommand && document.execCommand("copy");
  897. if (ok) {
  898. showToast("Copied");
  899. return;
  900. }
  901. } catch (e) { /* fall through to manual */ }
  902. showToast("Copy the text shown below");
  903. }
  904. function copyText(text) {
  905. if (!text) { return; }
  906. if (navigator.clipboard && navigator.clipboard.writeText) {
  907. navigator.clipboard.writeText(text).then(
  908. function () { showToast("Copied"); },
  909. function () { fallbackCopy(text); }
  910. );
  911. } else {
  912. fallbackCopy(text);
  913. }
  914. }
  915. function doCopy() {
  916. copyText(composePrompt());
  917. }
  918. function wireToolbar() {
  919. els.btnCopy.addEventListener("click", doCopy);
  920. els.btnSelectAll.addEventListener("click", function () {
  921. findings.forEach(function (f) { selected[f.id] = true; });
  922. document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = true; });
  923. updateSelection();
  924. });
  925. els.btnClear.addEventListener("click", function () {
  926. selected = Object.create(null);
  927. document.querySelectorAll(".finding .chk").forEach(function (c) { c.checked = false; });
  928. els.fallback.classList.remove("show");
  929. updateSelection();
  930. });
  931. els.btnExpandAll.addEventListener("click", function () {
  932. document.querySelectorAll(".finding").forEach(function (n) { n.classList.add("open"); });
  933. });
  934. els.btnCollapseAll.addEventListener("click", function () {
  935. document.querySelectorAll(".finding").forEach(function (n) { n.classList.remove("open"); });
  936. });
  937. }
  938. function init() {
  939. var island = document.getElementById("report-data");
  940. var parsed;
  941. try {
  942. if (!island) { throw new Error("report-data island element not found"); }
  943. parsed = JSON.parse(island.textContent);
  944. } catch (err) {
  945. showBanner(
  946. "Could not parse the report data island.\n\n" +
  947. "Error: " + (err && err.message ? err.message : String(err)) + "\n\n" +
  948. "The findings could not be rendered. The JSON inside the " +
  949. 'report-data island (the application/json script tag) is malformed.'
  950. );
  951. return;
  952. }
  953. var data = normalize(parsed);
  954. if (data.subject === PLACEHOLDER_SUBJECT) {
  955. els.subject.textContent = data.subject;
  956. showBanner(
  957. "This is the unfilled report shell.\n\n" +
  958. "The report-data island still carries the placeholder subject, so " +
  959. "there are no findings here. Generate a real report with " +
  960. "scripts/render_report.py."
  961. );
  962. return;
  963. }
  964. findings = data.findings;
  965. subjectPath = data.subject;
  966. standards = data.standards;
  967. findingsById = Object.create(null);
  968. findings.forEach(function (f) { findingsById[f.id] = f; });
  969. renderProfile(data.agent_profile);
  970. renderOverview(data);
  971. renderCapabilities(data.capabilities);
  972. renderLensVerdicts(data.detailed_analysis);
  973. renderSanctum(data.sanctum);
  974. renderExperience(data.experience);
  975. renderThemes(data.themes);
  976. renderStrengths(data.strengths);
  977. renderRecommendations(data.recommendations);
  978. renderFindings(findings);
  979. wireToolbar();
  980. updateSelection();
  981. }
  982. if (document.readyState === "loading") {
  983. document.addEventListener("DOMContentLoaded", init);
  984. } else {
  985. init();
  986. }
  987. })();
  988. </script>
  989. </body>
  990. </html>