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

@@ -8,10 +8,14 @@ from abc import ABC, abstractmethod
class LLMRateLimitError(Exception):
"""Raised by a provider when the API rate limit is exceeded.
The pipeline catches this to stop gracefully and let the user re-run
to continue from where it left off.
The pipeline catches this, waits retry_after seconds (checking for
cancellation), then retries. retry_after defaults to 65s if unknown.
"""
def __init__(self, message: str, retry_after: int = 65) -> None:
super().__init__(message)
self.retry_after = retry_after
class LLMProvider(ABC):
"""Abstract LLM interface for physics estimation and plausibility review."""

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import re
import math
from physcom.llm.base import LLMProvider, LLMRateLimitError
from physcom.llm.prompts import PHYSICS_ESTIMATION_PROMPT, PLAUSIBILITY_REVIEW_PROMPT
@@ -35,7 +36,7 @@ class GeminiLLMProvider(LLMProvider):
)
except Exception as exc:
if "429" in str(exc) or "RESOURCE_EXHAUSTED" in str(exc):
raise LLMRateLimitError(str(exc)) from exc
raise LLMRateLimitError(str(exc), self._parse_retry_after(exc)) from exc
raise
return self._parse_json(response.text, metrics)
@@ -53,10 +54,15 @@ class GeminiLLMProvider(LLMProvider):
)
except Exception as exc:
if "429" in str(exc) or "RESOURCE_EXHAUSTED" in str(exc):
raise LLMRateLimitError(str(exc)) from exc
raise LLMRateLimitError(str(exc), self._parse_retry_after(exc)) from exc
raise
return response.text.strip()
def _parse_retry_after(self, exc: Exception) -> int:
"""Extract retry delay from the error message, with a safe default."""
m = re.search(r"retry in (\d+(?:\.\d+)?)", str(exc))
return math.ceil(float(m.group(1))) + 5 if m else 65
def _parse_json(self, text: str, metrics: list[str]) -> dict[str, float]:
"""Strip markdown fences and parse JSON; fall back to 0.5 per metric on error."""
text = re.sub(r"```(?:json)?\s*", "", text).strip().rstrip("`").strip()