Add domain CRUD, energy density constraint, LLM status, reset results, score display fixes

Domain management:
- Add domain list/detail/form templates and full CRUD routes (domains.py)
- Add metric bound add/edit/delete via HTMX partials (_metrics_table.html)

Energy density constraint (Rule 6 in ConstraintResolver):
- Hard-block combos where power source provides <25% of platform's required Wh/kg
- Warn (conditional) when under-density but within 4x
- Solar Sail exempt (no stored energy); Airplane requires 400 Wh/kg, Spaceship 2000 Wh/kg
- Add energy_density_wh_kg provides to all 8 stored-energy power sources in seed data
- 3 new constraint resolver tests

LLM-complete status:
- Pipeline Pass 4 now sets combo status to llm_reviewed after successful LLM review
- update_combination_status guards against downgrading: scored won't overwrite
  llm_reviewed or reviewed; llm_reviewed won't overwrite reviewed
- Add badge-llm_reviewed CSS style (light blue)

Reset results:
- Repository.reset_domain_results() deletes combination_results, combination_scores,
  and pipeline_runs for a domain; pipeline re-evaluates on next run
- POST /results/<domain>/reset route with flash confirmation
- "Reset results" danger button with JS confirm dialog in results list

Fix composite score 0 displaying as --- (Jinja2 falsy 0.0 bug):
- Change `if r.composite_score` to `if r.composite_score is not none`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 11:13:00 -06:00
parent ee885b2390
commit 8dfe3607b1
20 changed files with 675 additions and 67 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
@@ -293,22 +294,36 @@ class Pipeline:
for s in db_scores
if s["normalized_score"] is not None
}
review = self.llm.review_plausibility(
description, score_dict
)
self.repo.save_result(
combo.id,
domain.id,
cur_result["composite_score"],
pass_reached=4,
novelty_flag=cur_result.get("novelty_flag"),
llm_review=review,
human_notes=cur_result.get("human_notes"),
)
result.pass4_reviewed += 1
self._update_run_counters(
run_id, result, current_pass=4
)
review: str | None = None
try:
review = self.llm.review_plausibility(
description, score_dict
)
except LLMRateLimitError as exc:
self._wait_for_rate_limit(run_id, exc.retry_after)
try:
review = self.llm.review_plausibility(
description, score_dict
)
except LLMRateLimitError:
pass # still limited; skip, retry next run
if review is not None:
self.repo.save_result(
combo.id,
domain.id,
cur_result["composite_score"],
pass_reached=4,
novelty_flag=cur_result.get("novelty_flag"),
llm_review=review,
human_notes=cur_result.get("human_notes"),
)
self.repo.update_combination_status(
combo.id, "llm_reviewed"
)
result.pass4_reviewed += 1
self._update_run_counters(
run_id, result, current_pass=4
)
except CancelledError:
if run_id is not None:
@@ -319,17 +334,6 @@ class Pipeline:
)
result.top_results = self.repo.get_top_results(domain.name, limit=20)
return result
except LLMRateLimitError:
# Rate limit hit — save progress and let the user re-run to continue.
# Already-reviewed combos are persisted; resumability skips them next time.
if run_id is not None:
self.repo.update_pipeline_run(
run_id,
status="completed",
completed_at=datetime.now(timezone.utc).isoformat(),
)
result.top_results = self.repo.get_top_results(domain.name, limit=20)
return result
# Mark run as completed
if run_id is not None:
@@ -342,6 +346,18 @@ class Pipeline:
result.top_results = self.repo.get_top_results(domain.name, limit=20)
return result
def _wait_for_rate_limit(self, run_id: int | None, retry_after: int) -> None:
"""Mark run rate_limited, sleep with cancel checks, then resume."""
if run_id is not None:
self.repo.update_pipeline_run(run_id, status="rate_limited")
waited = 0
while waited < retry_after:
time.sleep(5)
waited += 5
self._check_cancelled(run_id)
if run_id is not None:
self.repo.update_pipeline_run(run_id, status="running")
def _stub_estimate(
self, combo: Combination, metric_names: list[str]
) -> dict[str, float]: