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:
@@ -125,8 +125,20 @@ class Repository:
|
||||
self.conn.commit()
|
||||
|
||||
def delete_entity(self, entity_id: int) -> None:
|
||||
combo_ids = [
|
||||
r["combination_id"]
|
||||
for r in self.conn.execute(
|
||||
"SELECT combination_id FROM combination_entities WHERE entity_id = ?",
|
||||
(entity_id,),
|
||||
).fetchall()
|
||||
]
|
||||
if combo_ids:
|
||||
ph = ",".join("?" * len(combo_ids))
|
||||
self.conn.execute(f"DELETE FROM combination_results WHERE combination_id IN ({ph})", combo_ids)
|
||||
self.conn.execute(f"DELETE FROM combination_scores WHERE combination_id IN ({ph})", combo_ids)
|
||||
self.conn.execute(f"DELETE FROM combination_entities WHERE combination_id IN ({ph})", combo_ids)
|
||||
self.conn.execute(f"DELETE FROM combinations WHERE id IN ({ph})", combo_ids)
|
||||
self.conn.execute("DELETE FROM dependencies WHERE entity_id = ?", (entity_id,))
|
||||
self.conn.execute("DELETE FROM combination_entities WHERE entity_id = ?", (entity_id,))
|
||||
self.conn.execute("DELETE FROM entities WHERE id = ?", (entity_id,))
|
||||
self.conn.commit()
|
||||
|
||||
@@ -228,6 +240,99 @@ class Repository:
|
||||
rows = self.conn.execute("SELECT name FROM domains ORDER BY name").fetchall()
|
||||
return [self.get_domain(r["name"]) for r in rows]
|
||||
|
||||
def get_domain_by_id(self, domain_id: int) -> Domain | None:
|
||||
row = self.conn.execute("SELECT * FROM domains WHERE id = ?", (domain_id,)).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
weights = self.conn.execute(
|
||||
"""SELECT m.name, m.unit, dmw.weight, dmw.norm_min, dmw.norm_max, dmw.metric_id
|
||||
FROM domain_metric_weights dmw
|
||||
JOIN metrics m ON dmw.metric_id = m.id
|
||||
WHERE dmw.domain_id = ?""",
|
||||
(row["id"],),
|
||||
).fetchall()
|
||||
return Domain(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
description=row["description"] or "",
|
||||
metric_bounds=[
|
||||
MetricBound(
|
||||
metric_name=w["name"], weight=w["weight"],
|
||||
norm_min=w["norm_min"], norm_max=w["norm_max"],
|
||||
metric_id=w["metric_id"], unit=w["unit"] or "",
|
||||
)
|
||||
for w in weights
|
||||
],
|
||||
)
|
||||
|
||||
def update_domain(self, domain_id: int, name: str, description: str) -> None:
|
||||
self.conn.execute(
|
||||
"UPDATE domains SET name = ?, description = ? WHERE id = ?",
|
||||
(name, description, domain_id),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def add_metric_bound(self, domain_id: int, mb: MetricBound) -> MetricBound:
|
||||
metric_id = self.ensure_metric(mb.metric_name, mb.unit)
|
||||
mb.metric_id = metric_id
|
||||
self.conn.execute(
|
||||
"""INSERT OR REPLACE INTO domain_metric_weights
|
||||
(domain_id, metric_id, weight, norm_min, norm_max)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(domain_id, metric_id, mb.weight, mb.norm_min, mb.norm_max),
|
||||
)
|
||||
self.conn.commit()
|
||||
return mb
|
||||
|
||||
def update_metric_bound(
|
||||
self, domain_id: int, metric_id: int, weight: float, norm_min: float, norm_max: float, unit: str
|
||||
) -> None:
|
||||
self.conn.execute(
|
||||
"""UPDATE domain_metric_weights
|
||||
SET weight = ?, norm_min = ?, norm_max = ?
|
||||
WHERE domain_id = ? AND metric_id = ?""",
|
||||
(weight, norm_min, norm_max, domain_id, metric_id),
|
||||
)
|
||||
if unit:
|
||||
self.conn.execute(
|
||||
"UPDATE metrics SET unit = ? WHERE id = ?",
|
||||
(unit, metric_id),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def delete_metric_bound(self, domain_id: int, metric_id: int) -> None:
|
||||
self.conn.execute(
|
||||
"DELETE FROM domain_metric_weights WHERE domain_id = ? AND metric_id = ?",
|
||||
(domain_id, metric_id),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def delete_domain(self, domain_id: int) -> None:
|
||||
self.conn.execute("DELETE FROM pipeline_runs WHERE domain_id = ?", (domain_id,))
|
||||
self.conn.execute("DELETE FROM combination_results WHERE domain_id = ?", (domain_id,))
|
||||
self.conn.execute("DELETE FROM combination_scores WHERE domain_id = ?", (domain_id,))
|
||||
self.conn.execute("DELETE FROM domain_metric_weights WHERE domain_id = ?", (domain_id,))
|
||||
self.conn.execute("DELETE FROM domains WHERE id = ?", (domain_id,))
|
||||
self.conn.commit()
|
||||
|
||||
def reset_domain_results(self, domain_name: str) -> int:
|
||||
"""Delete all pipeline results for a domain so it can be re-run from scratch.
|
||||
|
||||
Returns the number of result rows deleted.
|
||||
"""
|
||||
domain = self.get_domain(domain_name)
|
||||
if not domain:
|
||||
return 0
|
||||
count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM combination_results WHERE domain_id = ?",
|
||||
(domain.id,),
|
||||
).fetchone()[0]
|
||||
self.conn.execute("DELETE FROM combination_scores WHERE domain_id = ?", (domain.id,))
|
||||
self.conn.execute("DELETE FROM combination_results WHERE domain_id = ?", (domain.id,))
|
||||
self.conn.execute("DELETE FROM pipeline_runs WHERE domain_id = ?", (domain.id,))
|
||||
self.conn.commit()
|
||||
return count
|
||||
|
||||
# ── Combinations ────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
@@ -265,13 +370,17 @@ class Repository:
|
||||
def update_combination_status(
|
||||
self, combo_id: int, status: str, block_reason: str | None = None
|
||||
) -> None:
|
||||
# Don't downgrade 'reviewed' to 'scored' — preserve human review state
|
||||
if status == "scored":
|
||||
# Don't downgrade from higher pass states — preserves human/LLM review data
|
||||
if status in ("scored", "llm_reviewed"):
|
||||
row = self.conn.execute(
|
||||
"SELECT status FROM combinations WHERE id = ?", (combo_id,)
|
||||
).fetchone()
|
||||
if row and row["status"] == "reviewed":
|
||||
return
|
||||
if row:
|
||||
cur = row["status"]
|
||||
if status == "scored" and cur in ("llm_reviewed", "reviewed"):
|
||||
return
|
||||
if status == "llm_reviewed" and cur == "reviewed":
|
||||
return
|
||||
self.conn.execute(
|
||||
"UPDATE combinations SET status = ?, block_reason = ? WHERE id = ?",
|
||||
(status, block_reason, combo_id),
|
||||
@@ -292,6 +401,60 @@ class Repository:
|
||||
block_reason=row["block_reason"], entities=entities,
|
||||
)
|
||||
|
||||
def _bulk_load_combinations(self, combo_ids: list[int]) -> dict[int, Combination]:
|
||||
"""Load multiple Combinations in O(4) queries instead of O(N*M)."""
|
||||
if not combo_ids:
|
||||
return {}
|
||||
ph = ",".join("?" * len(combo_ids))
|
||||
combo_rows = self.conn.execute(
|
||||
f"SELECT * FROM combinations WHERE id IN ({ph})", combo_ids
|
||||
).fetchall()
|
||||
combos: dict[int, Combination] = {
|
||||
r["id"]: Combination(
|
||||
id=r["id"], hash=r["hash"], status=r["status"],
|
||||
block_reason=r["block_reason"], entities=[],
|
||||
)
|
||||
for r in combo_rows
|
||||
}
|
||||
ce_rows = self.conn.execute(
|
||||
f"SELECT combination_id, entity_id FROM combination_entities WHERE combination_id IN ({ph})",
|
||||
combo_ids,
|
||||
).fetchall()
|
||||
combo_to_eids: dict[int, list[int]] = {}
|
||||
for r in ce_rows:
|
||||
combo_to_eids.setdefault(r["combination_id"], []).append(r["entity_id"])
|
||||
|
||||
entity_ids = list({r["entity_id"] for r in ce_rows})
|
||||
if entity_ids:
|
||||
eph = ",".join("?" * len(entity_ids))
|
||||
entity_rows = self.conn.execute(
|
||||
f"""SELECT e.id, e.name, e.description, d.name as dimension, e.dimension_id
|
||||
FROM entities e JOIN dimensions d ON e.dimension_id = d.id
|
||||
WHERE e.id IN ({eph})""",
|
||||
entity_ids,
|
||||
).fetchall()
|
||||
dep_rows = self.conn.execute(
|
||||
f"SELECT * FROM dependencies WHERE entity_id IN ({eph})", entity_ids
|
||||
).fetchall()
|
||||
deps_by_entity: dict[int, list[Dependency]] = {}
|
||||
for r in dep_rows:
|
||||
deps_by_entity.setdefault(r["entity_id"], []).append(Dependency(
|
||||
id=r["id"], category=r["category"], key=r["key"],
|
||||
value=r["value"], unit=r["unit"], constraint_type=r["constraint_type"],
|
||||
))
|
||||
entities_by_id: dict[int, Entity] = {
|
||||
r["id"]: Entity(
|
||||
id=r["id"], name=r["name"], description=r["description"] or "",
|
||||
dimension=r["dimension"], dimension_id=r["dimension_id"],
|
||||
dependencies=deps_by_entity.get(r["id"], []),
|
||||
)
|
||||
for r in entity_rows
|
||||
}
|
||||
for cid, eids in combo_to_eids.items():
|
||||
if cid in combos:
|
||||
combos[cid].entities = [entities_by_id[eid] for eid in eids if eid in entities_by_id]
|
||||
return combos
|
||||
|
||||
def list_combinations(self, status: str | None = None) -> list[Combination]:
|
||||
if status:
|
||||
rows = self.conn.execute(
|
||||
@@ -299,7 +462,9 @@ class Repository:
|
||||
).fetchall()
|
||||
else:
|
||||
rows = self.conn.execute("SELECT id FROM combinations ORDER BY id").fetchall()
|
||||
return [self.get_combination(r["id"]) for r in rows]
|
||||
ids = [r["id"] for r in rows]
|
||||
combos = self._bulk_load_combinations(ids)
|
||||
return [combos[i] for i in ids if i in combos]
|
||||
|
||||
# ── Scores & Results ────────────────────────────────────────
|
||||
|
||||
@@ -385,9 +550,13 @@ class Repository:
|
||||
).fetchone()
|
||||
if not row or row["total"] == 0:
|
||||
return None
|
||||
# Also count blocked combos (they have no results but exist)
|
||||
blocked = self.conn.execute(
|
||||
"SELECT COUNT(*) as cnt FROM combinations WHERE status = 'blocked'"
|
||||
"""SELECT COUNT(*) as cnt
|
||||
FROM combinations c
|
||||
JOIN combination_results cr ON cr.combination_id = c.id
|
||||
JOIN domains d ON cr.domain_id = d.id
|
||||
WHERE c.status = 'blocked' AND d.name = ?""",
|
||||
(domain_name,),
|
||||
).fetchone()
|
||||
return {
|
||||
"total_results": row["total"],
|
||||
@@ -422,19 +591,20 @@ class Repository:
|
||||
params.append(status)
|
||||
query += " ORDER BY cr.composite_score DESC"
|
||||
rows = self.conn.execute(query, params).fetchall()
|
||||
results = []
|
||||
for r in rows:
|
||||
combo = self.get_combination(r["combination_id"])
|
||||
results.append({
|
||||
"combination": combo,
|
||||
combo_ids = [r["combination_id"] for r in rows]
|
||||
combos = self._bulk_load_combinations(combo_ids)
|
||||
return [
|
||||
{
|
||||
"combination": combos.get(r["combination_id"]),
|
||||
"composite_score": r["composite_score"],
|
||||
"novelty_flag": r["novelty_flag"],
|
||||
"llm_review": r["llm_review"],
|
||||
"human_notes": r["human_notes"],
|
||||
"pass_reached": r["pass_reached"],
|
||||
"domain_id": r["domain_id"],
|
||||
})
|
||||
return results
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_top_results(self, domain_name: str, limit: int = 10) -> list[dict]:
|
||||
"""Return top-N results for a domain, ordered by composite_score DESC."""
|
||||
@@ -448,18 +618,30 @@ class Repository:
|
||||
LIMIT ?""",
|
||||
(domain_name, limit),
|
||||
).fetchall()
|
||||
results = []
|
||||
for r in rows:
|
||||
combo = self.get_combination(r["combination_id"])
|
||||
results.append({
|
||||
"combination": combo,
|
||||
combo_ids = [r["combination_id"] for r in rows]
|
||||
combos = self._bulk_load_combinations(combo_ids)
|
||||
return [
|
||||
{
|
||||
"combination": combos.get(r["combination_id"]),
|
||||
"composite_score": r["composite_score"],
|
||||
"novelty_flag": r["novelty_flag"],
|
||||
"llm_review": r["llm_review"],
|
||||
"human_notes": r["human_notes"],
|
||||
"pass_reached": r["pass_reached"],
|
||||
})
|
||||
return results
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_results_for_combination(self, combo_id: int) -> list[dict]:
|
||||
"""Return all domain results for a combination."""
|
||||
rows = self.conn.execute(
|
||||
"""SELECT cr.*, d.name as domain_name
|
||||
FROM combination_results cr
|
||||
JOIN domains d ON cr.domain_id = d.id
|
||||
WHERE cr.combination_id = ?""",
|
||||
(combo_id,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# ── Pipeline Runs ────────────────────────────────────────
|
||||
|
||||
@@ -473,10 +655,19 @@ class Repository:
|
||||
self.conn.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
_PIPELINE_RUN_UPDATABLE = frozenset({
|
||||
"status", "total_combos", "combos_pass1", "combos_pass2",
|
||||
"combos_pass3", "combos_pass4", "current_pass",
|
||||
"error_message", "started_at", "completed_at",
|
||||
})
|
||||
|
||||
def update_pipeline_run(self, run_id: int, **fields) -> None:
|
||||
"""Update arbitrary fields on a pipeline_run."""
|
||||
"""Update fields on a pipeline_run. Only allowlisted column names are accepted."""
|
||||
if not fields:
|
||||
return
|
||||
invalid = set(fields) - self._PIPELINE_RUN_UPDATABLE
|
||||
if invalid:
|
||||
raise ValueError(f"Invalid pipeline_run fields: {invalid}")
|
||||
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
||||
values = list(fields.values())
|
||||
values.append(run_id)
|
||||
|
||||
Reference in New Issue
Block a user