- 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>
524 lines
25 KiB
HTML
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 < now() - INTERVAL '30 days'</code> for <code>refresh_tokens</code>, and <code>expires_at < 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 & 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>
|