Add async pipeline with progress monitoring, resumability, and result transparency

Pipeline engine rewritten with combo-first loop: each combination is processed
through all requested passes before moving to the next, with incremental DB
saves after every step (crash-safe). Blocked combos now get result rows so they
appear in the results page with constraint violation reasons.

New pipeline_runs table tracks run lifecycle (pending/running/completed/failed/
cancelled). Web route launches pipeline in a background thread with its own DB
connection. HTMX polling partial shows live progress with per-pass breakdown.

Also: status guard prevents reviewed->scored downgrade, save_combination loads
existing status on dedup for correct resume, per-metric scores show domain
bounds + units + position bars, ensure_metric backfills units on existing rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Simonson, Andrew
2026-02-18 15:30:52 -06:00
parent 8118a62242
commit d2028a642b
17 changed files with 1263 additions and 217 deletions

View File

@@ -55,19 +55,55 @@
{% if scores %}
<h2>Per-Metric Scores</h2>
{% set bounds = {} %}
{% for mb in domain.metric_bounds %}
{% set _ = bounds.update({mb.metric_name: mb}) %}
{% endfor %}
<div class="card">
<table>
<thead>
<tr><th>Metric</th><th>Raw Value</th><th>Normalized</th><th>Method</th><th>Confidence</th></tr>
<tr>
<th>Metric</th>
<th>Raw Value</th>
<th>Domain Range</th>
<th>Position</th>
<th>Normalized</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
{% for s in scores %}
{% set mb = bounds.get(s.metric_name) %}
<tr>
<td>{{ s.metric_name }}</td>
<td>{{ "%.2f"|format(s.raw_value) if s.raw_value is not none else '' }}</td>
{% set unit = s.metric_unit or '' %}
<td class="score-cell">{{ "%.2f"|format(s.raw_value) if s.raw_value is not none else '—' }}{{ ' ' + unit if unit and s.raw_value is not none else '' }}</td>
<td>
{%- if mb -%}
{{ "%.2f"|format(mb.norm_min) }} — {{ "%.2f"|format(mb.norm_max) }}{{ ' ' + unit if unit else '' }}
{%- else -%}
{%- endif -%}
</td>
<td>
{%- if mb and s.raw_value is not none -%}
{%- if s.raw_value <= mb.norm_min -%}
<span class="badge badge-blocked">at/below min</span>
{%- elif s.raw_value >= mb.norm_max -%}
<span class="badge badge-valid">at/above max</span>
{%- else -%}
{% set pct = ((s.raw_value - mb.norm_min) / (mb.norm_max - mb.norm_min) * 100) | int %}
<div class="metric-bar-container">
<div class="metric-bar" style="width: {{ pct }}%"></div>
</div>
<span class="metric-bar-label">~{{ pct }}%</span>
{%- endif -%}
{%- else -%}
{%- endif -%}
</td>
<td class="score-cell">{{ "%.4f"|format(s.normalized_score) if s.normalized_score is not none else '—' }}</td>
<td>{{ s.estimation_method or '—' }}</td>
<td>{{ "%.2f"|format(s.confidence) if s.confidence is not none else '—' }}</td>
<td>{{ "%.0f%%"|format(mb.weight * 100) if mb else '—' }}</td>
</tr>
{% endfor %}
</tbody>