Date: Wed, 10 Jun 2026 23:36:07 +0500
Subject: [PATCH 226/318] feat(html): runtime-diagnosis cockpit with waterfall
timeline
Rewrite the observability HTML as a top-down diagnosis cockpit instead of a
data dump: executive runtime summary (stat cards + slowest op / hottest
span / peak memory highlights), correlated finish->worker event chains
(horizontal causality breadcrumb + indented detail via a rail, no
card-in-card), a memory-pipeline cost table that flags no-op-but-costly
spans, an MCP tool matrix (latency + request/response/token payload), and a
per-chain waterfall timeline that places bars by start offset and width by
duration so the spawn handoff gap is visible. Self-contained branded SVG,
auto dark/light, no JS.
---
codeclone/observability/render_html.py | 530 ++++++++++++++++++-------
tests/test_observability_render.py | 137 +++++++
2 files changed, 517 insertions(+), 150 deletions(-)
diff --git a/codeclone/observability/render_html.py b/codeclone/observability/render_html.py
index cd366d63..49370d87 100644
--- a/codeclone/observability/render_html.py
+++ b/codeclone/observability/render_html.py
@@ -6,11 +6,16 @@
"""Branded HTML renderer for the observability ``TraceView`` (Phase 29 output).
-Self-contained single page: the CodeClone brand logo + brand tokens (Inter /
-JetBrains Mono / oklch indigo, auto dark-light), a focused embedded stylesheet,
-and inline SVG bars — no external assets, no ``report`` import, no JS required.
-The trace is a column-aligned grid: names, bars, durations and metrics line up
-across every row, and an operation's child operations nest under it.
+A single self-contained page rendered as a *runtime-diagnosis cockpit*, not a
+data dump. It is laid out for a top-down reading trajectory that answers the
+operator's questions in order: an executive summary that names where time and
+memory went; the correlated finish->worker event chains (a horizontal causality
+breadcrumb plus indented detail — nesting is shown with an indent rail, never a
+card inside a card); a memory-pipeline cost table that flags spans that ran but
+produced nothing; and an MCP tool matrix that surfaces payload noise.
+
+CodeClone brand mark + brand tokens (Inter / JetBrains Mono / oklch indigo, auto
+dark-light), inline SVG bars, no JS, no external assets, no ``report`` import.
"""
from __future__ import annotations
@@ -18,7 +23,20 @@
from collections.abc import Mapping
from html import escape
-from .views import AggregatesView, McpToolAggregate, OperationView, SpanView, TraceView
+from .views import (
+ AggregatesView,
+ McpToolAggregate,
+ OperationView,
+ SpanCostView,
+ SpanView,
+ TraceView,
+ WaterfallGroup,
+ WaterfallRow,
+)
+
+# A no-op span only deserves a "costly" warning once it has actually spent time.
+_NOOP_COSTLY_MS = 50.0
+_KNOWN_SURFACES = frozenset({"mcp", "cli", "memory"})
# Reuse of the CodeClone brand mark (report/html/widgets/icons.py:BRAND_LOGO).
_LOGO = (
@@ -40,6 +58,7 @@
--text:oklch(96% 0.010 275);--dim:oklch(74% 0.028 275);--mute:oklch(56% 0.028 275);
--accent:#818cf8;--accent-soft:color-mix(in oklch,#818cf8 30%,transparent);
--track:oklch(28% 0.02 275);--warn:#f59e0b;
+--warn-soft:color-mix(in oklch,#f59e0b 14%,transparent);
--mcp:#818cf8;--cli:#2dd4bf;--memory:#fbbf24;
--font:"Inter","Inter Variable",-apple-system,BlinkMacSystemFont,"Segoe UI",
Roboto,sans-serif;
@@ -49,88 +68,116 @@
--bg:oklch(98.5% 0.006 275);--surface:#fff;--surface-2:oklch(97.3% 0.006 275);
--border:oklch(89% 0.018 275);--text:oklch(24% 0.040 275);
--dim:oklch(44% 0.046 275);--mute:oklch(55% 0.040 275);
---accent:#4f46e5;--accent-soft:color-mix(in oklch,#4f46e5 28%,transparent);
---track:oklch(92% 0.012 275);--mcp:#4f46e5;--cli:#0d9488;--memory:#b45309;
+--accent:#4f46e5;--accent-soft:color-mix(in oklch,#4f46e5 26%,transparent);
+--track:oklch(92% 0.012 275);--warn:#b45309;
+--warn-soft:color-mix(in oklch,#b45309 12%,transparent);
+--mcp:#4f46e5;--cli:#0d9488;--memory:#b45309;
}}
html{-webkit-text-size-adjust:100%}
body{background:var(--bg);color:var(--text);font-family:var(--font);
font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;
-padding:34px 20px 80px}
-.wrap{max-width:1000px;margin:0 auto}
+padding:36px 22px 90px}
+.wrap{max-width:1040px;margin:0 auto}
.head{display:flex;align-items:center;gap:13px;margin-bottom:5px}
.logo{flex-shrink:0}
h1{font-size:20px;font-weight:600;letter-spacing:-0.01em}
-.sub{color:var(--dim);font-size:12.5px;margin:0 0 28px 43px;font-family:var(--mono)}
+.sub{color:var(--dim);font-size:12.5px;margin:0 0 30px 43px;font-family:var(--mono)}
.sub b{color:var(--text);font-weight:550}
-.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:32px}
+section{margin-bottom:30px}
+h2{font-size:11px;text-transform:uppercase;letter-spacing:0.09em;
+color:var(--mute);font-weight:600;margin:0 0 4px 2px}
+.shint{color:var(--mute);font-size:12px;margin:0 0 11px 2px}
+.panel{background:var(--surface);border:1px solid var(--border);
+border-radius:11px;overflow:hidden}
+.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:14px}
.card{background:var(--surface);border:1px solid var(--border);
-border-radius:10px;padding:14px 16px}
-.card .v{font-size:23px;font-weight:600;letter-spacing:-0.02em;font-family:var(--mono)}
+border-radius:11px;padding:14px 16px}
+.card .v{font-size:24px;font-weight:600;letter-spacing:-0.02em;
+font-family:var(--mono)}
.card .l{color:var(--mute);font-size:10.5px;text-transform:uppercase;
letter-spacing:0.07em;margin-top:4px}
+.card.warn{border-color:var(--warn-soft)}
.card.warn .v{color:var(--warn)}
.card.accent .v{color:var(--accent)}
-h2{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;
-color:var(--mute);font-weight:600;margin:0 0 10px 2px}
-section{margin-bottom:30px}
-.panel{background:var(--surface);border:1px solid var(--border);
-border-radius:10px;overflow:hidden}
-.badge{font-size:10px;font-weight:600;font-family:var(--mono);padding:2px 6px;
-border-radius:5px;text-transform:uppercase;letter-spacing:0.03em;
-justify-self:start;
-background:color-mix(in oklch,var(--c,var(--accent)) 15%,transparent);
+.lead{padding:4px 16px}
+.lrow{display:grid;grid-template-columns:158px minmax(0,1fr) auto;align-items:center;
+gap:14px;padding:11px 0;border-top:1px solid var(--border)}
+.lrow:first-child{border-top:none}
+.llabel{color:var(--mute);font-size:11px;text-transform:uppercase;
+letter-spacing:0.05em}
+.lval{display:flex;align-items:center;gap:9px;min-width:0}
+.lname{font-family:var(--mono);font-size:13px;overflow:hidden;
+text-overflow:ellipsis;white-space:nowrap}
+.lin{color:var(--mute);font-size:11.5px;font-family:var(--mono)}
+.lmetric{font-family:var(--mono);font-size:14px;font-weight:600;white-space:nowrap}
+.badge{font-size:10px;font-weight:600;font-family:var(--mono);padding:2px 7px;
+border-radius:5px;text-transform:uppercase;letter-spacing:0.03em;flex-shrink:0;
+background:color-mix(in oklch,var(--c,var(--accent)) 16%,transparent);
color:var(--c,var(--accent))}
.surf-mcp{--c:var(--mcp)}.surf-cli{--c:var(--cli)}.surf-memory{--c:var(--memory)}
-.name{font-family:var(--mono);font-size:12.5px;min-width:0;
-overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.dur{font-family:var(--mono);font-size:12.5px;text-align:right;
+.chip{font-size:10.5px;font-family:var(--mono);padding:1px 8px;border-radius:20px;
+background:var(--surface-2);color:var(--dim);border:1px solid var(--border);
white-space:nowrap}
-.rss{font-family:var(--mono);font-size:11.5px;color:var(--warn);
-text-align:right;white-space:nowrap;font-weight:550}
+.chip.warn{color:var(--warn);border-color:transparent;background:var(--warn-soft);
+font-weight:600}
.bar{display:block;width:100%;height:7px}
-.chip{font-size:10.5px;font-family:var(--mono);padding:1px 7px;border-radius:20px;
-background:var(--surface-2);color:var(--dim);border:1px solid var(--border);
+.dur{font-family:var(--mono);font-size:12.5px;text-align:right;white-space:nowrap;
+color:var(--dim)}
+.rss{font-family:var(--mono);font-size:11.5px;color:var(--warn);white-space:nowrap;
+font-weight:550}
+.meta{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0}
+.pay{font-family:var(--mono);font-size:11px;color:var(--mute);white-space:nowrap}
+.chain{padding:6px 16px 12px}
+.group{padding:13px 0;border-top:1px solid var(--border)}
+.group:first-child{border-top:none}
+.crumb{display:flex;align-items:center;flex-wrap:wrap;gap:9px;margin-bottom:10px}
+.crumb .node{display:flex;align-items:center;gap:7px}
+.crumb .cname{font-family:var(--mono);font-size:12px;color:var(--text)}
+.crumb .arrow{color:var(--mute);font-size:13px}
+.oprow,.spanrow{display:grid;
+grid-template-columns:minmax(0,1fr) 160px 64px minmax(92px,auto);
+align-items:center;column-gap:14px;row-gap:2px;padding:5px 0}
+.lead-cell{display:flex;align-items:center;gap:9px;min-width:0}
+.opname{font-family:var(--mono);font-size:13px;font-weight:550;overflow:hidden;
+text-overflow:ellipsis;white-space:nowrap}
+.spanname{font-family:var(--mono);font-size:12px;color:var(--dim);overflow:hidden;
+text-overflow:ellipsis;white-space:nowrap}
+.tick{color:var(--accent);opacity:0.6;font-size:11px;flex-shrink:0}
+.spanrow .counters{grid-column:2/-1;font-family:var(--mono);font-size:10.5px;
+color:var(--mute);display:flex;flex-wrap:wrap;gap:0 15px}
+.counters b{color:var(--dim);font-weight:550;margin-right:4px}
+.spans{padding-left:17px}
+.kids{margin-left:13px;padding-left:17px;border-left:2px solid var(--accent-soft)}
+.wf{padding:8px 16px 12px}
+.wf-group{padding:13px 0;border-top:1px solid var(--border)}
+.wf-group:first-child{border-top:none}
+.wf-cap{display:flex;align-items:center;gap:8px;margin-bottom:9px;
+font-family:var(--mono);font-size:11px;color:var(--mute)}
+.wf-cap b{color:var(--dim);font-weight:600}
+.wf-row{display:grid;grid-template-columns:minmax(150px,238px) minmax(0,1fr) 58px;
+align-items:center;column-gap:12px;padding:2px 0}
+.wf-label{font-family:var(--mono);font-size:11.5px;overflow:hidden;
+text-overflow:ellipsis;white-space:nowrap}
+.wf-label.op{color:var(--text);font-weight:550}
+.wf-label.span{color:var(--dim)}
+.wf-track{position:relative;height:14px;background:var(--track);border-radius:4px}
+.wf-bar{position:absolute;top:2px;height:10px;border-radius:3px;
+background:var(--c,var(--accent))}
+.wf-bar.span{top:3px;height:8px;opacity:0.8}
+.wf-dur{font-family:var(--mono);font-size:11px;color:var(--mute);text-align:right;
white-space:nowrap}
-.chip.unknown{color:var(--warn);border-color:transparent;
-background:color-mix(in oklch,var(--warn) 13%,transparent)}
-.slow{display:grid;
-grid-template-columns:58px minmax(0,1fr) 150px 56px 78px;
-align-items:center;column-gap:13px;padding:9px 16px;
-border-top:1px solid var(--border)}
-.slow:first-child{border-top:none}
-.slow .name{color:var(--text)}
-.slow .dur{color:var(--dim)}
table{width:100%;border-collapse:collapse;font-size:12.5px}
th{text-align:left;padding:9px 16px;color:var(--mute);font-size:10.5px;
text-transform:uppercase;letter-spacing:0.05em;
-border-bottom:1px solid var(--border)}
-td{padding:8px 16px;border-top:1px solid var(--border);font-family:var(--mono)}
+border-bottom:1px solid var(--border);white-space:nowrap}
+td{padding:9px 16px;border-top:1px solid var(--border);font-family:var(--mono);
+white-space:nowrap}
td.t{font-family:var(--font)}
th.r,td.r{text-align:right}
-.tree{padding:8px}
-.op{border:1px solid var(--border);border-radius:9px;overflow:hidden;
-margin:7px 0;background:var(--surface)}
-.op:first-child{margin-top:0}
-.op .op{margin:8px 10px 10px 22px;border-left:2px solid var(--accent-soft)}
-.op-head{display:flex;align-items:center;gap:10px;padding:9px 13px;
-background:var(--surface-2)}
-.op-head .name{flex:1;font-size:13px;font-weight:550;color:var(--text)}
-.op-head .pay{font-family:var(--mono);font-size:11px;color:var(--mute);
-white-space:nowrap}
-.spans{padding:3px 0 5px}
-.span{display:grid;
-grid-template-columns:minmax(0,1fr) 150px 56px minmax(120px,0.9fr);
-align-items:center;column-gap:13px;row-gap:1px;padding:4px 14px 4px 16px}
-.span .name{grid-column:1;grid-row:1;color:var(--dim)}
-.span .bar{grid-column:2;grid-row:1}
-.span .dur{grid-column:3;grid-row:1;color:var(--dim)}
-.span .smeta{grid-column:4;grid-row:1;display:flex;align-items:center;
-gap:8px;min-width:0;overflow:hidden}
-.span .counters{grid-column:2/-1;grid-row:2;font-family:var(--mono);
-font-size:10.5px;color:var(--mute);display:flex;flex-wrap:wrap;gap:0 15px}
-.kv b{color:var(--dim);font-weight:550;margin-right:4px}
-.empty{padding:28px;text-align:center;color:var(--mute);font-size:13px}
-.foot{margin-top:36px;color:var(--mute);font-size:11px;text-align:center;
+tr.flag td{background:var(--warn-soft)}
+.muted{color:var(--mute)}
+.empty{padding:30px;text-align:center;color:var(--mute);font-size:13px}
+.foot{margin-top:38px;color:var(--mute);font-size:11px;text-align:center;
font-family:var(--mono)}
"""
@@ -144,7 +191,9 @@ def _ms(value: float) -> str:
def _mb(value: float | None) -> str:
- return "—" if value is None else f"{value:.1f} MB"
+ if value is None:
+ return "—"
+ return f"{value / 1024:.1f} GB" if value >= 1024 else f"{value:.1f} MB"
def _bytes(value: int | None) -> str:
@@ -157,6 +206,12 @@ def _bytes(value: int | None) -> str:
return f"{value} B"
+def _tokens(value: int | None) -> str:
+ if not value:
+ return "—"
+ return f"{value / 1000:.1f}k" if value >= 1000 else str(value)
+
+
def _bar(value: float, maximum: float, *, color: str = "var(--accent)") -> str:
frac = value / maximum if maximum > 0 else 0.0
fill = max(1.5, round(frac * 100, 1))
@@ -168,9 +223,6 @@ def _bar(value: float, maximum: float, *, color: str = "var(--accent)") -> str:
)
-_KNOWN_SURFACES = frozenset({"mcp", "cli", "memory"})
-
-
def _surface_badge(surface: str) -> str:
cls = f"surf-{surface}" if surface in _KNOWN_SURFACES else ""
return f'{_esc(surface)}'
@@ -179,7 +231,7 @@ def _surface_badge(surface: str) -> str:
def _reason_chip(reason_kind: str | None) -> str:
if not reason_kind:
return ""
- extra = " unknown" if reason_kind == "unknown" else ""
+ extra = " warn" if reason_kind == "unknown" else ""
return f''
@@ -193,10 +245,22 @@ def _counters(counters: Mapping[str, int]) -> str:
return f'{items}'
-def _rss(value: float | None) -> str:
- if value is None or value < 0.05:
- return ""
- return f''
+def _rss_text(value: float | None) -> str:
+ return "" if value is None or value < 0.05 else f"Δ{_mb(value)}"
+
+
+def _rss_badge(value: float | None) -> str:
+ text = _rss_text(value)
+ return f'' if text else ""
+
+
+def _payload(op: OperationView) -> str:
+ parts = []
+ if op.request_bytes is not None:
+ parts.append(f"↑{_bytes(op.request_bytes)}")
+ if op.response_bytes is not None:
+ parts.append(f"↓{_bytes(op.response_bytes)}")
+ return f'{" ".join(parts)}' if parts else ""
def _header(trace: TraceView) -> str:
@@ -222,114 +286,279 @@ def _stat(value: str, label: str, variant: str = "") -> str:
)
-def _stats(agg: AggregatesView) -> str:
- unknown_variant = "warn" if agg.unknown_expensive_rebuild_count else ""
- anomaly_variant = "warn" if agg.anomaly_count else ""
+def _section(title: str, body: str, *, subtitle: str = "") -> str:
+ hint = f'{_esc(subtitle)}
' if subtitle else ""
+ return f"{_esc(title)}
{hint}{body}"
+
+
+def _table(headers: tuple[tuple[str, bool], ...], rows: str) -> str:
+ ths = "".join(
+ f'{_esc(label)} | ' if right else f"{_esc(label)} | "
+ for label, right in headers
+ )
+ return (
+ f'"
+ )
+
+
+def _lead_row(label: str, value_html: str, metric: str) -> str:
return (
+ f'{_esc(label)}'
+ f'{value_html}'
+ f'{_esc(metric)}
'
+ )
+
+
+def _highlights(agg: AggregatesView) -> str:
+ rows: list[str] = []
+ if agg.slowest:
+ op = agg.slowest[0]
+ rows.append(
+ _lead_row(
+ "Slowest operation",
+ f"{_surface_badge(op.surface)}"
+ f'{_esc(op.name)}',
+ _ms(op.duration_ms),
+ )
+ )
+ if agg.slowest_span is not None:
+ span = agg.slowest_span
+ reason = _reason_chip(span.reason_kind)
+ rows.append(
+ _lead_row(
+ "Hottest span",
+ f"{_surface_badge(span.surface)}"
+ f'{_esc(span.name)}'
+ f'in {_esc(span.operation_name)}{reason}',
+ _ms(span.duration_ms),
+ )
+ )
+ if agg.max_rss_delta_mb is not None:
+ rows.append(
+ _lead_row(
+ "Peak memory Δ",
+ 'resident set growth',
+ _mb(agg.max_rss_delta_mb),
+ )
+ )
+ return f'{"".join(rows)}
' if rows else ""
+
+
+def _summary(trace: TraceView) -> str:
+ agg = trace.aggregates
+ costly = sum(
+ 1
+ for span in agg.semantic_costs
+ if span.no_op and span.duration_ms >= _NOOP_COSTLY_MS
+ )
+ unknown = agg.unknown_expensive_rebuild_count
+ cards = (
''
+ _stat(str(agg.operation_count), "operations", "accent")
+ _stat(_mb(agg.max_rss_delta_mb), "peak rss Δ")
- + _stat(
- str(agg.unknown_expensive_rebuild_count),
- "unknown heavy",
- unknown_variant,
- )
- + _stat(str(agg.anomaly_count), "anomalies", anomaly_variant)
+ + _stat(str(costly), "costly no-ops", "warn" if costly else "")
+ + _stat(str(unknown), "unknown reason", "warn" if unknown else "")
+ "
"
)
+ return _section("Runtime summary", cards + _highlights(agg))
+
+
+def _op_lineage(op: OperationView) -> list[OperationView]:
+ flat = [op]
+ for child in op.children:
+ flat.extend(_op_lineage(child))
+ return flat
-def _slowest(agg: AggregatesView) -> str:
- if not agg.slowest:
+def _breadcrumb(lineage: list[OperationView]) -> str:
+ if len(lineage) < 2:
return ""
- top = agg.slowest[0].duration_ms or 1.0
- rows = "".join(
- f'{_surface_badge(op.surface)}'
- f'
{_esc(op.name)}{_bar(op.duration_ms, top)}'
+ nodes = '
→ '.join(
+ f'
{_surface_badge(op.surface)}'
+ f'{_esc(op.name)}'
+ for op in lineage
+ )
+ return f'
{nodes}
'
+
+
+def _op_row(op: OperationView, group_max: float) -> str:
+ return (
+ '
'
+ f'{_surface_badge(op.surface)}{_esc(op.name)}'
+ f"{_bar(op.duration_ms, group_max)}"
f'{_ms(op.duration_ms)}'
- f'
'
- for op in agg.slowest
+ f'
{_rss_badge(op.rss_delta_mb)}{_payload(op)} '
)
+
+
+def _span_row(span: SpanView, op_duration: float) -> str:
+ color = "var(--warn)" if span.reason_kind == "unknown" else "var(--accent)"
+ meta = f"{_reason_chip(span.reason_kind)}{_rss_badge(span.rss_delta_mb)}"
return (
- f''
+ ''
+ f'▸'
+ f'{_esc(span.name)}'
+ f"{_bar(span.duration_ms, op_duration, color=color)}"
+ f'{_ms(span.duration_ms)}'
+ f'{meta}{_counters(span.counters)}
'
)
-def _rss_value(value: float | None) -> str:
- return "" if value is None or value < 0.05 else f"Δ{value:.1f} MB"
+def _op_block(op: OperationView, group_max: float) -> str:
+ op_duration = op.duration_ms or 1.0
+ spans = "".join(_span_row(span, op_duration) for span in op.spans)
+ spans_block = f'{spans}
' if spans else ""
+ kids = "".join(_op_block(child, group_max) for child in op.children)
+ kids_block = f'{kids}
' if kids else ""
+ return (
+ f'{_op_row(op, group_max)}{spans_block}
{kids_block}'
+ )
-def _mcp(tools: tuple[McpToolAggregate, ...]) -> str:
- if not tools:
- return ""
- rows = "".join(
- f'| {_esc(tool.name)} | {tool.count} | '
- f'{_ms(tool.p50_duration_ms)} | '
- f'{_ms(tool.p95_duration_ms)} | '
- f'{_bytes(tool.p95_response_bytes)} |
'
- for tool in tools
+def _chain_group(root: OperationView) -> str:
+ lineage = _op_lineage(root)
+ group_max = max((op.duration_ms for op in lineage), default=1.0) or 1.0
+ return (
+ f'{_breadcrumb(lineage)}{_op_block(root, group_max)}
'
+ )
+
+
+def _chain(trace: TraceView) -> str:
+ if not trace.operation_tree:
+ body = (
+ ''
+ "No operations recorded yet.
"
+ )
+ return _section("Correlated event chains", body)
+ groups = "".join(_chain_group(op) for op in trace.operation_tree)
+ return _section(
+ "Correlated event chains",
+ f'{groups}
',
+ subtitle="What triggered what, across processes — finish → spawned worker.",
+ )
+
+
+def _semantic_row(span: SpanCostView) -> str:
+ costly = span.no_op and span.duration_ms >= _NOOP_COSTLY_MS
+ if costly:
+ verdict = 'no-op · costly'
+ elif span.no_op:
+ verdict = 'no-op'
+ else:
+ verdict = 'productive'
+ reason = (
+ _esc(span.reason_kind) if span.reason_kind else '—'
)
return (
- 'MCP tool payloads
'
- '| Tool | Calls | '
- 'p50 | p95 | '
- 'p95 response |
'
- f"{rows}
"
+ f''
+ f'| {_esc(span.name)} | '
+ f'{_esc(span.operation_name)} | '
+ f"{reason} | "
+ f'{span.produced} | '
+ f'{span.skipped} | '
+ f'{_ms(span.duration_ms)} | '
+ f'{_mb(span.rss_delta_mb)} | '
+ f"{verdict} |
"
)
-def _payload(op: OperationView) -> str:
- parts = []
- if op.request_bytes is not None:
- parts.append(f"↑{_bytes(op.request_bytes)}")
- if op.response_bytes is not None:
- parts.append(f"↓{_bytes(op.response_bytes)}")
- return f'{" ".join(parts)}' if parts else ""
+def _semantic(agg: AggregatesView) -> str:
+ if not agg.semantic_costs:
+ return ""
+ rows = "".join(_semantic_row(span) for span in agg.semantic_costs)
+ headers = (
+ ("Span", False),
+ ("Operation", False),
+ ("Reason", False),
+ ("Produced", True),
+ ("Skipped", True),
+ ("Duration", True),
+ ("RSS Δ", True),
+ ("Verdict", False),
+ )
+ return _section(
+ "Memory pipeline cost",
+ _table(headers, rows),
+ subtitle="Reindex and rebuild spans — flags work that ran but "
+ "produced nothing.",
+ )
-def _span_row(span: SpanView, op_duration: float) -> str:
- color = "var(--warn)" if span.reason_kind == "unknown" else "var(--cli)"
- meta = _reason_chip(span.reason_kind) + _rss(span.rss_delta_mb)
+def _mcp_row(tool: McpToolAggregate) -> str:
return (
- f'{_esc(span.name)}'
- f"{_bar(span.duration_ms, op_duration, color=color)}"
- f'{_ms(span.duration_ms)}'
- f'{meta}'
- f"{_counters(span.counters)}
"
+ f'| {_esc(tool.name)} | '
+ f'{tool.count} | '
+ f'{_ms(tool.p50_duration_ms)} | '
+ f'{_ms(tool.p95_duration_ms)} | '
+ f'{_bytes(tool.p95_request_bytes)} | '
+ f'{_bytes(tool.p95_response_bytes)} | '
+ f'{_tokens(tool.p95_response_tokens)} |
'
)
-def _op_card(op: OperationView) -> str:
- op_duration = op.duration_ms or 1.0
- head = (
- f'{_surface_badge(op.surface)}'
- f'{_esc(op.name)}'
- f'{_ms(op.duration_ms)}'
- f"{_rss(op.rss_delta_mb)}{_payload(op)}
"
+def _mcp(tools: tuple[McpToolAggregate, ...]) -> str:
+ if not tools:
+ return ""
+ rows = "".join(_mcp_row(tool) for tool in tools)
+ headers = (
+ ("Tool", False),
+ ("Calls", True),
+ ("p50", True),
+ ("p95", True),
+ ("↑ req p95", True),
+ ("↓ resp p95", True),
+ ("resp tok p95", True),
)
- spans = (
- f'{"".join(_span_row(s, op_duration) for s in op.spans)}'
- "
"
- if op.spans
- else ""
+ return _section(
+ "MCP tool matrix",
+ _table(headers, rows),
+ subtitle="Per-tool latency and payload — spot tools that flood request "
+ "or response bytes.",
)
- children = "".join(_op_card(child) for child in op.children)
- return f'{head}{spans}{children}
'
-def _tree(trace: TraceView) -> str:
- if not trace.operation_tree:
- return (
- 'Trace
'
- '
No operations recorded yet.
'
- "
"
- )
- cards = "".join(_op_card(op) for op in trace.operation_tree)
- return f''
+def _wf_bar(row: WaterfallRow, total_ms: float) -> str:
+ span = total_ms if total_ms > 0 else 1.0
+ left = round(min(row.offset_ms / span * 100, 99.0), 2)
+ width = max(0.6, round(row.duration_ms / span * 100, 2))
+ kind = "op" if row.kind == "operation" else "span"
+ surf = f"surf-{row.surface}" if row.surface in _KNOWN_SURFACES else ""
+ tick = '▸' if kind == "span" else ""
+ return (
+ ''
+ f'
'
+ f"{tick}{_esc(row.label)}"
+ f'
'
+ f'
{_ms(row.duration_ms)} '
+ )
+
+
+def _wf_group(group: WaterfallGroup) -> str:
+ rows = "".join(_wf_bar(row, group.duration_ms) for row in group.rows)
+ cid = group.correlation_id[:8] if group.correlation_id else "—"
+ return (
+ f'{_esc(cid)}'
+ f"{_esc(group.started_at_utc)}"
+ f"span {_ms(group.duration_ms)}
{rows}
"
+ )
+
+
+def _waterfall(trace: TraceView) -> str:
+ if not trace.waterfall:
+ return ""
+ groups = "".join(_wf_group(group) for group in trace.waterfall)
+ return _section(
+ "Timeline",
+ f'{groups}
',
+ subtitle="Each causal chain on its own time axis — bars placed by start "
+ "offset, width by duration; a gap before a worker bar is the spawn handoff.",
+ )
def render_trace_html(trace: TraceView) -> str:
- """Render a ``TraceView`` as a self-contained, branded HTML document."""
+ """Render a ``TraceView`` as a self-contained, branded diagnosis cockpit."""
foot = f"CodeClone · platform observability · schema {_esc(trace.schema_version)}"
return (
''
@@ -337,10 +566,11 @@ def render_trace_html(trace: TraceView) -> str:
"CodeClone · Platform Observability"
f''
+ _header(trace)
- + _stats(trace.aggregates)
- + _slowest(trace.aggregates)
+ + _summary(trace)
+ + _waterfall(trace)
+ + _chain(trace)
+ + _semantic(trace.aggregates)
+ _mcp(trace.aggregates.mcp_tools)
- + _tree(trace)
+ f''
+ "
"
)
diff --git a/tests/test_observability_render.py b/tests/test_observability_render.py
index eaea4f7d..93aaa0ba 100644
--- a/tests/test_observability_render.py
+++ b/tests/test_observability_render.py
@@ -23,8 +23,11 @@
AggregatesView,
McpToolAggregate,
OperationView,
+ SpanCostView,
SpanView,
TraceView,
+ WaterfallGroup,
+ WaterfallRow,
)
from codeclone.surfaces.cli.observability import observability_main
@@ -89,6 +92,140 @@ def test_render_trace_html_is_branded() -> None:
assert "finish_controlled_change" in html
+def _cockpit_trace() -> TraceView:
+ reindex = SpanView(
+ span_id="sx",
+ name="memory.semantic.reindex",
+ duration_ms=850.0,
+ status="ok",
+ reason_kind="content_changed",
+ counters={"embedded": 0, "skipped_unchanged": 1423},
+ )
+ worker = OperationView(
+ operation_id="W",
+ correlation_id="A",
+ surface="memory",
+ name="memory.projection.job",
+ started_at_utc="2026-06-10T04:00:01Z",
+ duration_ms=900.0,
+ status="ok",
+ parent_operation_id="A",
+ rss_delta_mb=512.0,
+ spans=(reindex,),
+ )
+ finish = OperationView(
+ operation_id="A",
+ correlation_id="A",
+ surface="mcp",
+ name="finish_controlled_change",
+ started_at_utc="2026-06-10T04:00:00Z",
+ duration_ms=120.0,
+ status="ok",
+ request_bytes=51,
+ response_bytes=1873,
+ children=(worker,),
+ )
+ costly = SpanCostView(
+ span_id="sx",
+ name="memory.semantic.reindex",
+ surface="memory",
+ operation_id="W",
+ operation_name="memory.projection.job",
+ duration_ms=850.0,
+ reason_kind="content_changed",
+ produced=0,
+ skipped=1423,
+ no_op=True,
+ )
+ agg = AggregatesView(
+ operation_count=2,
+ slowest=(worker, finish),
+ max_rss_delta_mb=512.0,
+ mcp_tools=(
+ McpToolAggregate(
+ "finish_controlled_change",
+ 3,
+ 80.0,
+ 120.0,
+ 1873,
+ p95_request_bytes=51,
+ p95_response_tokens=469,
+ ),
+ ),
+ slowest_span=costly,
+ semantic_costs=(costly,),
+ )
+ return TraceView(
+ schema_version="1.0",
+ window_started_at_utc="2026-06-10T04:00:00Z",
+ window_ended_at_utc="2026-06-10T04:00:02Z",
+ aggregates=agg,
+ operation_tree=(finish,),
+ correlated_operations=(finish, worker),
+ )
+
+
+def test_render_cockpit_sections() -> None:
+ html = render_trace_html(_cockpit_trace())
+ # Section trajectory: summary -> chain -> memory cost -> MCP matrix.
+ assert "Runtime summary" in html
+ assert "Correlated event chains" in html
+ assert "Memory pipeline cost" in html
+ assert "MCP tool matrix" in html
+ # Cross-process correlation: a breadcrumb chains finish -> worker, and the
+ # worker nests under it via the indent rail (not a card inside a card).
+ assert "finish_controlled_change" in html
+ assert "memory.projection.job" in html
+ assert "→" in html
+ assert 'class="kids"' in html
+ # The reindex ran but embedded nothing -> flagged as a costly no-op.
+ assert "no-op" in html
+ assert "Hottest span" in html
+ # MCP matrix carries request bytes and response tokens, not just response bytes.
+ assert "51 B" in html
+ assert "469" in html
+
+
+def test_render_waterfall_timeline() -> None:
+ group = WaterfallGroup(
+ correlation_id="corr1234abcd",
+ started_at_utc="2026-06-10T04:00:00Z",
+ duration_ms=1000.0,
+ rows=(
+ WaterfallRow(
+ label="finish_controlled_change",
+ surface="mcp",
+ kind="operation",
+ depth=0,
+ offset_ms=0.0,
+ duration_ms=120.0,
+ ),
+ WaterfallRow(
+ label="memory.projection.job",
+ surface="memory",
+ kind="operation",
+ depth=1,
+ offset_ms=300.0,
+ duration_ms=700.0,
+ ),
+ ),
+ )
+ trace = TraceView(
+ schema_version="1.0",
+ window_started_at_utc="2026-06-10T04:00:00Z",
+ window_ended_at_utc="2026-06-10T04:00:01Z",
+ aggregates=AggregatesView(operation_count=2),
+ waterfall=(group,),
+ )
+ html = render_trace_html(trace)
+ assert "Timeline" in html
+ assert "wf-bar" in html
+ # The worker bar is offset 300/1000 = 30% and 700/1000 = 70% wide.
+ assert "left:30.0%" in html
+ assert "width:70.0%" in html
+ assert "memory.projection.job" in html
+
+
def test_render_trace_html_escapes_user_text() -> None:
span = SpanView(
span_id="s", name="", duration_ms=1.0, status="ok"
From 0e796dcbc4cc82e5add2e75584420e9a7aa1dbe6 Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Wed, 10 Jun 2026 23:36:42 +0500
Subject: [PATCH 227/318] feat(core): record memory.projection.spawn op for the
worker chain
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add operation B (memory.projection.spawn) in
execute_enqueue_projection_rebuild, wrapping the spawn decision. It inherits
the active finish op (A) as parent + correlation via
current_operation_context(), so the env handoff parents the worker (C)
under B — completing the cross-process A->B->C tree. Inert when
observability is disabled.
---
codeclone/memory/jobs/workflow.py | 21 ++++++++-
tests/test_projection_spawn_guard.py | 67 ++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 2 deletions(-)
diff --git a/codeclone/memory/jobs/workflow.py b/codeclone/memory/jobs/workflow.py
index d437ea8a..497eb512 100644
--- a/codeclone/memory/jobs/workflow.py
+++ b/codeclone/memory/jobs/workflow.py
@@ -12,7 +12,13 @@
from ...config.memory import MemoryConfig, resolve_memory_config
from ...config.observability import resolve_observability_config
-from ...observability import bootstrap, is_observability_enabled, shutdown
+from ...observability import (
+ bootstrap,
+ current_operation_context,
+ is_observability_enabled,
+ operation,
+ shutdown,
+)
from ...utils.ci import is_ci_environment
from ..exceptions import MemoryContractError
from ..models import MemoryProject
@@ -166,7 +172,18 @@ def execute_enqueue_projection_rebuild(
# redundant overlapping process.
spawn_skipped_reason = "worker_already_running"
else:
- spawn_result = spawn_projection_jobs_worker(root_path=resolved_root)
+ # Op B of the finish->spawn->worker chain (spec §4.3). The spawn
+ # decision becomes the active operation, inheriting the finish op (A)
+ # as parent + correlation, so the env handoff in spawn.py parents the
+ # worker (C) under B. Inert when observability is disabled.
+ parent = current_operation_context()
+ with operation(
+ name="memory.projection.spawn",
+ surface="memory",
+ parent_operation_id=parent[0] if parent else None,
+ correlation_id=parent[1] if parent else None,
+ ):
+ spawn_result = spawn_projection_jobs_worker(root_path=resolved_root)
spawned = spawn_result.spawned
worker_pid = spawn_result.pid
return {
diff --git a/tests/test_projection_spawn_guard.py b/tests/test_projection_spawn_guard.py
index bee47d93..68eb4720 100644
--- a/tests/test_projection_spawn_guard.py
+++ b/tests/test_projection_spawn_guard.py
@@ -12,8 +12,10 @@
import pytest
from codeclone.config.memory import MemoryConfig, resolve_memory_config
+from codeclone.config.observability import ObservabilityConfig
from codeclone.memory.jobs import compute_projection_stimulus
from codeclone.memory.jobs import workflow as jobs_workflow
+from codeclone.memory.jobs.spawn import SpawnWorkerResult
from codeclone.memory.jobs.store import (
enqueue_projection_job,
has_live_running_job,
@@ -23,6 +25,16 @@
from codeclone.memory.models import MemoryProject
from codeclone.memory.project import resolve_memory_db_path
from codeclone.memory.schema import open_memory_db
+from codeclone.observability import (
+ bootstrap,
+ current_operation_context,
+ operation,
+ shutdown,
+)
+from codeclone.observability.store.schema import (
+ observability_store_path,
+ open_observability_store,
+)
from codeclone.report.meta import current_report_timestamp_utc
from .memory_fixtures import cli_memory_repo
@@ -151,3 +163,58 @@ def test_enqueue_skips_spawn_when_worker_running(
assert payload["spawned"] is False
assert payload["spawn_skipped_reason"] == "worker_already_running"
assert payload["status"] == "enqueued"
+
+
+def test_enqueue_records_spawn_op_b_under_finish(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setattr(jobs_workflow, "is_ci_environment", lambda: False)
+ captured: dict[str, tuple[str, str] | None] = {}
+
+ def _fake_spawn(*, root_path: Path) -> SpawnWorkerResult:
+ # The spawn handoff reads the active operation here; under op B it must
+ # see B (not the finish op A), so the worker links parent=B.
+ captured["ctx"] = current_operation_context()
+ return SpawnWorkerResult(spawned=True, reason=None, pid=4242)
+
+ monkeypatch.setattr(jobs_workflow, "spawn_projection_jobs_worker", _fake_spawn)
+ with cli_memory_repo(tmp_path, with_draft=False) as (root, _project, _store):
+ config = resolve_memory_config(root)
+ bootstrap(ObservabilityConfig(enabled=True), root=root)
+ try:
+ with operation(
+ name="finish_controlled_change",
+ surface="mcp",
+ correlation_id="A-corr",
+ ) as finish_op:
+ finish_op_id = finish_op.operation_id
+ payload = execute_enqueue_projection_rebuild(
+ root_path=root,
+ config=config,
+ trigger="mcp_finish",
+ force=True,
+ spawn_worker=True,
+ )
+ finally:
+ shutdown()
+
+ assert payload["spawned"] is True
+ ctx = captured["ctx"]
+ assert ctx is not None
+ spawn_op_id, spawn_corr = ctx
+ assert spawn_corr == "A-corr" # B inherits A's correlation
+ assert spawn_op_id != finish_op_id # B is its own operation, not A
+
+ obs = open_observability_store(observability_store_path(root))
+ try:
+ row = obs.execute(
+ "SELECT operation_id, parent_operation_id, correlation_id "
+ "FROM platform_operations WHERE name='memory.projection.spawn'"
+ ).fetchone()
+ finally:
+ obs.close()
+ # Op B persisted, parented to the finish op (A) with A's correlation.
+ assert row is not None
+ assert row[0] == spawn_op_id
+ assert row[1] == finish_op_id
+ assert row[2] == "A-corr"
From 14d087d05a97ece1a001dcc5e6c8c640e6483202 Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Wed, 10 Jun 2026 23:36:55 +0500
Subject: [PATCH 228/318] feat(mcp): add stage spans to analyze_repository
Wrap the bootstrap/discover/process/analyze phases of the MCP
analyze_repository handler in pipeline.* spans so mcp.analyze_repository
carries the same stage timing as cli.analyze. This path calls the stages
directly and bypasses run_analysis_stages, so the spans are inline; they
attach to the active operation opened by the server registrar and are inert
when observability is disabled.
---
codeclone/surfaces/mcp/session.py | 37 ++++++++++++++++---------
tests/test_observability_correlation.py | 31 +++++++++++++++++++++
2 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/codeclone/surfaces/mcp/session.py b/codeclone/surfaces/mcp/session.py
index 5e7cd059..2dd2528d 100644
--- a/codeclone/surfaces/mcp/session.py
+++ b/codeclone/surfaces/mcp/session.py
@@ -15,6 +15,7 @@
from ...audit.runtime import open_audit_writer_for_root
from ...cache.store import resolve_cache_status
from ...memory.ide_governance import IdeGovernanceSessionState
+from ...observability import span
from ...report.meta import build_report_meta as _build_report_meta
from ...report.meta import current_report_timestamp_utc as _current_report_timestamp_utc
from . import _session_helpers as _helpers
@@ -251,19 +252,29 @@ def analyze_repository(self, request: MCPAnalysisRequest) -> dict[str, object]:
)
console = _BufferConsole()
- boot = bootstrap(
- args=args,
- root=root_path,
- output_paths=OutputPaths(json=_REPORT_DUMMY_PATH),
- cache_path=cache_path,
- )
- discovery_result = discover(boot=boot, cache=cache)
- processing_result = process(boot=boot, discovery=discovery_result, cache=cache)
- analysis_result = analyze(
- boot=boot,
- discovery=discovery_result,
- processing=processing_result,
- )
+ # Stage spans so mcp.analyze_repository carries the same discover/process/
+ # analyze timing as cli.analyze (this path bypasses run_analysis_stages,
+ # spec §6.1). Spans attach to the active operation from the MCP registrar;
+ # inert when observability is disabled or no operation is open.
+ with span(name="pipeline.bootstrap"):
+ boot = bootstrap(
+ args=args,
+ root=root_path,
+ output_paths=OutputPaths(json=_REPORT_DUMMY_PATH),
+ cache_path=cache_path,
+ )
+ with span(name="pipeline.discover"):
+ discovery_result = discover(boot=boot, cache=cache)
+ with span(name="pipeline.process"):
+ processing_result = process(
+ boot=boot, discovery=discovery_result, cache=cache
+ )
+ with span(name="pipeline.analyze"):
+ analysis_result = analyze(
+ boot=boot,
+ discovery=discovery_result,
+ processing=processing_result,
+ )
clone_baseline_state = resolve_clone_baseline_state(
baseline_path=baseline_path,
diff --git a/tests/test_observability_correlation.py b/tests/test_observability_correlation.py
index fcba7e0a..ab202106 100644
--- a/tests/test_observability_correlation.py
+++ b/tests/test_observability_correlation.py
@@ -118,3 +118,34 @@ def _fake_popen(argv: object, **kwargs: object) -> object:
# Observability disabled -> no active operation -> inherit parent env.
spawn.spawn_projection_jobs_worker(root_path=tmp_path)
assert captured["env"] is None
+
+
+def test_mcp_analyze_repository_emits_pipeline_spans(tmp_path: Path) -> None:
+ from codeclone.surfaces.mcp.service import CodeCloneMCPService
+ from codeclone.surfaces.mcp.session import MCPAnalysisRequest
+
+ (tmp_path / "module.py").write_text(
+ "def add(a, b):\n return a + b\n", encoding="utf-8"
+ )
+ service = CodeCloneMCPService(history_limit=4)
+ bootstrap(ObservabilityConfig(enabled=True), root=tmp_path)
+ try:
+ # The registrar opens this op around each MCP tool; emulate it so the
+ # session's stage spans have an operation to attach to.
+ with operation(name="mcp.analyze_repository", surface="mcp") as op:
+ op_id = op.operation_id
+ service.analyze_repository(
+ MCPAnalysisRequest(root=str(tmp_path), respect_pyproject=False)
+ )
+ finally:
+ shutdown()
+
+ conn = open_observability_store(observability_store_path(tmp_path))
+ try:
+ rows = conn.execute(
+ "SELECT name FROM platform_spans WHERE operation_id=?", (op_id,)
+ ).fetchall()
+ finally:
+ conn.close()
+ names = {row[0] for row in rows}
+ assert {"pipeline.discover", "pipeline.process", "pipeline.analyze"} <= names
From cae7ac34099a01e47102ccda62c73a0162e131f3 Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Wed, 10 Jun 2026 23:39:59 +0500
Subject: [PATCH 229/318] chore(deps): bump transitive lockfile pins
uv.lock re-resolution only: cryptography 44.0->45.0, filelock
3.29.1->3.29.3, readme-renderer 48.0.0->48.0.1. Transitive/dev pins; no
direct dependency or package-set change.
---
uv.lock | 114 ++++++++++++++++++++++++++++----------------------------
1 file changed, 57 insertions(+), 57 deletions(-)
diff --git a/uv.lock b/uv.lock
index 9e0f2db9..f3e78ea8 100644
--- a/uv.lock
+++ b/uv.lock
@@ -548,62 +548,62 @@ toml = [
[[package]]
name = "cryptography"
-version = "48.0.0"
+version = "48.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
- { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
- { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
- { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
- { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
- { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
- { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
- { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
- { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
- { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
- { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
- { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
- { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
- { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
- { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
- { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
- { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
- { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
- { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
- { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
- { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
- { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
- { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
- { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
- { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
- { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
- { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
- { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
- { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
- { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
- { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
- { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
- { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
- { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
- { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
- { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
- { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
- { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" },
- { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" },
- { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" },
- { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" },
- { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" },
- { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
+ { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
+ { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
+ { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
+ { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
+ { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
+ { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
+ { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
]
[[package]]
@@ -682,11 +682,11 @@ wheels = [
[[package]]
name = "filelock"
-version = "3.29.1"
+version = "3.29.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/f5/3557bf28e0f1943e4849154c821533706e6dea010f96fb6aa0b6949037d1/filelock-3.29.3.tar.gz", hash = "sha256:7fc1b3f39cf172fd8203812043c57b8a65aef9969f38b6704f628b881f761a84", size = 61956, upload-time = "2026-06-10T17:37:11.832Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" },
+ { url = "https://files.pythonhosted.org/packages/81/8f/b61d427c4f49a8bdadc93f4e7e74df8a6df6f77ee6e26bf0df53d3925363/filelock-3.29.3-py3-none-any.whl", hash = "sha256:e58333029cc9b925f39aad59b1d8f0a1ad836af4e60d7217f4a4dba87461261d", size = 42324, upload-time = "2026-06-10T17:37:10.37Z" },
]
[[package]]
@@ -2407,16 +2407,16 @@ wheels = [
[[package]]
name = "readme-renderer"
-version = "44.0"
+version = "45.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "nh3" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" },
+ { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" },
]
[[package]]
From 66aa8abec1e02b34c2f07e2ba9eb3fbaac687f15 Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Wed, 10 Jun 2026 23:51:57 +0500
Subject: [PATCH 230/318] feat(html): name the top memory consumer in the
runtime summary
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Turn the Peak RSS metric into a conclusion: the reader records
AggregatesView.peak_memory_span (the span with the largest rss_delta) and
the cockpit highlight names it — "Top memory consumer: in —
· %" — so the peak points at who took the memory instead of leaving
the reader to do the math.
---
codeclone/observability/render_html.py | 16 ++++++++++++++-
codeclone/observability/store/reader.py | 5 +++++
codeclone/observability/views.py | 1 +
tests/test_observability_reader.py | 5 +++++
tests/test_observability_render.py | 27 +++++++++++++++++++++++++
5 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/codeclone/observability/render_html.py b/codeclone/observability/render_html.py
index 49370d87..36ce6d66 100644
--- a/codeclone/observability/render_html.py
+++ b/codeclone/observability/render_html.py
@@ -334,7 +334,21 @@ def _highlights(agg: AggregatesView) -> str:
_ms(span.duration_ms),
)
)
- if agg.max_rss_delta_mb is not None:
+ if agg.peak_memory_span is not None and agg.max_rss_delta_mb:
+ # Name who took the memory, not just how much — the metric becomes a
+ # conclusion ("X grew the RSS", with its share of the peak).
+ peak = agg.peak_memory_span
+ share = round((peak.rss_delta_mb or 0.0) / agg.max_rss_delta_mb * 100)
+ rows.append(
+ _lead_row(
+ "Top memory consumer",
+ f"{_surface_badge(peak.surface)}"
+ f'{_esc(peak.name)}'
+ f'in {_esc(peak.operation_name)}',
+ f"{_mb(peak.rss_delta_mb)} · {share}%",
+ )
+ )
+ elif agg.max_rss_delta_mb is not None:
rows.append(
_lead_row(
"Peak memory Δ",
diff --git a/codeclone/observability/store/reader.py b/codeclone/observability/store/reader.py
index 7accf597..7ba4e3d9 100644
--- a/codeclone/observability/store/reader.py
+++ b/codeclone/observability/store/reader.py
@@ -287,6 +287,10 @@ def _aggregates(
key=lambda s: (-s.duration_ms, s.operation_id, s.span_id),
)
semantic_costs = tuple(s for s in span_costs if s.surface == "memory")
+ memory_ranked = sorted(
+ (s for s in span_costs if s.rss_delta_mb is not None),
+ key=lambda s: (-(s.rss_delta_mb or 0.0), s.operation_id, s.span_id),
+ )
return AggregatesView(
operation_count=len(flat),
slowest=slowest,
@@ -297,6 +301,7 @@ def _aggregates(
mcp_tools=_mcp_tool_aggregates(flat),
slowest_span=span_costs[0] if span_costs else None,
semantic_costs=semantic_costs[:_SEMANTIC_COST_LIMIT],
+ peak_memory_span=memory_ranked[0] if memory_ranked else None,
)
diff --git a/codeclone/observability/views.py b/codeclone/observability/views.py
index 1f7daf9b..56b75226 100644
--- a/codeclone/observability/views.py
+++ b/codeclone/observability/views.py
@@ -96,6 +96,7 @@ class AggregatesView:
mcp_tools: tuple[McpToolAggregate, ...] = ()
slowest_span: SpanCostView | None = None
semantic_costs: tuple[SpanCostView, ...] = ()
+ peak_memory_span: SpanCostView | None = None
@dataclass(frozen=True, slots=True)
diff --git a/tests/test_observability_reader.py b/tests/test_observability_reader.py
index 259f20fd..6012d871 100644
--- a/tests/test_observability_reader.py
+++ b/tests/test_observability_reader.py
@@ -138,6 +138,11 @@ def test_build_trace_view_tree_and_aggregates(tmp_path: Path) -> None:
assert job_row.offset_ms == 1000.0
assert rows[("memory.semantic.reindex", "span")].depth == 2
+ # Top memory consumer: the reindex span carries the largest rss delta.
+ assert agg.peak_memory_span is not None
+ assert agg.peak_memory_span.name == "memory.semantic.reindex"
+ assert agg.peak_memory_span.rss_delta_mb == 6144.0
+
def test_build_trace_view_focus_by_operation_id(tmp_path: Path) -> None:
_seed(tmp_path)
diff --git a/tests/test_observability_render.py b/tests/test_observability_render.py
index 93aaa0ba..afb2d609 100644
--- a/tests/test_observability_render.py
+++ b/tests/test_observability_render.py
@@ -186,6 +186,33 @@ def test_render_cockpit_sections() -> None:
assert "469" in html
+def test_render_peak_memory_contributor() -> None:
+ consumer = SpanCostView(
+ span_id="s",
+ name="memory.semantic.reindex",
+ surface="memory",
+ operation_id="W",
+ operation_name="memory.projection.job",
+ duration_ms=1700.0,
+ rss_delta_mb=480.0,
+ )
+ trace = TraceView(
+ schema_version="1.0",
+ window_started_at_utc="t",
+ window_ended_at_utc="t",
+ aggregates=AggregatesView(
+ operation_count=1,
+ max_rss_delta_mb=600.0,
+ peak_memory_span=consumer,
+ ),
+ )
+ html = render_trace_html(trace)
+ # The peak-memory highlight names the consumer + its share, not a bare number.
+ assert "Top memory consumer" in html
+ assert "memory.semantic.reindex" in html
+ assert "80%" in html # 480 / 600 = 80%
+
+
def test_render_waterfall_timeline() -> None:
group = WaterfallGroup(
correlation_id="corr1234abcd",
From 06edcc47493e601ebb837e7de518759430c4218a Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Thu, 11 Jun 2026 12:20:39 +0500
Subject: [PATCH 231/318] feat(core): count SQL queries per observability span
(29.DB)
First slice of DB observability (performance-truth, not audit-truth):
observability.runtime.record_db_query is a sqlite set_trace_callback that
attributes each statement to the active span as a db_queries counter (plus
db_writes for insert/update/delete/replace). instrument_db_connection
registers it only when observability is enabled, so disabled processes pay
zero per-query trace overhead; it is hooked into open_memory_db. The counter
flows into the existing span counters, so "semantic.reindex is expensive"
can now be read as "expensive due to N SQL reads". Resolves the add_counter
forward-declared dead-code. Timing/rows/slow and the scattered raw-connect
sites are later slices.
---
codeclone/memory/schema.py | 8 ++++-
codeclone/observability/__init__.py | 4 +++
codeclone/observability/runtime.py | 41 ++++++++++++++++++---
tests/test_observability_correlation.py | 47 +++++++++++++++++++++++++
4 files changed, 95 insertions(+), 5 deletions(-)
diff --git a/codeclone/memory/schema.py b/codeclone/memory/schema.py
index 5963288a..8015d79f 100644
--- a/codeclone/memory/schema.py
+++ b/codeclone/memory/schema.py
@@ -233,12 +233,18 @@
def open_memory_db(path: Path) -> sqlite3.Connection:
# synchronous=FULL: every commit survives unclean process exit.
# Memory records are few, each governance-governed and valuable.
- return open_sqlite_db(
+ conn = open_sqlite_db(
path,
ensure_schema=ensure_schema,
foreign_keys=True,
synchronous="FULL",
)
+ # Performance telemetry only: count SQL per active observability span so the
+ # cockpit can attribute span cost to DB work. No-op when disabled.
+ from ..observability import instrument_db_connection
+
+ instrument_db_connection(conn)
+ return conn
def ensure_schema(conn: sqlite3.Connection) -> None:
diff --git a/codeclone/observability/__init__.py b/codeclone/observability/__init__.py
index 806f8daa..c1dab3ce 100644
--- a/codeclone/observability/__init__.py
+++ b/codeclone/observability/__init__.py
@@ -20,9 +20,11 @@
bind_root,
bootstrap,
current_operation_context,
+ instrument_db_connection,
is_observability_enabled,
operation,
payload_capture_enabled,
+ record_db_query,
shutdown,
span,
)
@@ -33,9 +35,11 @@
"bind_root",
"bootstrap",
"current_operation_context",
+ "instrument_db_connection",
"is_observability_enabled",
"operation",
"payload_capture_enabled",
+ "record_db_query",
"shutdown",
"span",
]
diff --git a/codeclone/observability/runtime.py b/codeclone/observability/runtime.py
index 0694a77d..cc384167 100644
--- a/codeclone/observability/runtime.py
+++ b/codeclone/observability/runtime.py
@@ -15,6 +15,7 @@
from __future__ import annotations
+import sqlite3
import time
import uuid
from collections.abc import Iterator
@@ -139,10 +140,9 @@ def __init__(
self._status = "ok"
self._counters: dict[str, int] = {}
- # set_counter is wired by the 29.10 worker instrumentation. add_counter and
- # set_reason_kind stay forward-declared until a caller needs them
- # (loop-accumulation counters; post-hoc reason classification, e.g. semantic).
- # codeclone: ignore[dead-code]
+ # set_counter is wired by the 29.10 worker instrumentation; add_counter by
+ # the 29.DB query-trace hook (record_db_query). set_reason_kind stays
+ # forward-declared until a caller needs post-hoc reason classification.
def add_counter(self, key: str, value: int = 1) -> None:
self._counters[key] = self._counters.get(key, 0) + value
@@ -407,15 +407,48 @@ def span(
)
+_DB_WRITE_KINDS = frozenset({"insert", "update", "delete", "replace"})
+
+
+def _classify_sql(sql: str) -> str:
+ stripped = sql.lstrip()
+ if not stripped:
+ return ""
+ return stripped.split(None, 1)[0].lower()
+
+
+def record_db_query(sql: str) -> None:
+ """Trace-callback sink: attribute one SQL statement to the active span as a
+ ``db_queries`` counter (plus ``db_writes`` for mutations). No-op outside a
+ span. Performance telemetry only — never audit or contract truth.
+ """
+ span_handle = _CURRENT_SPAN.get()
+ if span_handle is None:
+ return
+ span_handle.add_counter("db_queries", 1)
+ if _classify_sql(sql) in _DB_WRITE_KINDS:
+ span_handle.add_counter("db_writes", 1)
+
+
+def instrument_db_connection(conn: sqlite3.Connection) -> None:
+ """Attach the per-span DB-query counter to ``conn``. No-op (and no per-query
+ trace overhead) when observability is disabled for this process.
+ """
+ if _ENABLED:
+ conn.set_trace_callback(record_db_query)
+
+
__all__ = [
"OperationHandle",
"SpanHandle",
"bind_root",
"bootstrap",
"current_operation_context",
+ "instrument_db_connection",
"is_observability_enabled",
"operation",
"payload_capture_enabled",
+ "record_db_query",
"shutdown",
"span",
]
diff --git a/tests/test_observability_correlation.py b/tests/test_observability_correlation.py
index ab202106..7124bec7 100644
--- a/tests/test_observability_correlation.py
+++ b/tests/test_observability_correlation.py
@@ -6,6 +6,8 @@
from __future__ import annotations
+import json
+import sqlite3
import subprocess
from collections.abc import Iterator
from pathlib import Path
@@ -19,8 +21,10 @@
from codeclone.observability import (
bootstrap,
current_operation_context,
+ instrument_db_connection,
operation,
shutdown,
+ span,
)
from codeclone.observability.store.schema import (
observability_store_path,
@@ -149,3 +153,46 @@ def test_mcp_analyze_repository_emits_pipeline_spans(tmp_path: Path) -> None:
conn.close()
names = {row[0] for row in rows}
assert {"pipeline.discover", "pipeline.process", "pipeline.analyze"} <= names
+
+
+def test_db_query_counter_attaches_to_active_span(tmp_path: Path) -> None:
+ bootstrap(ObservabilityConfig(enabled=True), root=tmp_path)
+ try:
+ with (
+ operation(name="memory.projection.job", surface="memory"),
+ span(name="memory.semantic.reindex"),
+ ):
+ conn = sqlite3.connect(":memory:")
+ instrument_db_connection(conn)
+ conn.execute("CREATE TABLE t (a INTEGER)")
+ conn.execute("INSERT INTO t VALUES (1)")
+ conn.execute("INSERT INTO t VALUES (2)")
+ conn.execute("SELECT * FROM t").fetchall()
+ conn.close()
+ finally:
+ shutdown()
+
+ obs = open_observability_store(observability_store_path(tmp_path))
+ try:
+ row = obs.execute(
+ "SELECT counters_json FROM platform_spans "
+ "WHERE name='memory.semantic.reindex'"
+ ).fetchone()
+ finally:
+ obs.close()
+ counters = json.loads(row[0]) if row and row[0] else {}
+ # create + 2 inserts + select all land on the active span; only the inserts
+ # are writes.
+ assert counters.get("db_queries", 0) >= 4
+ assert counters.get("db_writes", 0) == 2
+
+
+def test_instrument_db_connection_is_inert_when_disabled() -> None:
+ # Disabled process: no trace callback, no counting, no error, zero overhead.
+ conn = sqlite3.connect(":memory:")
+ try:
+ instrument_db_connection(conn)
+ conn.execute("CREATE TABLE t (a)")
+ assert current_operation_context() is None
+ finally:
+ conn.close()
From 710e87cbf62913fb3be7032228bf615be0ce8d51 Mon Sep 17 00:00:00 2001
From: Den Rozhnovskiy
Date: Thu, 11 Jun 2026 12:29:58 +0500
Subject: [PATCH 232/318] feat(html): add DB cost table to the cockpit
Aggregate the span-level db_queries/db_writes counters into a DB COST
section (DbCostRow on AggregatesView.db_costs): per span class, the number
of spans, total queries, total writes, queries-per-call (an N+1 signal) and
the worst single instance. On real data this immediately shows
memory.semantic.reindex is SQL-read-bound (1306 queries, 0 writes), turning
"reindex is slow" into "reindex runs N queries". Op-level DB for spanless
MCP operations is a later collection slice.
---
codeclone/observability/render_html.py | 34 +++++++++++++++
codeclone/observability/store/reader.py | 24 +++++++++++
codeclone/observability/views.py | 18 ++++++++
tests/test_observability_reader.py | 56 +++++++++++++++++++++++++
tests/test_observability_render.py | 27 ++++++++++++
5 files changed, 159 insertions(+)
diff --git a/codeclone/observability/render_html.py b/codeclone/observability/render_html.py
index 36ce6d66..32f67f52 100644
--- a/codeclone/observability/render_html.py
+++ b/codeclone/observability/render_html.py
@@ -25,6 +25,7 @@
from .views import (
AggregatesView,
+ DbCostRow,
McpToolAggregate,
OperationView,
SpanCostView,
@@ -571,6 +572,38 @@ def _waterfall(trace: TraceView) -> str:
)
+def _db_row(row: DbCostRow) -> str:
+ per_call = round(row.total_queries / row.span_count) if row.span_count else 0
+ return (
+ f'| {_esc(row.span_name)} | '
+ f'{row.span_count} | '
+ f'{row.total_queries} | '
+ f'{row.total_writes} | '
+ f'{per_call} | '
+ f'{row.max_queries} |
'
+ )
+
+
+def _db_cost(agg: AggregatesView) -> str:
+ if not agg.db_costs:
+ return ""
+ rows = "".join(_db_row(row) for row in agg.db_costs)
+ headers = (
+ ("Span", False),
+ ("Spans", True),
+ ("Queries", True),
+ ("Writes", True),
+ ("Q / call", True),
+ ("Max", True),
+ )
+ return _section(
+ "DB cost",
+ _table(headers, rows),
+ subtitle="SQLite work per span (performance-truth) — a high Q/call is "
+ "N+1-shaped: many reads for little produced.",
+ )
+
+
def render_trace_html(trace: TraceView) -> str:
"""Render a ``TraceView`` as a self-contained, branded diagnosis cockpit."""
foot = f"CodeClone · platform observability · schema {_esc(trace.schema_version)}"
@@ -584,6 +617,7 @@ def render_trace_html(trace: TraceView) -> str:
+ _waterfall(trace)
+ _chain(trace)
+ _semantic(trace.aggregates)
+ + _db_cost(trace.aggregates)
+ _mcp(trace.aggregates.mcp_tools)
+ f''
+ "