Three sections slot in after the bump detail, before telemetry: the review-loop heartbeat (is it alive?), the headline stats (the transitive ring), and the per-review table. A review's findings cell reads clean when none, else the hazard count, the flagged rules, and a one-click link to the advisory comment.
_review_record — the headline + advisory note · 545 lines _review_record — the headline + advisory note 545 lines · Python expand all
⋯ 404 lines hidden (lines 1–404) 1 """Render the view model to one self-contained HTML page (pure). 3 All CSS is inline, there is no JavaScript, and the page makes no network 4 request of its own — it is a static projection of an already-computed 5 :class:`~froot.dashboard.model.DashboardModel`. Every dynamic value is 6 HTML-escaped at the boundary. The ordering is trust-first (is it alive → track 7 record → oracle → judgment → the human's queue), so it reads top to bottom. 10 from __future__ import annotations 12 from datetime import UTC , datetime , timedelta 13 from html import escape 14 from typing import TYPE_CHECKING 17 from froot . dashboard . model import ( 27 :root { --fg:#1a1a1a;--mut:#6b6b6b;--line:#e4e4e4;--bg:#fff;--ok:#1a7f37; 28 --warn:#9a6700;--bad:#cf222e;--accent:#0969da;--card:#fafafa} 29 @media(prefers-color-scheme:dark) { :root { --fg:#e6e6e6;--mut:#9a9a9a; 30 --line:#262626;--bg:#0d0d0d;--ok:#3fb950;--warn:#d29922;--bad:#f85149; 31 --accent:#58a6ff;--card:#141414}} 32 * { box-sizing:border-box} 33 body { margin:0;background:var(--bg);color:var(--fg); 34 font:15px/1.55 system-ui,-apple-system, " Segoe UI " ,Roboto,sans-serif} 35 main { max-width:820px;margin:0 auto;padding:34px 20px 72px} 36 h1 { font-size:22px;margin:0;letter-spacing:-.01em} 37 .tag { color:var(--mut);margin:3px 0 0;font-size:13px} 38 .meta { color:var(--mut);font-size:12px;margin:12px 0 0} 39 .sources { display:flex;flex-wrap:wrap;gap:16px;margin:10px 0 0;font-size:12px} 40 section { margin:30px 0 0} 41 h2 { font-size:12px;text-transform:uppercase;letter-spacing:.08em; 42 color:var(--mut);margin:0 0 12px;font-weight:600; 43 border-bottom:1px solid var(--line);padding-bottom:6px} 44 .stats { display:flex;flex-wrap:wrap;gap:26px} 45 .stat .n { font-size:26px;font-weight:600;line-height:1.1} 46 .stat .l { color:var(--mut);font-size:12px;margin-top:2px} 47 .dot { display:inline-block;width:9px;height:9px;border-radius:50 % ; 49 .dot.ok { background:var(--ok)}.dot.warn { background:var(--warn)} 50 .dot.bad { background:var(--bad)}.dot.mute { background:var(--mut)} 51 table { width:100 % ;border-collapse:collapse;font-size:13px} 52 th,td { text-align:left;padding:6px 12px 6px 0; 53 border-bottom:1px solid var(--line);vertical-align:top} 54 th { color:var(--mut);font-weight:600;font-size:11px;text-transform:uppercase; 56 .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px} 57 a { color:var(--accent);text-decoration:none}a:hover { text-decoration:underline} 58 .row { display:flex;align-items:baseline;gap:8px;padding:4px 0} 59 .mut { color:var(--mut)}.ok { color:var(--ok)}.warn { color:var(--warn)} 61 .note { color:var(--mut);font-size:12px;margin:10px 0 0} 62 footer { margin:44px 0 0;padding-top:16px;border-top:1px solid var(--line); 63 color:var(--mut);font-size:12px;line-height:1.7} 64 footer b { color:var(--fg);font-weight:600} 73 _VERDICT_CLASS = { " clean " : " ok " , " risky " : " warn " , " unknown " : " mut " } 76 def _aware ( when : datetime ) - > datetime : 77 """Treat a stray naive timestamp as UTC so arithmetic never raises.""" 78 return when if when . tzinfo is not None else when . replace ( tzinfo = UTC ) 81 def _ago ( when : datetime | None , now : datetime ) - > str : 82 """A compact 'time since' label (``6h ago``).""" 85 secs = ( now - _aware ( when ) ) . total_seconds ( ) 89 return f " { int ( secs / / 60 ) } m ago " 91 return f " { int ( secs / / 3600 ) } h ago " 92 return f " { int ( secs / / 86400 ) } d ago " 95 def _until ( when : datetime | None , now : datetime ) - > str : 96 """A compact 'time until' label (``in 18h`` / ``due now``).""" 99 secs = ( _aware ( when ) - now ) . total_seconds ( ) 103 return f " in { int ( secs / / 60 ) } m " 105 return f " in { int ( secs / / 3600 ) } h " 106 return f " in { int ( secs / / 86400 ) } d " 109 def _dot ( kind : str ) - > str : 110 """A status dot span (``ok`` / ``warn`` / ``bad`` / ``mute``).""" 111 return f ' <span class= " dot { kind } " ></span> ' 114 def _tag ( value : str | None , classes : dict [ str , str ] ) - > str : 115 """A small coloured label for a verdict/CI value, or an em-dash.""" 117 return ' <span class= " mut " >—</span> ' 118 cls = classes . get ( value , " mut " ) 119 return f ' <span class= " { cls } " > { escape ( value ) } </span> ' 122 def _stat ( n : object , label : str ) - > str : 124 f ' <div class= " stat " ><div class= " n " > { escape ( str ( n ) ) } </div> ' 125 f ' <div class= " l " > { escape ( label ) } </div></div> ' 129 def _header ( model : DashboardModel ) - > str : 130 now = model . generated_at 131 repos = " , " . join ( model . repos_configured ) or " none configured " 133 f " <span> { _dot ( ' ok ' if s . ok else ' bad ' ) } " 134 f ' { escape ( s . name ) } <span class= " mut " > { escape ( s . detail ) } </span></span> ' 135 for s in model . sources 137 stamp = now . strftime ( " % Y- % m- %d % H: % M UTC " ) 141 ' <p class= " tag " >durable maintenance loops · ' 142 " reputation read-model</p> " 143 f ' <p class= " meta " >watching <span class= " mono " > { escape ( repos ) } </span> ' 144 f " · generated { escape ( stamp ) } · " 145 " derived live, stored nowhere</p> " 146 f ' <div class= " sources " > { dots } </div> ' 151 def _heartbeat ( model : DashboardModel ) - > str : 152 now = model . generated_at 153 interval = model . scan_interval_seconds 155 def line ( loop : ScanLoop ) - > str : 158 if loop . last_tick is not None : 159 nxt = _aware ( loop . last_tick ) + timedelta ( seconds = interval ) 160 last = _ago ( loop . last_tick , now ) 162 f ' <span class= " mut " >· last { last } ' 163 f " · next { _until ( nxt , now ) } </span> " 166 dot = " bad " if loop . status in ( " terminated " , " none " ) else " warn " 167 tail = f ' <span class= " mut " >· { escape ( loop . status ) } </span> ' 169 f ' <div class= " row " > { _dot ( dot ) } ' 170 f ' <span class= " mono " > { escape ( loop . repo ) } </span> { tail } </div> ' 173 if not model . scan_loops : 174 body = ' <p class= " note " >No repos configured (FROOT_REPOS unset).</p> ' 176 body = " " . join ( line ( loop ) for loop in model . scan_loops ) 177 return f " <section><h2>Is the loop alive?</h2> { body } </section> " 180 def _track_record ( model : DashboardModel ) - > str : 181 t = model . track_record 182 rate = " — " if t . merge_rate is None else f " { t . merge_rate * 100 : .0f } % " 185 if t . median_ttm_minutes is None 186 else f " { t . median_ttm_minutes : .0f } min " 190 _stat ( t . opened , " proposed " ) , 191 _stat ( t . merged , " merged " ) , 192 _stat ( t . open_now , " awaiting " ) , 193 _stat ( t . closed_unmerged , " closed " ) , 194 _stat ( rate , " merge rate " ) , 195 _stat ( ttm , " median time-to-merge " ) , 199 ' <p class= " note " >Merge rate is the Stage-1 signal — narrow to ' 200 " npm patch bumps by construction. It counts a human merge, not a " 201 " confirmed good outcome: revert tracking is a later loop, so merge is " 202 " not yet proof of success.</p> " 205 " <section><h2>Track record · the reputation</h2> " 206 f ' <div class= " stats " > { stats } </div> { note } </section> ' 210 def _verification ( model : DashboardModel ) - > str : 211 v = model . verification 214 _stat ( v . passed , " CI passed " ) , 215 _stat ( v . failed , " CI failed " ) , 216 _stat ( v . absent , " no checks " ) , 217 _stat ( v . timed_out , " timed out " ) , 218 _stat ( v . unknown , " unknown " ) , 221 if v . with_reading == 0 : 222 note = ' <p class= " note " >No CI readings yet.</p> ' 225 f ' <p class= " note " >A real oracle reported on ' 226 f " <b> { v . oracle_existed } </b> of { v . with_reading } bumps with a " 227 ' reading. <span class= " mut " >‘no checks’ means CI was ' 228 " absent — not a pass; never conflated.</span> " 232 " <section><h2>Verification · CI is the oracle</h2> " 233 f ' <div class= " stats " > { stats } </div> { note } </section> ' 237 def _judgment ( model : DashboardModel ) - > str : 241 _stat ( j . clean , " clean " ) , 242 _stat ( j . risky , " risky " ) , 243 _stat ( j . unknown , " unknown " ) , 244 _stat ( j . none , " no verdict " ) , 247 if j . clean_but_failed or j . flagged_but_passed : 249 f ' <p class= " note " >Calibration: <b> { j . clean_but_failed } </b> ' 250 " ‘clean’ bumps whose CI failed, " 251 f " <b> { j . flagged_but_passed } </b> flagged bumps whose CI passed.</p> " 255 ' <p class= " note " >The model’s only job is the changelog ' 256 " verdict; the spine proposes the bump either way.</p> " 259 " <section><h2>Model judgment · the one model call</h2> " 260 f ' <div class= " stats " > { stats } </div> { note } </section> ' 264 def _gate ( model : DashboardModel ) - > str : 265 now = model . generated_at 267 body = ' <p class= " note " >Queue empty — nothing awaiting you.</p> ' 271 f ' <td class= " mono " > { escape ( row . package ) } </td> ' 272 f " <td> { escape ( _ago ( row . opened_at , now ) ) } </td> " 273 f " <td> { _pr_link ( row ) } </td> " 275 for row in model . gate 278 " <table><thead><tr><th>package</th><th>waiting</th><th>pr</th> " 279 f " </tr></thead><tbody> { rows } </tbody></table> " 282 " <section><h2>Approval gate · what a human owns</h2> " 287 def _bumps ( model : DashboardModel ) - > str : 288 now = model . generated_at 290 body = ' <p class= " note " >No bumps proposed yet.</p> ' 294 f ' <td class= " mono " > { escape ( row . package ) } </td> ' 295 f ' <td class= " mono mut " > { escape ( row . from_version or " ? " ) } → ' 296 f " { escape ( row . to_version ) } </td> " 297 f " <td> { _tag ( row . verdict , _VERDICT_CLASS ) } </td> " 298 f " <td> { _tag ( row . ci , _CI_CLASS ) } </td> " 299 f " <td> { _state_tag ( row . state ) } </td> " 300 f ' <td class= " mut " > { escape ( _ago ( row . opened_at , now ) ) } </td> ' 301 f " <td> { _pr_link ( row ) } </td> " 303 for row in model . bumps 306 " <table><thead><tr><th>package</th><th>bump</th><th>verdict</th> " 307 " <th>ci</th><th>state</th><th>opened</th><th>pr</th></tr></thead> " 308 f " <tbody> { rows } </tbody></table> " 310 return f " <section><h2>Bumps · the detail</h2> { body } </section> " 313 def _failures ( model : DashboardModel ) - > str : 314 if not model . failures : 316 now = model . generated_at 319 f ' <td class= " mono " > { escape ( _short_id ( f . workflow_id ) ) } </td> ' 320 f " <td> { _state_tag ( f . kind ) } </td> " 321 f ' <td class= " mut " > { escape ( f . reason or " — " ) } </td> ' 322 f ' <td class= " mut " > { escape ( _ago ( f . when , now ) ) } </td> ' 324 for f in model . failures 327 " <section><h2>Failures · where the loop did not close</h2> " 328 " <table><thead><tr><th>bump</th><th>kind</th><th>reason</th> " 329 f " <th>when</th></tr></thead><tbody> { rows } </tbody></table></section> " 333 def _telemetry ( model : DashboardModel ) - > str : 334 t : RunTelemetry = model . telemetry 337 " <section><h2>Run telemetry · ClickHouse</h2> " 338 ' <p class= " note " >Unavailable (not configured or unreachable). ' 339 " GitHub + Temporal carry the dashboard regardless.</p></section> " 341 now = model . generated_at 345 f ' <td class= " mono " > { escape ( a . name ) } </td> ' 346 f " <td> { a . count } </td> " 347 f ' <td class= " mut " > { a . avg_ms : .0f } ms</td> ' 348 f ' <td class= " mut " > { a . max_ms : .0f } ms</td> ' 350 for a in t . activities 353 " <table><thead><tr><th>activity</th><th>runs</th><th>avg</th> " 354 f " <th>max</th></tr></thead><tbody> { rows } </tbody></table> " 357 table = ' <p class= " note " >No froot spans in the window.</p> ' 359 f ' <p class= " note " > { t . total_spans } spans · ' 360 f " { t . error_spans } errored · last activity " 361 f " { escape ( _ago ( t . last_activity , now ) ) } · " 362 f " { t . window_days } -day window.</p> " 365 " <section><h2>Run telemetry · ClickHouse</h2> " 366 f " { summary } { table } </section> " 370 def _review_heartbeat ( model : DashboardModel ) - > str : 371 now = model . generated_at 372 interval = model . review_interval_seconds 374 def line ( loop : ReviewLoop ) - > str : 377 if loop . last_tick is not None : 378 nxt = _aware ( loop . last_tick ) + timedelta ( seconds = interval ) 379 last = _ago ( loop . last_tick , now ) 381 f ' <span class= " mut " >· last { last } ' 382 f " · next { _until ( nxt , now ) } </span> " 385 dot = " bad " if loop . status in ( " terminated " , " none " ) else " warn " 386 tail = f ' <span class= " mut " >· { escape ( loop . status ) } </span> ' 388 f ' <div class= " row " > { _dot ( dot ) } ' 389 f ' <span class= " mono " > { escape ( loop . repo ) } </span> { tail } </div> ' 392 if not model . review_loops : 394 ' <p class= " note " >No determinism-review loops running ' 395 " (the transitive ring watches the @workflow.defn repos).</p> " 398 body = " " . join ( line ( loop ) for loop in model . review_loops ) 400 " <section><h2>Determinism review · is it alive?</h2> " 405 def _review_record ( model : DashboardModel ) - > str : 406 r = model . review_record 409 _stat ( r . reviewed , " reviewed " ) , 410 _stat ( r . flagged , " flagged " ) , 411 _stat ( r . clean , " clean " ) , 412 _stat ( r . hazards , " hazards " ) , 413 _stat ( r . repos_covered , " repos covered " ) , 417 ' <p class= " note " >The transitive ring: it chases first-party helper ' 418 " calls out of each workflow to catch a hazard the lexical CI kernel " 419 " can’t see. <b>Advisory</b> — the blocking gate stays the " 420 " kernel’s CI check. The hazard-resolved rate (was a flag gone on " 421 " a later commit?) is a later loop; it needs accumulated history.</p> " 424 " <section><h2>Determinism review · the transitive ring</h2> " 425 f ' <div class= " stats " > { stats } </div> { note } </section> ' ⋯ 117 lines hidden (lines 429–545) 429 def _reviews ( model : DashboardModel ) - > str : 430 now = model . generated_at 431 if not model . reviews : 432 body = ' <p class= " note " >No PRs reviewed yet.</p> ' 436 f ' <td class= " mono " > { escape ( row . repo ) } </td> ' 437 f " <td> { _review_pr_link ( row ) } </td> " 438 f ' <td class= " mono mut " > { escape ( ( row . head_sha or " " ) [ : 7 ] ) or " — " } ' 440 f " <td> { _findings_cell ( row ) } </td> " 441 f ' <td class= " mut " > { escape ( _ago ( row . reviewed_at , now ) ) } </td> ' 443 for row in model . reviews 446 " <table><thead><tr><th>repo</th><th>pr</th><th>head</th> " 447 " <th>findings</th><th>reviewed</th></tr></thead> " 448 f " <tbody> { rows } </tbody></table> " 451 " <section><h2>Determinism reviews · the detail</h2> " 459 " <b>Authority envelope.</b> Stage 1 — froot holds " 460 " <b>write authority</b> only: it opens PRs, a human approves every " 461 " merge (commit authority = none). Trust, when any is granted, is " 462 " earned, narrow to npm patch bumps, conditional on its environment " 463 ' (judge <span class= " mono " >gemma4:e4b</span>, lockfile-only regen), ' 464 " revocable, and time-expiring. Today it records the track record; it " 465 " does not yet act on it.<br> " 466 " Everything above is derived on this request from GitHub (outcomes) + " 467 " Temporal (runs) + ClickHouse (telemetry). froot keeps no database; " 468 " reload to recompute. " 473 def _pr_link ( row : BumpRow ) - > str : 474 if row . pr_url is None or row . pr_number is None : 475 return ' <span class= " mut " >—</span> ' 476 return f ' <a href= " { escape ( row . pr_url , quote = True ) } " ># { row . pr_number } </a> ' 479 def _review_pr_link ( row : ReviewRow ) - > str : 480 if row . pr_url is None or row . pr_number is None : 481 return ' <span class= " mut " >—</span> ' 482 return f ' <a href= " { escape ( row . pr_url , quote = True ) } " ># { row . pr_number } </a> ' 485 def _findings_cell ( row : ReviewRow ) - > str : 486 """A review's findings: 'clean', or the hazard count + rules + comment.""" 487 if row . findings == 0 : 488 return ' <span class= " ok " >clean</span> ' 490 f ' <span class= " mono mut " > { escape ( " , " . join ( row . rules ) ) } </span> ' 495 f ' <a href= " { escape ( row . comment_url , quote = True ) } " >comment</a> ' 499 noun = " hazard " if row . findings == 1 else " hazards " 500 return f ' <span class= " bad " > { row . findings } { noun } </span> { rules } { comment } ' 503 def _state_tag ( state : str ) - > str : 513 return f ' <span class= " { cls } " > { escape ( state ) } </span> ' 516 def _short_id ( workflow_id : str ) - > str : 517 """Drop the ``froot-bump-`` prefix for a readable failures row.""" 518 return workflow_id . removeprefix ( " froot-bump- " ) 521 def page ( model : DashboardModel ) - > str : 522 """Render the whole dashboard as one self-contained HTML document.""" 526 _track_record ( model ) , 527 _verification ( model ) , 532 _review_heartbeat ( model ) , 533 _review_record ( model ) , 539 ' <!doctype html><html lang= " en " ><head><meta charset= " utf-8 " > ' 540 ' <meta name= " viewport " content= " width=device-width,initial-scale=1 " > ' 541 " <title>froot · read-model</title> " 542 f " <style> { _CSS } </style></head><body><main> " 544 + " </main></body></html> "
_findings_cell — clean, or count + rules + comment · 545 lines _findings_cell — clean, or count + rules + comment 545 lines · Python expand all
⋯ 484 lines hidden (lines 1–484) 1 """Render the view model to one self-contained HTML page (pure). 3 All CSS is inline, there is no JavaScript, and the page makes no network 4 request of its own — it is a static projection of an already-computed 5 :class:`~froot.dashboard.model.DashboardModel`. Every dynamic value is 6 HTML-escaped at the boundary. The ordering is trust-first (is it alive → track 7 record → oracle → judgment → the human's queue), so it reads top to bottom. 10 from __future__ import annotations 12 from datetime import UTC , datetime , timedelta 13 from html import escape 14 from typing import TYPE_CHECKING 17 from froot . dashboard . model import ( 27 :root { --fg:#1a1a1a;--mut:#6b6b6b;--line:#e4e4e4;--bg:#fff;--ok:#1a7f37; 28 --warn:#9a6700;--bad:#cf222e;--accent:#0969da;--card:#fafafa} 29 @media(prefers-color-scheme:dark) { :root { --fg:#e6e6e6;--mut:#9a9a9a; 30 --line:#262626;--bg:#0d0d0d;--ok:#3fb950;--warn:#d29922;--bad:#f85149; 31 --accent:#58a6ff;--card:#141414}} 32 * { box-sizing:border-box} 33 body { margin:0;background:var(--bg);color:var(--fg); 34 font:15px/1.55 system-ui,-apple-system, " Segoe UI " ,Roboto,sans-serif} 35 main { max-width:820px;margin:0 auto;padding:34px 20px 72px} 36 h1 { font-size:22px;margin:0;letter-spacing:-.01em} 37 .tag { color:var(--mut);margin:3px 0 0;font-size:13px} 38 .meta { color:var(--mut);font-size:12px;margin:12px 0 0} 39 .sources { display:flex;flex-wrap:wrap;gap:16px;margin:10px 0 0;font-size:12px} 40 section { margin:30px 0 0} 41 h2 { font-size:12px;text-transform:uppercase;letter-spacing:.08em; 42 color:var(--mut);margin:0 0 12px;font-weight:600; 43 border-bottom:1px solid var(--line);padding-bottom:6px} 44 .stats { display:flex;flex-wrap:wrap;gap:26px} 45 .stat .n { font-size:26px;font-weight:600;line-height:1.1} 46 .stat .l { color:var(--mut);font-size:12px;margin-top:2px} 47 .dot { display:inline-block;width:9px;height:9px;border-radius:50 % ; 49 .dot.ok { background:var(--ok)}.dot.warn { background:var(--warn)} 50 .dot.bad { background:var(--bad)}.dot.mute { background:var(--mut)} 51 table { width:100 % ;border-collapse:collapse;font-size:13px} 52 th,td { text-align:left;padding:6px 12px 6px 0; 53 border-bottom:1px solid var(--line);vertical-align:top} 54 th { color:var(--mut);font-weight:600;font-size:11px;text-transform:uppercase; 56 .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px} 57 a { color:var(--accent);text-decoration:none}a:hover { text-decoration:underline} 58 .row { display:flex;align-items:baseline;gap:8px;padding:4px 0} 59 .mut { color:var(--mut)}.ok { color:var(--ok)}.warn { color:var(--warn)} 61 .note { color:var(--mut);font-size:12px;margin:10px 0 0} 62 footer { margin:44px 0 0;padding-top:16px;border-top:1px solid var(--line); 63 color:var(--mut);font-size:12px;line-height:1.7} 64 footer b { color:var(--fg);font-weight:600} 73 _VERDICT_CLASS = { " clean " : " ok " , " risky " : " warn " , " unknown " : " mut " } 76 def _aware ( when : datetime ) - > datetime : 77 """Treat a stray naive timestamp as UTC so arithmetic never raises.""" 78 return when if when . tzinfo is not None else when . replace ( tzinfo = UTC ) 81 def _ago ( when : datetime | None , now : datetime ) - > str : 82 """A compact 'time since' label (``6h ago``).""" 85 secs = ( now - _aware ( when ) ) . total_seconds ( ) 89 return f " { int ( secs / / 60 ) } m ago " 91 return f " { int ( secs / / 3600 ) } h ago " 92 return f " { int ( secs / / 86400 ) } d ago " 95 def _until ( when : datetime | None , now : datetime ) - > str : 96 """A compact 'time until' label (``in 18h`` / ``due now``).""" 99 secs = ( _aware ( when ) - now ) . total_seconds ( ) 103 return f " in { int ( secs / / 60 ) } m " 105 return f " in { int ( secs / / 3600 ) } h " 106 return f " in { int ( secs / / 86400 ) } d " 109 def _dot ( kind : str ) - > str : 110 """A status dot span (``ok`` / ``warn`` / ``bad`` / ``mute``).""" 111 return f ' <span class= " dot { kind } " ></span> ' 114 def _tag ( value : str | None , classes : dict [ str , str ] ) - > str : 115 """A small coloured label for a verdict/CI value, or an em-dash.""" 117 return ' <span class= " mut " >—</span> ' 118 cls = classes . get ( value , " mut " ) 119 return f ' <span class= " { cls } " > { escape ( value ) } </span> ' 122 def _stat ( n : object , label : str ) - > str : 124 f ' <div class= " stat " ><div class= " n " > { escape ( str ( n ) ) } </div> ' 125 f ' <div class= " l " > { escape ( label ) } </div></div> ' 129 def _header ( model : DashboardModel ) - > str : 130 now = model . generated_at 131 repos = " , " . join ( model . repos_configured ) or " none configured " 133 f " <span> { _dot ( ' ok ' if s . ok else ' bad ' ) } " 134 f ' { escape ( s . name ) } <span class= " mut " > { escape ( s . detail ) } </span></span> ' 135 for s in model . sources 137 stamp = now . strftime ( " % Y- % m- %d % H: % M UTC " ) 141 ' <p class= " tag " >durable maintenance loops · ' 142 " reputation read-model</p> " 143 f ' <p class= " meta " >watching <span class= " mono " > { escape ( repos ) } </span> ' 144 f " · generated { escape ( stamp ) } · " 145 " derived live, stored nowhere</p> " 146 f ' <div class= " sources " > { dots } </div> ' 151 def _heartbeat ( model : DashboardModel ) - > str : 152 now = model . generated_at 153 interval = model . scan_interval_seconds 155 def line ( loop : ScanLoop ) - > str : 158 if loop . last_tick is not None : 159 nxt = _aware ( loop . last_tick ) + timedelta ( seconds = interval ) 160 last = _ago ( loop . last_tick , now ) 162 f ' <span class= " mut " >· last { last } ' 163 f " · next { _until ( nxt , now ) } </span> " 166 dot = " bad " if loop . status in ( " terminated " , " none " ) else " warn " 167 tail = f ' <span class= " mut " >· { escape ( loop . status ) } </span> ' 169 f ' <div class= " row " > { _dot ( dot ) } ' 170 f ' <span class= " mono " > { escape ( loop . repo ) } </span> { tail } </div> ' 173 if not model . scan_loops : 174 body = ' <p class= " note " >No repos configured (FROOT_REPOS unset).</p> ' 176 body = " " . join ( line ( loop ) for loop in model . scan_loops ) 177 return f " <section><h2>Is the loop alive?</h2> { body } </section> " 180 def _track_record ( model : DashboardModel ) - > str : 181 t = model . track_record 182 rate = " — " if t . merge_rate is None else f " { t . merge_rate * 100 : .0f } % " 185 if t . median_ttm_minutes is None 186 else f " { t . median_ttm_minutes : .0f } min " 190 _stat ( t . opened , " proposed " ) , 191 _stat ( t . merged , " merged " ) , 192 _stat ( t . open_now , " awaiting " ) , 193 _stat ( t . closed_unmerged , " closed " ) , 194 _stat ( rate , " merge rate " ) , 195 _stat ( ttm , " median time-to-merge " ) , 199 ' <p class= " note " >Merge rate is the Stage-1 signal — narrow to ' 200 " npm patch bumps by construction. It counts a human merge, not a " 201 " confirmed good outcome: revert tracking is a later loop, so merge is " 202 " not yet proof of success.</p> " 205 " <section><h2>Track record · the reputation</h2> " 206 f ' <div class= " stats " > { stats } </div> { note } </section> ' 210 def _verification ( model : DashboardModel ) - > str : 211 v = model . verification 214 _stat ( v . passed , " CI passed " ) , 215 _stat ( v . failed , " CI failed " ) , 216 _stat ( v . absent , " no checks " ) , 217 _stat ( v . timed_out , " timed out " ) , 218 _stat ( v . unknown , " unknown " ) , 221 if v . with_reading == 0 : 222 note = ' <p class= " note " >No CI readings yet.</p> ' 225 f ' <p class= " note " >A real oracle reported on ' 226 f " <b> { v . oracle_existed } </b> of { v . with_reading } bumps with a " 227 ' reading. <span class= " mut " >‘no checks’ means CI was ' 228 " absent — not a pass; never conflated.</span> " 232 " <section><h2>Verification · CI is the oracle</h2> " 233 f ' <div class= " stats " > { stats } </div> { note } </section> ' 237 def _judgment ( model : DashboardModel ) - > str : 241 _stat ( j . clean , " clean " ) , 242 _stat ( j . risky , " risky " ) , 243 _stat ( j . unknown , " unknown " ) , 244 _stat ( j . none , " no verdict " ) , 247 if j . clean_but_failed or j . flagged_but_passed : 249 f ' <p class= " note " >Calibration: <b> { j . clean_but_failed } </b> ' 250 " ‘clean’ bumps whose CI failed, " 251 f " <b> { j . flagged_but_passed } </b> flagged bumps whose CI passed.</p> " 255 ' <p class= " note " >The model’s only job is the changelog ' 256 " verdict; the spine proposes the bump either way.</p> " 259 " <section><h2>Model judgment · the one model call</h2> " 260 f ' <div class= " stats " > { stats } </div> { note } </section> ' 264 def _gate ( model : DashboardModel ) - > str : 265 now = model . generated_at 267 body = ' <p class= " note " >Queue empty — nothing awaiting you.</p> ' 271 f ' <td class= " mono " > { escape ( row . package ) } </td> ' 272 f " <td> { escape ( _ago ( row . opened_at , now ) ) } </td> " 273 f " <td> { _pr_link ( row ) } </td> " 275 for row in model . gate 278 " <table><thead><tr><th>package</th><th>waiting</th><th>pr</th> " 279 f " </tr></thead><tbody> { rows } </tbody></table> " 282 " <section><h2>Approval gate · what a human owns</h2> " 287 def _bumps ( model : DashboardModel ) - > str : 288 now = model . generated_at 290 body = ' <p class= " note " >No bumps proposed yet.</p> ' 294 f ' <td class= " mono " > { escape ( row . package ) } </td> ' 295 f ' <td class= " mono mut " > { escape ( row . from_version or " ? " ) } → ' 296 f " { escape ( row . to_version ) } </td> " 297 f " <td> { _tag ( row . verdict , _VERDICT_CLASS ) } </td> " 298 f " <td> { _tag ( row . ci , _CI_CLASS ) } </td> " 299 f " <td> { _state_tag ( row . state ) } </td> " 300 f ' <td class= " mut " > { escape ( _ago ( row . opened_at , now ) ) } </td> ' 301 f " <td> { _pr_link ( row ) } </td> " 303 for row in model . bumps 306 " <table><thead><tr><th>package</th><th>bump</th><th>verdict</th> " 307 " <th>ci</th><th>state</th><th>opened</th><th>pr</th></tr></thead> " 308 f " <tbody> { rows } </tbody></table> " 310 return f " <section><h2>Bumps · the detail</h2> { body } </section> " 313 def _failures ( model : DashboardModel ) - > str : 314 if not model . failures : 316 now = model . generated_at 319 f ' <td class= " mono " > { escape ( _short_id ( f . workflow_id ) ) } </td> ' 320 f " <td> { _state_tag ( f . kind ) } </td> " 321 f ' <td class= " mut " > { escape ( f . reason or " — " ) } </td> ' 322 f ' <td class= " mut " > { escape ( _ago ( f . when , now ) ) } </td> ' 324 for f in model . failures 327 " <section><h2>Failures · where the loop did not close</h2> " 328 " <table><thead><tr><th>bump</th><th>kind</th><th>reason</th> " 329 f " <th>when</th></tr></thead><tbody> { rows } </tbody></table></section> " 333 def _telemetry ( model : DashboardModel ) - > str : 334 t : RunTelemetry = model . telemetry 337 " <section><h2>Run telemetry · ClickHouse</h2> " 338 ' <p class= " note " >Unavailable (not configured or unreachable). ' 339 " GitHub + Temporal carry the dashboard regardless.</p></section> " 341 now = model . generated_at 345 f ' <td class= " mono " > { escape ( a . name ) } </td> ' 346 f " <td> { a . count } </td> " 347 f ' <td class= " mut " > { a . avg_ms : .0f } ms</td> ' 348 f ' <td class= " mut " > { a . max_ms : .0f } ms</td> ' 350 for a in t . activities 353 " <table><thead><tr><th>activity</th><th>runs</th><th>avg</th> " 354 f " <th>max</th></tr></thead><tbody> { rows } </tbody></table> " 357 table = ' <p class= " note " >No froot spans in the window.</p> ' 359 f ' <p class= " note " > { t . total_spans } spans · ' 360 f " { t . error_spans } errored · last activity " 361 f " { escape ( _ago ( t . last_activity , now ) ) } · " 362 f " { t . window_days } -day window.</p> " 365 " <section><h2>Run telemetry · ClickHouse</h2> " 366 f " { summary } { table } </section> " 370 def _review_heartbeat ( model : DashboardModel ) - > str : 371 now = model . generated_at 372 interval = model . review_interval_seconds 374 def line ( loop : ReviewLoop ) - > str : 377 if loop . last_tick is not None : 378 nxt = _aware ( loop . last_tick ) + timedelta ( seconds = interval ) 379 last = _ago ( loop . last_tick , now ) 381 f ' <span class= " mut " >· last { last } ' 382 f " · next { _until ( nxt , now ) } </span> " 385 dot = " bad " if loop . status in ( " terminated " , " none " ) else " warn " 386 tail = f ' <span class= " mut " >· { escape ( loop . status ) } </span> ' 388 f ' <div class= " row " > { _dot ( dot ) } ' 389 f ' <span class= " mono " > { escape ( loop . repo ) } </span> { tail } </div> ' 392 if not model . review_loops : 394 ' <p class= " note " >No determinism-review loops running ' 395 " (the transitive ring watches the @workflow.defn repos).</p> " 398 body = " " . join ( line ( loop ) for loop in model . review_loops ) 400 " <section><h2>Determinism review · is it alive?</h2> " 405 def _review_record ( model : DashboardModel ) - > str : 406 r = model . review_record 409 _stat ( r . reviewed , " reviewed " ) , 410 _stat ( r . flagged , " flagged " ) , 411 _stat ( r . clean , " clean " ) , 412 _stat ( r . hazards , " hazards " ) , 413 _stat ( r . repos_covered , " repos covered " ) , 417 ' <p class= " note " >The transitive ring: it chases first-party helper ' 418 " calls out of each workflow to catch a hazard the lexical CI kernel " 419 " can’t see. <b>Advisory</b> — the blocking gate stays the " 420 " kernel’s CI check. The hazard-resolved rate (was a flag gone on " 421 " a later commit?) is a later loop; it needs accumulated history.</p> " 424 " <section><h2>Determinism review · the transitive ring</h2> " 425 f ' <div class= " stats " > { stats } </div> { note } </section> ' 429 def _reviews ( model : DashboardModel ) - > str : 430 now = model . generated_at 431 if not model . reviews : 432 body = ' <p class= " note " >No PRs reviewed yet.</p> ' 436 f ' <td class= " mono " > { escape ( row . repo ) } </td> ' 437 f " <td> { _review_pr_link ( row ) } </td> " 438 f ' <td class= " mono mut " > { escape ( ( row . head_sha or " " ) [ : 7 ] ) or " — " } ' 440 f " <td> { _findings_cell ( row ) } </td> " 441 f ' <td class= " mut " > { escape ( _ago ( row . reviewed_at , now ) ) } </td> ' 443 for row in model . reviews 446 " <table><thead><tr><th>repo</th><th>pr</th><th>head</th> " 447 " <th>findings</th><th>reviewed</th></tr></thead> " 448 f " <tbody> { rows } </tbody></table> " 451 " <section><h2>Determinism reviews · the detail</h2> " 459 " <b>Authority envelope.</b> Stage 1 — froot holds " 460 " <b>write authority</b> only: it opens PRs, a human approves every " 461 " merge (commit authority = none). Trust, when any is granted, is " 462 " earned, narrow to npm patch bumps, conditional on its environment " 463 ' (judge <span class= " mono " >gemma4:e4b</span>, lockfile-only regen), ' 464 " revocable, and time-expiring. Today it records the track record; it " 465 " does not yet act on it.<br> " 466 " Everything above is derived on this request from GitHub (outcomes) + " 467 " Temporal (runs) + ClickHouse (telemetry). froot keeps no database; " 468 " reload to recompute. " 473 def _pr_link ( row : BumpRow ) - > str : 474 if row . pr_url is None or row . pr_number is None : 475 return ' <span class= " mut " >—</span> ' 476 return f ' <a href= " { escape ( row . pr_url , quote = True ) } " ># { row . pr_number } </a> ' 479 def _review_pr_link ( row : ReviewRow ) - > str : 480 if row . pr_url is None or row . pr_number is None : 481 return ' <span class= " mut " >—</span> ' 482 return f ' <a href= " { escape ( row . pr_url , quote = True ) } " ># { row . pr_number } </a> ' 485 def _findings_cell ( row : ReviewRow ) - > str : 486 """A review's findings: 'clean', or the hazard count + rules + comment.""" 487 if row . findings == 0 : 488 return ' <span class= " ok " >clean</span> ' 490 f ' <span class= " mono mut " > { escape ( " , " . join ( row . rules ) ) } </span> ' 495 f ' <a href= " { escape ( row . comment_url , quote = True ) } " >comment</a> ' 499 noun = " hazard " if row . findings == 1 else " hazards " 500 return f ' <span class= " bad " > { row . findings } { noun } </span> { rules } { comment } ' 503 def _state_tag ( state : str ) - > str : ⋯ 39 lines hidden (lines 507–545) 513 return f ' <span class= " { cls } " > { escape ( state ) } </span> ' 516 def _short_id ( workflow_id : str ) - > str : 517 """Drop the ``froot-bump-`` prefix for a readable failures row.""" 518 return workflow_id . removeprefix ( " froot-bump- " ) 521 def page ( model : DashboardModel ) - > str : 522 """Render the whole dashboard as one self-contained HTML document.""" 526 _track_record ( model ) , 527 _verification ( model ) , 532 _review_heartbeat ( model ) , 533 _review_record ( model ) , 539 ' <!doctype html><html lang= " en " ><head><meta charset= " utf-8 " > ' 540 ' <meta name= " viewport " content= " width=device-width,initial-scale=1 " > ' 541 " <title>froot · read-model</title> " 542 f " <style> { _CSS } </style></head><body><main> " 544 + " </main></body></html> "
Verified: ruff (format + lint), mypy strict (93 files), and 186 tests — 11 new, covering loop liveness, the record counts, the repo-prefix attribution, and the rendered sections (flagged vs. clean).