dependency-patch and security-patch are distinct trust classes that never share a record (§3.9), so each earns its own tab. LoopView holds one loop's complete standing — the same shape the combined view has, scoped to one loop.
src/froot/dashboard/model.py · 479 lines src/froot/dashboard/model.py 479 lines · Python expand all
⋯ 391 lines hidden (lines 1–391) 1 """The dashboard's view model — pure, frozen, fully derived. 3 These types are the shape the renderer projects to HTML. They carry *already 4 computed* numbers (the aggregates live in :mod:`~froot.dashboard.read_model`), 5 so the renderer is a dumb projection and every figure on the page is 6 unit-tested apart from any I/O. Nothing here is persisted: a 7 :class:`DashboardModel` is built fresh per request and discarded after the 8 response (derive, never store). 11 from __future__ import annotations 13 from datetime import datetime 15 from froot . domain . base import Frozen 18 class SourceHealth ( Frozen ) : 19 """Whether one external truth answered this request. 22 name: The source label (``github`` / ``temporal`` / ``clickhouse``). 23 ok: True when the source returned data; False when it errored or is off. 24 detail: A short human note — the row count, ``off``, or the error. 32 class ScanLoop ( Frozen ) : 33 """The liveness of one repo's durable scan schedule (the signal stage). 36 repo: The ``owner/name`` slug this loop watches. 37 loop: Which maintenance loop this row is for (``dependency-patch`` / 38 ``security-patch``), so the two loops' scans show distinctly. 39 status: The current scan workflow status (``running`` / 40 ``continued_as_new`` / ``terminated`` / ``none`` / ...), lowercased. 41 live: True when the loop is actively self-scheduling (running / CAN). 42 last_tick: When the current scan execution started (≈ the last tick), 43 or ``None`` if no scan workflow exists for the repo. 47 loop : str = " dependency-patch " 50 last_tick : datetime | None 54 """One proposed dependency bump, joined across GitHub and Temporal. 56 GitHub is authoritative for the outcome (state / timestamps / PR); Temporal 57 enriches with the model verdict and the CI reading while it is still in the 58 7-day window, with the PR body as the durable fallback for the verdict. 61 repo: The ``owner/name`` slug. 62 loop: Which loop proposed it (``dependency-patch`` / 63 ``security-patch``), from the PR's loop label — so the two loops' 65 package: The bumped dependency. 66 from_version: The version bumped from, if known. 67 to_version: The version bumped to. 68 state: The GitHub PR state — ``open`` / ``merged`` / ``closed``. 69 verdict: The changelog verdict (``clean`` / ``risky`` / ``unknown``), or 70 ``None`` when neither Temporal nor the PR body yields one. 71 ci: The terminal CI reading (``passed`` / ``failed`` / ``absent`` / 72 ``timed_out``), or ``None`` when only durable GitHub data remains. 73 pr_number: The PR number, if a PR exists. 74 pr_url: The PR URL, if a PR exists. 75 opened_at: When the PR was opened. 76 merged_at: When the PR was merged, if it was. 77 ttm_minutes: Time-to-merge in minutes (merged - opened), if merged. 78 age_hours: Age in hours for a still-open PR (now - opened), or ``None``. 79 would_auto_merge: For an open PR, whether it meets the earned-autonomy 80 grant — the loop's actual merge decision on an allowlisted repo, the 81 advisory shadow-gate verdict everywhere else (the default). 82 held_reason: Why an open PR would *not* auto-merge, the first blocker. 83 post_merge: For a merged PR, whether the merge *held* — ``held`` (the 84 branch's CI stayed green after the merge), ``broke`` (it went red), 85 ``reverted`` (a later commit reverted it), or ``None`` when no 86 post-merge signal is recoverable (the outcome leg, coarse). 90 loop : str = " dependency-patch " 92 from_version : str | None 99 opened_at : datetime | None 100 merged_at : datetime | None 101 ttm_minutes : float | None 102 age_hours : float | None 103 would_auto_merge : bool = False 104 held_reason : str | None = None 105 post_merge : str | None = None 106 # The judgment environment (judge-model slug) the PR was opened under, or 107 # ``None`` if unstamped; the gate counts only the current env (§3.7). 108 env : str | None = None 111 class ClassGate ( Frozen ) : 112 """The earned-autonomy standing of one (repo, loop) class. 114 The MHE economics of approval (§3.6) made legible: a class earns its gate 115 move with a high enough approval rate over enough recently-decided PRs, 116 and the budget framing shows what moving the gate would reclaim. 119 repo: The ``owner/name`` slug. 120 loop: The loop this class is for. 121 decided: PRs decided (merged or closed) in the recent window. 122 merged: How many of those were merged. 123 merge_rate: ``merged / decided``, or ``None`` if none decided. 124 determined: Merges with a confirmed post-merge outcome in the window 125 (held / broke / reverted) — the defect bearing's evidence. 126 defects: Of those, how many broke the branch or were reverted. 127 defect_rate: ``defects / determined``, or ``None`` if none determined — 128 the second, independent bearing the gate triangulates against. 129 prior_env_decided: Decided PRs in the window earned under a *different* 130 (or no) environment — they no longer count, so this figure makes a 131 model-change reset legible rather than a mysterious drop to zero. 132 earned: Whether the class has earned its gate move under the policy. 133 blocker: Why it has not, if it has not (else ``None``). 134 approvals_per_week: The steward approvals this class costs now 135 (merges per week over the window) — the current budget draw. 136 reclaim_per_week: How many of those a gate move would auto-merge 137 (the clean-and-green ones), i.e. the budget reclaimed — ``0`` until 138 the class is ``earned``, since an un-earned class reclaims nothing. 139 window_days: The look-back window the figures cover. 146 merge_rate : float | None 149 defect_rate : float | None 150 prior_env_decided : int 153 approvals_per_week : float 154 reclaim_per_week : float 158 class Failure ( Frozen ) : 159 """A bump loop that did not close — the honest friction signal. 162 workflow_id: The Temporal workflow id (encodes repo/package/target). 163 kind: ``terminated`` or ``failed``. 164 reason: The human-readable termination/failure reason, if recovered. 165 when: When the workflow closed, if known. 171 when : datetime | None 174 class ActivityStat ( Frozen ) : 175 """Latency of one activity stage, from traces (run-telemetry enrichment). 178 name: The activity name (``scan_candidates``, ``open_pull_request``). 179 count: How many executions in the window. 180 avg_ms: Mean duration in milliseconds. 181 max_ms: Max duration in milliseconds. 190 class RunTelemetry ( Frozen ) : 191 """Trace-derived run telemetry from ClickHouse, or an unavailable marker. 194 available: True when ClickHouse answered with froot traces. 195 total_spans: Total froot spans in the window. 196 error_spans: Spans that ended in an error. 197 last_activity: The most recent froot span timestamp. 198 window_days: The look-back window the figures cover. 199 activities: Per-stage latency rows. 205 last_activity : datetime | None 207 activities : tuple [ ActivityStat , . . . ] 210 class TrackRecord ( Frozen ) : 211 """The reputation headline, derived from GitHub PR outcomes. 214 opened: Total bumps froot has proposed. 215 merged: How many a human merged. 216 closed_unmerged: How many were closed without merging. 217 open_now: How many are still awaiting a decision. 218 merge_rate: ``merged / (merged + closed_unmerged)``, or ``None`` if none 219 have been decided yet. 220 median_ttm_minutes: Median time-to-merge across merged PRs, or ``None``. 227 merge_rate : float | None 228 median_ttm_minutes : float | None 231 class Verification ( Frozen ) : 232 """The CI-oracle breakdown — kept honest about whether an oracle existed. 235 passed: Bumps whose CI went green. 236 failed: Bumps whose CI went red. 237 absent: Bumps where no CI check existed (no oracle). 238 timed_out: Bumps where froot stopped waiting on CI. 239 unknown: Bumps with no recoverable CI reading (aged out of Temporal). 240 oracle_existed: Bumps where a real oracle reported (passed + failed). 241 with_reading: Bumps with any CI reading at all (excludes ``unknown``). 253 class Reliability ( Frozen ) : 254 """Did the merges *hold*? — the post-merge outcome leg (coarse, low-recall). 256 A merge is not the same as a success: the success leg is whether the merge 257 *held* once it landed. This breaks recent merges into held / broke / 258 reverted, with the honest caveat that it sees only what the default branch's 259 CI and git-reverts reveal — a manual or bundled revert is invisible, so the 260 defect rate is a *floor*, not the truth. It is the natural-traffic bearing 261 the adversarial canary leg exercises. 264 held: Merges whose branch CI stayed green after the merge. 265 broke: Merges whose branch CI went red after the merge. 266 reverted: Merges a later commit git-reverted (the minority that are). 267 unverified: Merges with no recoverable post-merge signal (no branch 268 oracle, or aged past the commit window) — never counted as held. 269 determined: ``held + broke + reverted`` — merges actually classified. 270 defect_rate: ``(broke + reverted) / determined``, or ``None`` if none 271 were determined. A floor on the true defect rate, by construction. 272 window_days: The look-back window over merges considered. 280 defect_rate : float | None 285 """Adversarial canary probes — does the guardrail still bite? (§2.11). 287 A canary is a deliberately-bad bump planted to test the guardrail; a healthy 288 loop must refuse to merge it. These counts are kept **strictly apart** from 289 the genuine track record and defect rate — a synthetic failure must never 290 pollute the real bearings — and shown on their own. ``escaped > 0`` is the 291 alarm: a known-bad bump that landed means the guardrail has a hole. 294 caught: Probes the guardrail refused (closed, or never landed). 295 escaped: Probes that merged anyway — a guardrail hole (should be 0). 296 pending: Probes still in flight (open, no verdict yet). 297 total: All canary probes seen. 306 class Judgment ( Frozen ) : 307 """The model's changelog-verdict mix and its calibration against CI. 310 clean: Verdicts of ``clean``. 311 risky: Verdicts of ``risky``. 312 unknown: Verdicts of ``unknown``. 313 none: Bumps with no recoverable verdict. 314 clean_but_failed: ``clean`` verdicts whose CI failed (mis-judged). 315 flagged_but_passed: ``risky``/``unknown`` verdicts whose CI passed. 322 clean_but_failed : int 323 flagged_but_passed : int 326 class ReviewLoop ( Frozen ) : 327 """Liveness of one repo's determinism-review loop (the transitive ring). 330 repo: The ``owner/name`` slug this loop reviews. 331 status: The review workflow status (``running`` / 332 ``continued_as_new`` / ``terminated`` / ...), lowercased. 333 live: True when the loop is actively self-scheduling. 334 last_tick: When the current review execution started (≈ the last tick), 335 or ``None`` if no review workflow exists for the repo. 341 last_tick : datetime | None 344 class ReviewRow ( Frozen ) : 345 """One per-PR determinism review, from its ``PrReviewWorkflow`` result. 348 repo: The ``owner/name`` slug the PR belongs to. 349 pr_number: The reviewed PR number, if known. 350 pr_url: The PR's web URL, if it can be formed. 351 head_sha: The head commit the review ran against. 352 findings: How many transitive hazards the review surfaced. 353 rules: The distinct banned calls flagged (``datetime.datetime.now``…). 354 comment_url: The advisory comment's URL, if one was posted. 355 status: The review workflow status (``completed`` / ``running`` / ...). 356 reviewed_at: When the review closed, or started if still running. 360 pr_number : int | None 364 rules : tuple [ str , . . . ] 365 comment_url : str | None 367 reviewed_at : datetime | None 370 class ReviewRecord ( Frozen ) : 371 """The determinism reviewer's headline, derived from completed reviews. 373 The hazard-resolved rate (was a flagged hazard gone on a later commit?) is 374 a later addition — it needs accumulated cross-commit history, so it is not 378 reviewed: Completed per-PR reviews in the recent window. 379 flagged: Reviews that surfaced at least one hazard. 380 clean: Reviews that surfaced none. 381 hazards: Total hazards surfaced across all reviews. 382 repos_covered: Distinct repos with a live review loop. 392 class LoopView ( Frozen ) : 393 """One bump loop's complete standing — the same treatment for every loop. 395 dependency-patch and security-patch are distinct trust classes (§3.9) that 396 never share a record, so each gets its own self-contained view: its gate and 397 four bearings, its queue, its detail. Built by partitioning the bump rows by 398 loop and running the same aggregates the combined view uses, so a loop's tab 399 is exactly the combined dashboard scoped to that one loop. 402 loop: The loop key (``dependency-patch`` / ``security-patch``). 403 title: The human title for the tab. 404 scan_loops: Liveness of this loop's per-repo scan schedules. 405 scan_interval_seconds: This loop's scan cadence (telemetry-in-context). 406 track_record: This loop's reputation headline. 407 class_gates: This loop's per-repo earned-autonomy standing (the gate). 408 verification: This loop's CI-oracle breakdown. 409 reliability: This loop's post-merge outcome leg. 410 probes: This loop's adversarial canary tally. 411 judgment: This loop's model-verdict mix and calibration. 412 gate: This loop's open PRs awaiting a human, freshest last. 413 bumps: This loop's proposed bumps, newest first. 414 failures: This loop's bump loops that did not close. 419 scan_loops : tuple [ ScanLoop , . . . ] 420 scan_interval_seconds : int 421 track_record : TrackRecord 422 class_gates : tuple [ ClassGate , . . . ] 423 verification : Verification 424 reliability : Reliability 427 gate : tuple [ BumpRow , . . . ] 428 bumps : tuple [ BumpRow , . . . ] 429 failures : tuple [ Failure , . . . ] ⋯ 48 lines hidden (lines 432–479) 432 class DashboardModel ( Frozen ) : 433 """The whole 10,000ft view, fully derived and ready to render. 436 generated_at: When this view was assembled (UTC). 437 repos_configured: The repos froot is pointed at (``FROOT_REPOS``). 438 scan_interval_seconds: The configured gap between scan ticks. 439 sources: Per-source health for this request. 440 scan_loops: Liveness of each repo's scan schedule. 441 track_record: The reputation headline. 442 class_gates: The earned-autonomy standing per (repo, loop) — the gate. 443 verification: The CI-oracle breakdown. 444 reliability: Did the merges hold post-merge (the outcome leg, coarse). 445 probes: Adversarial canary results, kept apart from the real bearings. 446 judgment: The model-verdict mix and calibration. 447 gate: Open PRs awaiting a human, the freshest last. 448 bumps: Every proposed bump, newest first (the detail behind the stats). 449 failures: Bump loops that did not close. 450 review_interval_seconds: The configured gap between review poll ticks. 451 review_loops: Liveness of each Temporal repo's determinism-review loop. 452 review_record: The determinism reviewer's headline. 453 reviews: Every per-PR determinism review, newest first. 454 telemetry: Trace-derived run telemetry (best-effort). 455 bump_loops: One self-contained view per bump loop — the per-loop tabs. 456 The top-level bump aggregates above are the combined ("all loops") 457 view; these split it per trust class. 460 generated_at : datetime 461 repos_configured : tuple [ str , . . . ] 462 scan_interval_seconds : int 463 sources : tuple [ SourceHealth , . . . ] 464 scan_loops : tuple [ ScanLoop , . . . ] 465 track_record : TrackRecord 466 class_gates : tuple [ ClassGate , . . . ] 467 verification : Verification 468 reliability : Reliability 471 gate : tuple [ BumpRow , . . . ] 472 bumps : tuple [ BumpRow , . . . ] 473 failures : tuple [ Failure , . . . ] 474 review_interval_seconds : int 475 review_loops : tuple [ ReviewLoop , . . . ] 476 review_record : ReviewRecord 477 reviews : tuple [ ReviewRow , . . . ] 478 telemetry : RunTelemetry 479 bump_loops : tuple [ LoopView , . . . ] = ( )
The read-model partitions the already-computed bump rows by loop and runs the same aggregates per loop, so a loop's tab is the whole dashboard scoped to one loop. Failures live in Temporal, not the PR rows, so they are attributed by workflow id — the segmentless ids are dependency-patch, the security-patch-segmented ones are security-patch.
src/froot/dashboard/read_model.py · 792 lines src/froot/dashboard/read_model.py 792 lines · Python expand all
⋯ 494 lines hidden (lines 1–494) 1 """Assemble the dashboard view — pure, from the three readers' output. 3 This is the reputation read-model proper: it joins GitHub (authoritative 4 outcomes) to Temporal (recent verdict + CI reading) by PR number, derives the 5 MHE-framed aggregates (track record, verification, judgment, the approval 6 queue), and returns a fully-computed 7 :class:`~froot.dashboard.model.DashboardModel` the renderer just projects. No 8 I/O and no clock of its own — ``now`` is passed in — so every figure on the 9 page is unit-tested apart from the network. 12 from __future__ import annotations 14 from typing import TYPE_CHECKING 16 from froot . dashboard . model import ( 34 from froot . domain . loop import Loop 35 from froot . domain . repo import RepoRef , TargetRepo 36 from froot . policy . autonomy import AutonomyPolicy , class_earned , pr_autonomy 37 from froot . policy . canary import is_canary , score_probe 38 from froot . policy . naming import review_workflow_id , scan_workflow_id 39 from froot . result import Ok 42 from datetime import datetime 44 from froot . dashboard . github_source import GithubPr 45 from froot . dashboard . temporal_source import ( 52 # The conservative fallback policy: empty allowlist, so the shadow gate holds 53 # everything until a caller passes a real one (built from FROOT_AUTOMERGE_*). 54 _DEFAULT_POLICY = AutonomyPolicy ( ) 56 _LIVE_STATUSES = frozenset ( { " running " , " continued_as_new " } ) 57 # Every way a bump can end without closing cleanly — all belong in the honest 58 # Failures panel, not silently dropped. 59 _FAILURE_STATUSES = frozenset ( { " terminated " , " failed " , " canceled " , " timed_out " } ) 62 def _median ( values : list [ float ] ) - > float | None : 63 """The median of ``values``, or ``None`` if empty (pure).""" 66 ordered = sorted ( values ) 67 mid = len ( ordered ) / / 2 68 if len ( ordered ) % 2 == 1 : 70 return ( ordered [ mid - 1 ] + ordered [ mid ] ) / 2 73 def _scan_id ( repo : str , loop : Loop ) - > str | None : 74 """The deterministic scan-loop id for an ``owner/name`` slug + loop.""" 75 match RepoRef . parse ( repo ) : 77 return scan_workflow_id ( TargetRepo ( repo = ref ) , loop ) 83 repos : tuple [ str , . . . ] , 84 loops : tuple [ Loop , . . . ] , 85 scans : tuple [ ScanExecution , . . . ] , 86 ) - > tuple [ ScanLoop , . . . ] : 87 """One liveness row per configured (repo, loop) (latest execution wins).""" 88 by_id : dict [ str , ScanExecution ] = { } 90 current = by_id . get ( scan . workflow_id ) 91 if current is None or _newer ( scan . start , current . start ) : 92 by_id [ scan . workflow_id ] = scan 93 rows : list [ ScanLoop ] = [ ] 96 scan_id = _scan_id ( repo , loop ) 97 execution = by_id . get ( scan_id ) if scan_id is not None else None 113 status = execution . status , 114 live = execution . status in _LIVE_STATUSES , 115 last_tick = execution . start , 121 def _newer ( a : datetime | None , b : datetime | None ) - > bool : 122 """True if ``a`` is a later instant than ``b`` (``None`` is oldest).""" 132 prs : tuple [ GithubPr , . . . ] , 133 bumps : tuple [ BumpExecution , . . . ] , 134 outcomes : dict [ tuple [ str , int ] , str ] , 135 ) - > tuple [ BumpRow , . . . ] : 136 """Join GitHub PRs (authoritative) to Temporal outcomes by PR number. 138 ``outcomes`` carries the post-merge signal (``held`` / ``broke`` / 139 ``reverted`` / ``unknown``) per recently-merged ``(repo, number)``; it is 140 absent for older or non-merged PRs, which leaves ``post_merge`` ``None``. 142 # Keyed on (repo, PR number), not the bare number: Temporal lists every 143 # repo's bumps in the namespace, so two repos that each have a PR #N would 144 # otherwise cross-attribute one's verdict/CI onto the other's row. 145 by_pr : dict [ tuple [ str , int ] , BumpExecution ] = { 146 ( bump . repo , bump . pr_number ) : bump 148 if bump . repo is not None and bump . pr_number is not None 150 rows : list [ BumpRow ] = [ ] 152 execution = by_pr . get ( ( pr . repo , pr . number ) ) 153 verdict = ( execution . verdict if execution else None ) or pr . verdict 154 ci = execution . ci if execution else None 155 ttm = _minutes_between ( pr . opened_at , pr . merged_at ) 156 age = _hours_between ( pr . opened_at , now ) if pr . state == " open " else None 161 package = pr . package or " ? " , 162 from_version = pr . from_version , 163 to_version = pr . to_version or " ? " , 169 opened_at = pr . opened_at , 170 merged_at = pr . merged_at , 173 post_merge = outcomes . get ( ( pr . repo , pr . number ) ) , 177 rows . sort ( key = _opened_sort_key , reverse = True ) 181 def _opened_sort_key ( row : BumpRow ) - > float : 182 """Sort key putting the most recently opened PR first (unknown last).""" 183 return row . opened_at . timestamp ( ) if row . opened_at is not None else 0.0 187 start : datetime | None , end : datetime | None 189 """Whole-ish minutes from ``start`` to ``end``, or ``None``.""" 190 if start is None or end is None : 192 return round ( ( end - start ) . total_seconds ( ) / 60 , 1 ) 196 start : datetime | None , end : datetime | None 198 """Hours from ``start`` to ``end``, or ``None``.""" 199 if start is None or end is None : 201 return round ( ( end - start ) . total_seconds ( ) / 3600 , 1 ) 204 def _track_record ( rows : tuple [ BumpRow , . . . ] ) - > TrackRecord : 205 """Counts, merge rate, and median time-to-merge from the bump rows.""" 206 merged = [ r for r in rows if r . state == " merged " ] 207 closed = sum ( 1 for r in rows if r . state == " closed " ) 208 open_now = sum ( 1 for r in rows if r . state == " open " ) 209 decided = len ( merged ) + closed 210 ttms = [ r . ttm_minutes for r in merged if r . ttm_minutes is not None ] 214 closed_unmerged = closed , 216 merge_rate = ( len ( merged ) / decided ) if decided else None , 217 median_ttm_minutes = _median ( ttms ) , 221 def _verification ( rows : tuple [ BumpRow , . . . ] ) - > Verification : 222 """The CI-oracle breakdown, keeping ``absent`` distinct from a failure.""" 223 passed = sum ( 1 for r in rows if r . ci == " passed " ) 224 failed = sum ( 1 for r in rows if r . ci == " failed " ) 225 absent = sum ( 1 for r in rows if r . ci == " absent " ) 226 timed_out = sum ( 1 for r in rows if r . ci == " timed_out " ) 227 unknown = sum ( 1 for r in rows if r . ci is None ) 234 oracle_existed = passed + failed , 235 with_reading = passed + failed + absent + timed_out , 239 def _reliability ( rows : tuple [ BumpRow , . . . ] , window_days : int ) - > Reliability : 240 """The post-merge outcome breakdown — did the merges hold? (coarse). 242 Reads only the ``post_merge`` tag the outcome reader set: ``unknown`` means 243 an in-window merge we could not classify (no branch oracle / aged out), kept 244 distinct from a held one; ``None`` (older / non-merged) is not counted here. 245 The defect rate is a floor — manual and bundled reverts are invisible. 247 held = sum ( 1 for r in rows if r . post_merge == " held " ) 248 broke = sum ( 1 for r in rows if r . post_merge == " broke " ) 249 reverted = sum ( 1 for r in rows if r . post_merge == " reverted " ) 250 unverified = sum ( 1 for r in rows if r . post_merge == " unknown " ) 251 determined = held + broke + reverted 256 unverified = unverified , 257 determined = determined , 258 defect_rate = ( ( broke + reverted ) / determined ) if determined else None , 259 window_days = window_days , 263 def _probes ( canary_rows : tuple [ BumpRow , . . . ] ) - > Probes : 264 """Tally the adversarial canary probes — caught / escaped / pending. 266 These rows are the synthetic bad bumps; they are scored on the strict bar 267 (a canary must never merge) and kept out of every genuine bearing. 269 scored = [ score_probe ( r . state ) for r in canary_rows ] 271 caught = sum ( 1 for s in scored if s == " caught " ) , 272 escaped = sum ( 1 for s in scored if s == " escaped " ) , 273 pending = sum ( 1 for s in scored if s == " pending " ) , 278 def _judgment ( rows : tuple [ BumpRow , . . . ] ) - > Judgment : 279 """The verdict mix plus the two calibration cells worth flagging.""" 280 clean = sum ( 1 for r in rows if r . verdict == " clean " ) 281 risky = sum ( 1 for r in rows if r . verdict == " risky " ) 282 unknown = sum ( 1 for r in rows if r . verdict == " unknown " ) 283 none = sum ( 1 for r in rows if r . verdict is None ) 284 clean_but_failed = sum ( 285 1 for r in rows if r . verdict == " clean " and r . ci == " failed " 287 flagged_but_passed = sum ( 290 if r . verdict in ( " risky " , " unknown " ) and r . ci == " passed " 297 clean_but_failed = clean_but_failed , 298 flagged_but_passed = flagged_but_passed , 302 def _decided_at ( row : BumpRow ) - > datetime | None : 303 """When a decided PR was decided — merge time, else open time as a proxy. 305 A merged PR has an exact merge instant; a closed-unmerged one does not (the 306 issues list froot reads carries no close timestamp), so it is windowed by 307 when it opened. The proxy only nudges a closed PR's window membership by its 308 own lifetime — close enough for a recency window measured in months. 310 return row . merged_at or row . opened_at 313 def _within ( when : datetime | None , now : datetime , window_days : int ) - > bool : 314 """Whether ``when`` falls within ``window_days`` before ``now`` (pure).""" 317 return ( now - when ) . total_seconds ( ) < = window_days * 86400.0 322 rows : tuple [ BumpRow , . . . ] , 323 repos : tuple [ str , . . . ] , 324 loops : tuple [ Loop , . . . ] , 325 policy : AutonomyPolicy , 327 ) - > tuple [ ClassGate , . . . ] : 328 """The earned-autonomy standing of each (repo, loop) class. 330 Counts only PRs *decided within the window* — trust is recent, not lifetime 331 (§2.11) — *and* earned under the current ``environment`` (§3.7's conditional 332 property): a PR opened under a different judge model no longer counts, so a 333 model swap resets the class. ``prior_env_decided`` keeps that reset legible. 334 The budget figures (approvals / reclaim per week) translate the record into 335 the steward-time MHE meters (§3.6): what the class costs now, and what 336 moving its gate would hand back. 338 weeks = max ( policy . window_days / 7.0 , 1.0 ) 339 gates : list [ ClassGate ] = [ ] 346 and r . loop == loop . value 347 and r . state in ( " merged " , " closed " ) 348 and _within ( _decided_at ( r ) , now , policy . window_days ) 350 # An empty ``environment`` means "don't filter" (none configured); 351 # otherwise only PRs stamped with the current one count. 353 r for r in in_window if not environment or r . env == environment 355 prior_env_decided = len ( in_window ) - len ( decided_rows ) 356 merged_rows = [ r for r in decided_rows if r . state == " merged " ] 357 decided = len ( decided_rows ) 358 merged = len ( merged_rows ) 359 # The post-merge defect bearing, per class (§3.8): confirmed-held 360 # outcomes and how many of those went bad. Independent of the rate. 364 if r . post_merge in ( " held " , " broke " , " reverted " ) 367 1 for r in merged_rows if r . post_merge in ( " broke " , " reverted " ) 369 earned , blocker = class_earned ( 372 determined = determined , 376 # Reclaim is the budget a gate *move* hands back — so it is zero 377 # until the class has actually earned the move. Counting the 378 # clean-and-green merges of an un-earned class would imply savings 379 # the gate would refuse (every PR stays held at "class not earned"). 384 if r . verdict == " clean " and r . ci == " passed " 395 merge_rate = ( merged / decided ) if decided else None , 396 determined = determined , 399 ( defects / determined ) if determined else None 401 prior_env_decided = prior_env_decided , 404 approvals_per_week = round ( merged / weeks , 2 ) , 405 reclaim_per_week = round ( reclaimable / weeks , 2 ) , 406 window_days = policy . window_days , 414 prs : tuple [ GithubPr , . . . ] , 415 outcomes : dict [ tuple [ str , int ] , str ] , 418 policy : AutonomyPolicy , 421 """Whether one (repo, loop) class has earned its gate, from live data. 423 The exact computation the dashboard's class-gate panel shows, exposed so the 424 acting gate (the loop) reuses it rather than duplicating the windowing, 425 environment filter, and triangulation. Pure over already-fetched data. 427 rows = _bump_rows ( now , prs , ( ) , outcomes ) 428 gates = _class_gates ( now , rows , ( repo , ) , ( loop , ) , policy , environment ) 429 return any ( g . earned for g in gates ) 433 rows : tuple [ BumpRow , . . . ] , 434 gates : tuple [ ClassGate , . . . ] , 435 policy : AutonomyPolicy , 436 ) - > tuple [ BumpRow , . . . ] : 437 """Open PRs awaiting a human, most-aged first, each carrying a verdict. 439 The verdict is the gate's: would this PR auto-merge under its class's grant 440 — the loop's real decision where the repo is allowlisted, advisory (the 441 shadow gate) where it is not. The reason is the grant met, or the first 444 earned_by_class = { ( g . repo , g . loop ) : ( g . earned , g . blocker ) for g in gates } 445 open_rows = [ r for r in rows if r . state == " open " ] 447 key = lambda r : r . age_hours if r . age_hours is not None else 0.0 , 450 annotated : list [ BumpRow ] = [ ] 451 for row in open_rows : 452 earned , blocker = earned_by_class . get ( 453 ( row . repo , row . loop ) , ( False , " no record for this class " ) 455 verdict = pr_autonomy ( 466 " would_auto_merge " : verdict . would_merge , 468 None if verdict . would_merge else verdict . reason 473 return tuple ( annotated ) 476 def _failures ( bumps : tuple [ BumpExecution , . . . ] ) - > tuple [ Failure , . . . ] : 477 """Bump loops that did not close, newest first.""" 480 workflow_id = bump . workflow_id , 486 if bump . status in _FAILURE_STATUSES 489 key = lambda f : f . when . timestamp ( ) if f . when is not None else 0.0 , 492 return tuple ( failures ) 495 def _loop_title ( loop : Loop ) - > str : 496 """The human tab title for a loop key (``dependency-patch`` -> ...).""" 497 return loop . value . capitalize ( ) 500 def _failure_loop ( workflow_id : str , loops : tuple [ Loop , . . . ] ) - > Loop | None : 501 """Which loop a failed bump belongs to, from its workflow id. 503 Non-default loops carry their name as an id segment 504 (``froot-bump-security-patch-…``); dependency-patch is segmentless 505 (``froot-bump-…``), so it is the fallback once the specific loops miss. 508 if loop is not Loop . DEPENDENCY_PATCH and workflow_id . startswith ( 509 f " froot-bump- { loop . value } - " 512 if workflow_id . startswith ( " froot-bump- " ) : 514 ( loop for loop in loops if loop is Loop . DEPENDENCY_PATCH ) , None 521 rows : tuple [ BumpRow , . . . ] , 522 canary_rows : tuple [ BumpRow , . . . ] , 523 failures : tuple [ Failure , . . . ] , 524 scans : tuple [ ScanExecution , . . . ] , 525 repos : tuple [ str , . . . ] , 526 loops : tuple [ Loop , . . . ] , 527 policy : AutonomyPolicy , 529 scan_interval_seconds : int , 530 reliability_window_days : int , 531 ) - > tuple [ LoopView , . . . ] : 532 """One self-contained :class:`LoopView` per loop — the per-loop tabs. 534 Partitions the already-computed bump rows by loop and runs the very same 535 aggregates the combined view uses, so each loop's tab is the whole dashboard 536 scoped to one trust class (§3.9). Failures are attributed by workflow id. 538 views : list [ LoopView ] = [ ] 540 loop_rows = tuple ( r for r in rows if r . loop == loop . value ) 541 loop_canary = tuple ( r for r in canary_rows if r . loop == loop . value ) 542 loop_gates = _class_gates ( 543 now , loop_rows , repos , ( loop , ) , policy , environment 545 loop_failures = tuple ( 546 f for f in failures if _failure_loop ( f . workflow_id , loops ) is loop 551 title = _loop_title ( loop ) , 552 scan_loops = _scan_loops ( repos , ( loop , ) , scans ) , 553 scan_interval_seconds = scan_interval_seconds , 554 track_record = _track_record ( loop_rows ) , 555 class_gates = loop_gates , 556 verification = _verification ( loop_rows ) , 557 reliability = _reliability ( loop_rows , reliability_window_days ) , 558 probes = _probes ( loop_canary ) , 559 judgment = _judgment ( loop_rows ) , 560 gate = _gate ( loop_rows , loop_gates , policy ) , 562 failures = loop_failures , ⋯ 225 lines hidden (lines 568–792) 568 def _review_id ( repo : str ) - > str | None : 569 """The deterministic review-loop id for an ``owner/name`` slug, if valid.""" 570 match RepoRef . parse ( repo ) : 572 return review_workflow_id ( TargetRepo ( repo = ref ) ) 577 def _pr_review_prefix ( repo : str ) - > str | None : 578 """The id prefix every per-PR review of ``repo`` shares (the join key).""" 579 review_id = _review_id ( repo ) 580 if review_id is None : 582 # froot-review-<slug> -> froot-pr-review-<slug>- ; the pr/sha tail follows. 583 return " froot-pr-review- " + review_id . removeprefix ( " froot-review- " ) + " - " 586 def _attribute_repo ( workflow_id : str , repos : tuple [ str , . . . ] ) - > str | None : 587 """The configured repo a per-PR-review id belongs to (longest prefix).""" 588 best : str | None = None 591 prefix = _pr_review_prefix ( repo ) 592 if prefix and workflow_id . startswith ( prefix ) and len ( prefix ) > best_len : 593 best , best_len = repo , len ( prefix ) 598 repos : tuple [ str , . . . ] , reviews : tuple [ ReviewExecution , . . . ] 599 ) - > tuple [ ReviewLoop , . . . ] : 600 """One liveness row per repo that actually has a review loop. 602 Reviews are scoped to the Temporal repos, so a configured npm repo with no 603 review loop is omitted rather than shown as a dead one. 605 by_id : dict [ str , ReviewExecution ] = { } 606 for review in reviews : 607 current = by_id . get ( review . workflow_id ) 608 if current is None or _newer ( review . start , current . start ) : 609 by_id [ review . workflow_id ] = review 610 loops : list [ ReviewLoop ] = [ ] 612 review_id = _review_id ( repo ) 613 execution = by_id . get ( review_id ) if review_id is not None else None 614 if execution is None : 619 status = execution . status , 620 live = execution . status in _LIVE_STATUSES , 621 last_tick = execution . start , 628 pr_reviews : tuple [ PrReviewExecution , . . . ] , repos : tuple [ str , . . . ] 629 ) - > tuple [ ReviewRow , . . . ] : 630 """Project each per-PR review into a row, newest review first.""" 631 rows : list [ ReviewRow ] = [ ] 632 for execution in pr_reviews : 633 repo = _attribute_repo ( execution . workflow_id , repos ) 635 f " https://github.com/ { repo } /pull/ { execution . pr_number } " 636 if repo is not None and execution . pr_number is not None 642 pr_number = execution . pr_number , 644 head_sha = execution . head_sha , 645 findings = execution . findings , 646 rules = execution . rules , 647 comment_url = execution . comment_url , 648 status = execution . status , 649 reviewed_at = execution . close or execution . start , 653 key = lambda r : r . reviewed_at . timestamp ( ) if r . reviewed_at else 0.0 , 660 loops : tuple [ ReviewLoop , . . . ] , rows : tuple [ ReviewRow , . . . ] 662 """Counts over the completed reviews (resolved-rate is a later loop).""" 663 completed = [ r for r in rows if r . status == " completed " ] 664 flagged = sum ( 1 for r in completed if r . findings > 0 ) 665 hazards = sum ( r . findings for r in completed ) 667 reviewed = len ( completed ) , 669 clean = len ( completed ) - flagged , 671 repos_covered = len ( loops ) , 676 github_error : str | None , 678 temporal_error : str | None , 680 telemetry : RunTelemetry , 681 clickhouse_error : str | None , 682 ) - > tuple [ SourceHealth , . . . ] : 683 """Per-source health for the header strip.""" 684 clickhouse_ok = clickhouse_error is None 685 if clickhouse_error == " off " : 686 clickhouse_detail = " off " 687 elif clickhouse_error is not None : 688 clickhouse_detail = clickhouse_error 690 spans = telemetry . total_spans 691 clickhouse_detail = f " { spans } spans / { telemetry . window_days } d " 695 ok = github_error is None , 696 detail = github_error or f " { github_count } PRs " , 700 ok = temporal_error is None , 701 detail = temporal_error or f " { temporal_count } workflows " , 704 name = " clickhouse " , ok = clickhouse_ok , detail = clickhouse_detail 712 repos : tuple [ str , . . . ] , 713 loops : tuple [ Loop , . . . ] = ( Loop . DEPENDENCY_PATCH , ) , 714 policy : AutonomyPolicy = _DEFAULT_POLICY , 715 scan_interval_seconds : int , 716 review_interval_seconds : int , 717 github : tuple [ tuple [ GithubPr , . . . ] , str | None ] , 720 tuple [ ScanExecution , . . . ] , 721 tuple [ BumpExecution , . . . ] , 722 tuple [ ReviewExecution , . . . ] , 723 tuple [ PrReviewExecution , . . . ] , 727 telemetry : tuple [ RunTelemetry , str | None ] , 728 outcomes : dict [ tuple [ str , int ] , str ] | None = None , 729 reliability_window_days : int = 90 , 730 environment : str = " " , 732 """Build the whole view from the readers' ``(data, error)`` outputs. 734 ``outcomes`` is the post-merge signal per ``(repo, number)`` from the 735 outcome reader (best-effort); absent for a merge older than the window. 737 prs , github_error = github 738 ( scans , bumps , reviews , pr_reviews ) , temporal_error = temporal 739 run_telemetry , clickhouse_error = telemetry 741 all_rows = _bump_rows ( now , prs , bumps , outcomes or { } ) 742 # Canary probes are synthetic bad bumps — keep them out of every genuine 743 # bearing (track record, defect rate, gates) so a planted failure can never 744 # pollute the real reputation; they get their own tally. 745 canary_rows = tuple ( r for r in all_rows if is_canary ( r . to_version ) ) 746 rows = tuple ( r for r in all_rows if not is_canary ( r . to_version ) ) 747 class_gates = _class_gates ( now , rows , repos , loops , policy , environment ) 748 all_failures = _failures ( bumps ) 749 bump_loops = _bump_loops ( 759 scan_interval_seconds , 760 reliability_window_days , 762 review_loops = _review_loops ( repos , reviews ) 763 review_rows = _review_rows ( pr_reviews , repos ) 764 return DashboardModel ( 766 repos_configured = repos , 767 scan_interval_seconds = scan_interval_seconds , 772 len ( scans ) + len ( bumps ) + len ( reviews ) + len ( pr_reviews ) , 776 scan_loops = _scan_loops ( repos , loops , scans ) , 777 track_record = _track_record ( rows ) , 778 class_gates = class_gates , 779 verification = _verification ( rows ) , 780 reliability = _reliability ( rows , reliability_window_days ) , 781 probes = _probes ( canary_rows ) , 782 judgment = _judgment ( rows ) , 783 gate = _gate ( rows , class_gates , policy ) , 785 failures = all_failures , 786 review_interval_seconds = review_interval_seconds , 787 review_loops = review_loops , 788 review_record = _review_record ( review_loops , review_rows ) , 790 telemetry = run_telemetry , 791 bump_loops = bump_loops ,
⚠️ Gotcha
The top-level combined aggregates (track_record, etc.) stay as the 'all loops' summary — bump_loops splits them, it does not replace them, so existing read-model behaviour is unchanged.