Files
resolutionflow/docs/architecture/workflows-analysis.html
Michael Chihlas e5b26245ca
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m45s
CI / e2e (pull_request) Successful in 10m13s
CI / backend (pull_request) Successful in 11m27s
docs: add architecture reports, public-landing routing plan, build-a-page tutorial, self-serve signup phase-2 design
- docs/architecture/: god-node map + report (2026-05-06), workflows.json/html + analysis snapshot
- docs/plans/2026-05-13-public-landing-routing-refactor.md
- docs/tutorials/build-a-page.md
- abc-feat-self-serve-signup-phase-2-design-20260507-112020.md (root)

Core dumps (core.144926, core.145678, docs/architecture/core.1392564) and
agent .remember/ state are intentionally left untracked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:59:29 -04:00

524 lines
25 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ResolutionFlow — Workflow Analysis</title>
<style>
:root {
--bg-page: #0e1016;
--bg-card: #1e2028;
--bg-elev: #2a2d38;
--border: #2a2d38;
--border-hover: #3a3d48;
--text-heading: #f4f5f7;
--text-primary: #d6d8df;
--text-muted: #8b8e98;
--text-dim: #5a5d68;
--accent: #60a5fa;
--accent-dim: rgba(96, 165, 250, 0.15);
--warning: #fbbf24;
--warning-dim: rgba(251, 191, 36, 0.12);
--info: #67e8f9;
--success: #34d399;
--success-dim: rgba(52, 211, 153, 0.12);
--danger: #f87171;
--danger-dim: rgba(248, 113, 113, 0.12);
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--sans: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
--heading: "Bricolage Grotesque", "IBM Plex Sans", sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg-page);
color: var(--text-primary);
font-family: var(--sans);
font-size: 14px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 880px;
margin: 0 auto;
padding: 48px 32px 80px;
}
header.page {
border-bottom: 1px solid var(--border);
padding-bottom: 24px;
margin-bottom: 32px;
}
header.page .eyebrow {
font-family: var(--mono);
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 8px;
}
header.page h1 {
font-family: var(--heading);
font-weight: 700;
font-size: 32px;
line-height: 1.15;
color: var(--text-heading);
margin: 0 0 12px;
letter-spacing: -0.015em;
}
header.page .meta {
color: var(--text-muted);
font-size: 13px;
}
header.page .meta a { color: var(--accent); text-decoration: none; }
header.page .meta a:hover { text-decoration: underline; }
h2 {
font-family: var(--heading);
font-weight: 700;
font-size: 22px;
color: var(--text-heading);
margin: 48px 0 14px;
letter-spacing: -0.01em;
}
h3 {
font-family: var(--heading);
font-weight: 600;
font-size: 17px;
color: var(--text-heading);
margin: 28px 0 8px;
letter-spacing: -0.005em;
}
p { margin: 0 0 12px; }
ul { margin: 0 0 16px; padding-left: 22px; }
ul li { margin-bottom: 6px; }
a { color: var(--accent); }
code, kbd {
font-family: var(--mono);
font-size: 12px;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.04);
padding: 1px 6px;
border-radius: 3px;
color: var(--text-primary);
}
strong { color: var(--text-heading); font-weight: 600; }
em { color: var(--text-primary); font-style: italic; }
.tldr {
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 6px;
padding: 20px 24px;
margin-bottom: 36px;
}
.tldr h2 { margin: 0 0 8px; font-size: 14px; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); }
.tldr p { margin: 0 0 10px; font-size: 15px; line-height: 1.55; }
.tldr p:last-child { margin-bottom: 0; }
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin: 20px 0 8px;
}
.metric {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
padding: 14px 16px;
}
.metric .label {
font-family: var(--mono);
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.metric .value {
font-family: var(--heading);
font-weight: 700;
font-size: 26px;
color: var(--text-heading);
line-height: 1.1;
}
.metric .sub {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
table.data {
width: 100%;
border-collapse: collapse;
margin: 12px 0 20px;
font-size: 13px;
}
table.data th, table.data td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
table.data th {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 700;
background: var(--bg-card);
}
table.data td.num { font-family: var(--mono); text-align: right; }
table.data td.kind { font-family: var(--mono); font-size: 12px; }
table.data td .pill {
display: inline-block;
padding: 1px 7px;
border-radius: 3px;
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
td .pill.yes { background: var(--warning-dim); color: var(--warning); border: 1px solid rgba(251,191,36,0.3); }
td .pill.no { background: rgba(255,255,255,0.04); color: var(--text-muted); border: 1px solid var(--border); }
td .pill.maybe { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(96,165,250,0.3); }
/* Heatmap */
.heatmap {
margin: 16px 0;
border-collapse: collapse;
font-family: var(--mono);
font-size: 11px;
}
.heatmap th, .heatmap td {
border: 1px solid var(--border);
padding: 8px 6px;
text-align: center;
min-width: 44px;
color: var(--text-muted);
}
.heatmap th {
background: var(--bg-card);
color: var(--text-muted);
text-transform: lowercase;
font-weight: 600;
letter-spacing: 0.04em;
}
.heatmap td.label {
text-align: right;
background: var(--bg-card);
color: var(--text-muted);
font-weight: 600;
padding-right: 10px;
}
.heatmap td.empty { color: var(--text-dim); }
.heatmap td.diag { color: var(--text-dim); background: rgba(255,255,255,0.015); }
.heatmap td.h1 { color: var(--text-heading); background: rgba(96,165,250,0.1); }
.heatmap td.h2 { color: var(--text-heading); background: rgba(96,165,250,0.22); font-weight: 700; }
.heatmap td.h3 { color: var(--bg-page); background: var(--accent); font-weight: 700; }
.heatmap-caption { color: var(--text-muted); font-size: 12px; margin: 8px 0 16px; }
.concern {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
padding: 18px 22px;
margin: 14px 0;
}
.concern.warning { border-left: 3px solid var(--warning); }
.concern.info { border-left: 3px solid var(--accent); }
.concern.success { border-left: 3px solid var(--success); }
.concern h3 { margin-top: 0; display: flex; align-items: baseline; gap: 10px; }
.concern h3 .tag {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 2px 7px;
border-radius: 3px;
}
.concern.warning h3 .tag { background: var(--warning-dim); color: var(--warning); }
.concern.info h3 .tag { background: var(--accent-dim); color: var(--accent); }
.concern.success h3 .tag { background: var(--success-dim); color: var(--success); }
.concern p:last-child { margin-bottom: 0; }
.caveat {
background: rgba(255,255,255,0.025);
border-left: 2px solid var(--text-dim);
padding: 12px 18px;
margin: 16px 0;
font-size: 13px;
color: var(--text-muted);
}
.caveat strong { color: var(--text-primary); }
footer.page {
margin-top: 64px;
padding-top: 24px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-muted);
}
@media (max-width: 720px) {
.container { padding: 32px 20px; }
.metrics { grid-template-columns: repeat(2, 1fr); }
header.page h1 { font-size: 26px; }
.heatmap { font-size: 10px; }
.heatmap th, .heatmap td { padding: 6px 4px; min-width: 32px; }
}
</style>
</head>
<body>
<div class="container">
<header class="page">
<div class="eyebrow">Architecture review · 2026-05-13</div>
<h1>ResolutionFlow workflow analysis</h1>
<div class="meta">
Based on <a href="workflows.html">workflows.html</a> · 28 user-facing flows · 297 traced steps · 120 unique files
</div>
</header>
<div class="tldr">
<h2>Bottom line</h2>
<p>You're <strong>not bloated</strong>, and most of the "circles" in the diagram are <strong>visualization artifact, not architecture problems</strong>. Each HTTP call shows up as two steps (request + response), so a normal round-trip <em>looks</em> like a circle even though it's one unit of work.</p>
<p>Three real items worth engineering attention: <code>ai_sessions.py</code> is becoming a god endpoint, the three chat services have a confusing boundary, and the auth token tables have no physical cleanup so they accrue rows forever. Everything else looks structurally healthy.</p>
</div>
<h2>Headline numbers</h2>
<div class="metrics">
<div class="metric">
<div class="label">Avg steps / flow</div>
<div class="value">10.6</div>
<div class="sub">healthy range for multi-tenant SaaS</div>
</div>
<div class="metric">
<div class="label">Avg files / flow</div>
<div class="value">7.5</div>
<div class="sub">one file per layer, roughly</div>
</div>
<div class="metric">
<div class="label">Revisit ratio</div>
<div class="value">1.39</div>
<div class="sub">1.0 = flat; 2.0+ = chat-shaped</div>
</div>
<div class="metric">
<div class="label">"Backward" edges</div>
<div class="value">15%</div>
<div class="sub">mostly HTTP response, not real circles</div>
</div>
</div>
<h2>Why the diagrams look circular</h2>
<p>Each HTTP request and its response are encoded as <strong>two separate steps</strong>. So an API call architecturally goes <em>one direction</em>, but visually looks like a loop. Breakdown of the 44 backward-flowing edges:</p>
<table class="data">
<thead>
<tr><th>Kind</th><th class="num">Count</th><th>Real circle?</th><th>Example</th></tr>
</thead>
<tbody>
<tr>
<td class="kind">http_post / http_get response</td>
<td class="num">20</td>
<td><span class="pill no">artifact</span></td>
<td>Server returns 200 to client. Not a circle.</td>
</tr>
<tr>
<td class="kind">function_call return value</td>
<td class="num">8</td>
<td><span class="pill no">artifact</span></td>
<td><code>oauth_providers</code> returns an <code>OAuthProfile</code> to the endpoint that called it.</td>
</tr>
<tr>
<td class="kind">state_update (hook → component/page)</td>
<td class="num">8</td>
<td><span class="pill maybe">idiomatic</span></td>
<td>Hook returns updated state, page re-renders. Pure React data flow.</td>
</tr>
<tr>
<td class="kind">redirect (OAuth provider → app)</td>
<td class="num">4</td>
<td><span class="pill yes">real</span></td>
<td>Google/Microsoft sends user back to <code>/oauth/callback</code>. Architecturally required.</td>
</tr>
<tr>
<td class="kind">webhook</td>
<td class="num">1</td>
<td><span class="pill yes">real</span></td>
<td>Stripe POSTs to <code>/webhooks/stripe</code>. External system re-enters us.</td>
</tr>
<tr>
<td class="kind">navigation / external_api / other</td>
<td class="num">3</td>
<td><span class="pill yes">real</span></td>
<td>Page-to-page nav, Anthropic returning a response.</td>
</tr>
</tbody>
</table>
<p>After subtracting the request/response duality, the <em>real</em> backward edges are about <strong>3% of steps</strong>, and every one of them is in a place where the architecture demands it (React state propagation, OAuth callbacks, webhooks).</p>
<h2>What's healthy</h2>
<div class="concern success">
<h3>Clean layer discipline <span class="tag">good</span></h3>
<p>The system mostly respects layer boundaries. <code>endpoint → service</code> (34x), <code>service → external</code> (37x), <code>api_client → endpoint</code> (30x) dominate the traffic. Things flow in the expected direction.</p>
</div>
<div class="concern success">
<h3><code>flowpilot_engine</code> is the right kind of shared service <span class="tag">good</span></h3>
<p>Touched by 5 flows (start, respond, resolve, pause, abandon). That's a coordination kernel doing its job — high fan-in is correct for orchestration code.</p>
</div>
<div class="concern success">
<h3>PostgreSQL in 25/28 flows <span class="tag">good</span></h3>
<p>Star topology, not a tangle. That's what a database is supposed to look like.</p>
</div>
<h2>Layer transition heatmap</h2>
<p>How many times each layer-pair appears across all steps. Bright cells = well-traveled paths. Empty cells = layer boundaries that aren't crossed (mostly a good sign).</p>
<table class="heatmap">
<thead>
<tr>
<th></th>
<th>page</th><th>comp</th><th>hook</th><th>store</th><th>api_c</th><th>http</th><th>endp</th><th>serv</th><th>core</th><th>model</th><th>ext</th>
</tr>
</thead>
<tbody>
<tr><td class="label">page</td> <td class="diag">13</td><td class="h1">5</td> <td class="h1">6</td><td class="h1">12</td><td class="h2">17</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td>2</td></tr>
<tr><td class="label">comp</td> <td>1</td> <td class="diag">5</td><td>2</td><td class="empty">·</td><td>1</td><td class="empty">·</td><td>1</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td></tr>
<tr><td class="label">hook</td> <td class="h1">7</td><td>1</td> <td class="diag empty">·</td><td class="empty">·</td><td class="h2">11</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td></tr>
<tr><td class="label">store</td> <td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="diag">4</td><td>2</td><td class="empty">·</td><td>1</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td>1</td></tr>
<tr><td class="label">api_client</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="diag empty">·</td><td>5</td><td class="h3">30</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td>1</td></tr>
<tr><td class="label">endpoint</td> <td>3</td><td class="empty">·</td><td class="h1">9</td><td>2</td><td>4</td><td class="empty">·</td><td class="diag">1</td><td class="h3">34</td><td class="h1">8</td><td>2</td><td class="h3">29</td></tr>
<tr><td class="label">service</td> <td>1</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td>2</td><td class="empty">·</td><td>3</td><td class="diag h1">9</td><td>5</td><td>4</td><td class="h3">37</td></tr>
<tr><td class="label">core</td> <td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="diag empty">·</td><td class="empty">·</td><td>4</td></tr>
<tr><td class="label">model</td> <td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="diag empty">·</td><td>1</td></tr>
<tr><td class="label">external</td> <td>4</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td>1</td><td>1</td><td class="empty">·</td><td class="empty">·</td><td class="diag empty">·</td></tr>
<tr><td class="label">http_client</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="diag empty">·</td><td>5</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td><td class="empty">·</td></tr>
</tbody>
</table>
<p class="heatmap-caption">Read row → column. Diagonal = same-layer transitions. Above-diagonal = "backward" (e.g. <code>endpoint → hook</code> = HTTP response). The strong upper-right concentration (<code>endpoint → service → external</code>) is the right shape.</p>
<h2>Top coupling hot-spots</h2>
<p>Files appearing in the most flows. The first two (PostgreSQL, Anthropic) are expected; everything else is worth a glance.</p>
<table class="data">
<thead>
<tr><th class="num">Flows</th><th>File</th><th>Layer</th><th>Read</th></tr>
</thead>
<tbody>
<tr><td class="num">25</td><td><code>external:postgres</code></td><td>external</td><td>Expected. The DB is the hub.</td></tr>
<tr><td class="num">10</td><td><code>external:anthropic_api</code></td><td>external</td><td>Expected for an AI product.</td></tr>
<tr><td class="num"><strong>7</strong></td><td><code>backend/app/api/endpoints/ai_sessions.py</code></td><td>endpoint</td><td><strong>God endpoint candidate.</strong> See concern below.</td></tr>
<tr><td class="num">6</td><td><code>frontend/src/api/aiSessions.ts</code></td><td>api_client</td><td>Mirrors the god endpoint. Splits naturally if backend splits.</td></tr>
<tr><td class="num">5</td><td><code>backend/app/services/flowpilot_engine.py</code></td><td>service</td><td>Healthy coordination kernel.</td></tr>
<tr><td class="num">5</td><td><code>backend/app/api/endpoints/auth.py</code></td><td>endpoint</td><td>5 auth flows, 5 endpoints. Reasonable.</td></tr>
<tr><td class="num">5</td><td><code>frontend/src/store/authStore.ts</code></td><td>store</td><td>Centralized auth state. Correct.</td></tr>
<tr><td class="num">5</td><td><code>frontend/src/pages/FlowPilotSessionPage.tsx</code></td><td>page</td><td>Worth checking — see OAuth concern.</td></tr>
<tr><td class="num">5</td><td><code>frontend/src/hooks/useFlowPilotSession.ts</code></td><td>hook</td><td>Always co-travels with the page. Right pattern.</td></tr>
</tbody>
</table>
<h2>Things worth examining</h2>
<div class="concern warning">
<h3>1. <code>ai_sessions.py</code> is a god endpoint <span class="tag">split candidate</span></h3>
<p>Appears in 7 flows. Houses ~12 route handlers in one file: <code>create</code>, <code>respond</code>, <code>chat</code>, <code>resolve</code>, <code>escalate</code>, <code>pause</code>, <code>abandon</code>, <code>pickup</code>, <code>list</code>, <code>get</code>, plus the <code>/chat</code> + <code>/respond</code> overload. It's the highest-coupled non-DB node.</p>
<p>Suggested seam:</p>
<ul>
<li><code>session_lifecycle.py</code> — create, resolve, escalate, pause, abandon, pickup</li>
<li><code>session_messaging.py</code> — chat, respond</li>
</ul>
<p>Frontend <code>aiSessions.ts</code> would split along the same line. Net change: clearer ownership, no functional impact.</p>
</div>
<div class="concern warning">
<h3>2. Three chat services with a confusing boundary <span class="tag">name vs reality</span></h3>
<p>Three files exist with overlapping responsibilities:</p>
<ul>
<li><code>backend/app/services/unified_chat_service.py</code> — chat session handling, marker parsing</li>
<li><code>backend/app/services/assistant_chat_service.py</code><code>_call_ai</code> infrastructure (Anthropic with caching, MCP, vision)</li>
<li><code>backend/app/core/ai_chat_service.py</code> — flow-builder chat for editors (separate domain)</li>
</ul>
<p>The <code>PROJECT_CONTEXT.md</code> note says <code>assistant_chat_service</code> was "removed except for retention settings," but the trace shows <code>unified_chat_service.send_chat_message</code> still calls into it for <code>_call_ai</code>. So the file is load-bearing infrastructure, not retention scaffolding.</p>
<p>Two paths forward:</p>
<ul>
<li>Rename <code>assistant_chat_service.py</code><code>ai_call_utils.py</code> (or fold the <code>_call_ai</code> function into <code>core/ai_provider.py</code> where the provider abstraction already lives).</li>
<li>Update <code>PROJECT_CONTEXT.md</code> to match reality.</li>
</ul>
<p>Either way the confusing seam goes away.</p>
</div>
<div class="concern warning">
<h3>3. OAuth login is the most "circular" real flow <span class="tag">overloaded callback</span></h3>
<p>19 steps, 4 backward edges, 3 self-loops — by far the most complex auth flow. Some complexity is unavoidable (provider redirect = 2 boundary crossings). But 3 self-loops on <code>OAuthCallbackPage</code> suggest the page is doing too much local state shuffling: CSRF state validation, code exchange, invite-code stash retrieval, JWT storage, navigation, welcome-banner logic.</p>
<p>Worth a look: move OAuth state handling into either <code>authStore</code> (which would centralize all auth state in one place) or a <code>useOAuthCallback</code> hook. The page itself should be mostly declarative.</p>
</div>
<div class="concern warning">
<h3>4. Three auth-token tables grow without bound <span class="tag">add cleanup</span></h3>
<p>Auth writes to <code>refresh_tokens</code>, <code>password_reset_tokens</code>, <code>email_verification_tokens</code>, and <code>oauth_identities</code>. Each table is individually justified (different lifecycles, different lookup patterns, JTI rotation for refresh) — <strong>this is not bloat in the code</strong>. But the cleanup story is missing.</p>
<p>Verified directly: <code>retention_cleanup.py</code> only sweeps <code>AssistantChat</code>. <code>scheduler.py</code> only has one other cleanup job, for <code>AIConversation</code>. The auth endpoint code in <code>auth.py</code> <em>revokes</em> tokens (<code>UPDATE … SET revoked_at = now()</code>) but never <em>deletes</em> them. So:</p>
<ul>
<li><code>refresh_tokens</code> — revoked rows stay forever. One row per login + one per refresh rotation.</li>
<li><code>password_reset_tokens</code> — one row per forgot-password request, no cleanup at all.</li>
<li><code>email_verification_tokens</code> — one row per signup (and per re-send), no cleanup.</li>
<li><code>oauth_identities</code> — correctly persistent; this is a permanent FK from user to provider, not a cleanup target.</li>
</ul>
<p>Suggested fix: add a daily APScheduler job in <code>retention_cleanup.py</code> (or a sibling) that hard-deletes rows where <code>revoked_at &lt; now() - INTERVAL '30 days'</code> for <code>refresh_tokens</code>, and <code>expires_at &lt; now() - INTERVAL '7 days'</code> for the two single-use token tables. Pattern matches the existing <code>cleanup_expired_chats</code> shape and the <code>_cleanup_expired_ai_conversations</code> job in <code>scheduler.py</code>.</p>
<p class="heatmap-caption">Earlier draft of this concern pointed to <code>retention_cleanup.py</code> as the place to <em>verify</em> existing cleanup. That was wrong — no such cleanup exists. Corrected after direct check.</p>
</div>
<h2>Things <em>not</em> to worry about</h2>
<div class="concern success">
<h3>Hook ↔ page state loops in session flows</h3>
<p>That's just React. <code>useFlowPilotSession</code> and <code>FlowPilotSessionPage</code> always travel together because the hook <em>is</em> that page's controller — they're maximally coupled by design, which is the right pattern.</p>
</div>
<div class="concern success">
<h3>Low "work percentage" on simple flows</h3>
<p>"Pause &amp; leave" comes out at 11% real work, 89% plumbing. That's correct — pause is structurally just <code>PATCH status='paused'</code>. There's no work to do beyond plumbing. The metric undersells simple flows.</p>
</div>
<div class="concern success">
<h3>The 25-flow PostgreSQL hub</h3>
<p>Star topology, not a tangle. A database serving every flow is the architectural ideal.</p>
</div>
<h2>Caveats on this analysis</h2>
<div class="caveat">
<strong>Work vs plumbing heuristic undersells reality.</strong> It counts <code>http_post</code> as plumbing even when it carries the actual payload. Work percentages should be read as <em>roughly 2x</em> the displayed value.
</div>
<div class="caveat">
<strong>Only user-facing flows are traced.</strong> Background work (knowledge flywheel scheduler, retention cleanup, PSA retry scheduler, MCP turn routing) isn't in here — and that's exactly where bloat tends to hide because nobody watches it. A follow-up trace of the background jobs would close the loop.
</div>
<div class="caveat">
<strong>~6 of 297 steps marked <code>unverified</code></strong> (mostly knowledge-flywheel-created proposals). They're included in the totals but the conclusions don't depend on them.
</div>
<div class="caveat">
<strong>"Backward edge" includes HTTP responses.</strong> An HTTP round-trip looks like one forward step (request) plus one backward step (response). That alone accounts for the majority of the 15% backward share. The interesting backward edges are the ~3% that aren't request/response duality.
</div>
<footer class="page">
Generated from <code>workflows.json</code> · 28 user-facing flows · 297 steps · 120 files · ResolutionFlow 2026-05-13
</footer>
</div>
</body>
</html>