release: prepare v1.3.0
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
"""Bounded critique -> repair -> re-critique orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .plan_spec import Issue
|
||||
from .repairs import (
|
||||
repair_construction_left,
|
||||
repair_dim_overlap,
|
||||
repair_duplicate_entities,
|
||||
repair_iso128,
|
||||
repair_layer_color,
|
||||
repair_untrimmed_corner,
|
||||
)
|
||||
from .scoring import score_findings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backends.base import AutoCADBackend
|
||||
|
||||
RepairHandler = Callable[["AutoCADBackend", Issue], Awaitable[dict]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepairAction:
|
||||
focus: str
|
||||
fingerprint: str
|
||||
handles: list[str]
|
||||
status: str
|
||||
detail: dict = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"focus": self.focus,
|
||||
"fingerprint": self.fingerprint,
|
||||
"handles": list(self.handles),
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefineRound:
|
||||
number: int
|
||||
score_before: float
|
||||
score_after: float
|
||||
error_count_before: int
|
||||
error_count_after: int
|
||||
transaction: str
|
||||
actions: list[RepairAction]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"number": self.number,
|
||||
"score_before": self.score_before,
|
||||
"score_after": self.score_after,
|
||||
"error_count_before": self.error_count_before,
|
||||
"error_count_after": self.error_count_after,
|
||||
"transaction": self.transaction,
|
||||
"actions": [action.to_dict() for action in self.actions],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefineResult:
|
||||
status: str
|
||||
initial_score: float
|
||||
final_score: float
|
||||
rounds: list[RefineRound]
|
||||
remaining_issues: list[dict]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"status": self.status,
|
||||
"initial_score": self.initial_score,
|
||||
"final_score": self.final_score,
|
||||
"rounds": [item.to_dict() for item in self.rounds],
|
||||
"remaining_issues": list(self.remaining_issues),
|
||||
}
|
||||
|
||||
|
||||
class RepairRegistry:
|
||||
def __init__(self, *, defaults: bool = False):
|
||||
self._handlers: dict[str, RepairHandler] = {}
|
||||
if defaults:
|
||||
self.register("construction_left", repair_construction_left)
|
||||
self.register("duplicate_entities", repair_duplicate_entities)
|
||||
self.register("layer_color", repair_layer_color)
|
||||
self.register("iso128", repair_iso128)
|
||||
self.register("untrimmed_corner", repair_untrimmed_corner)
|
||||
self.register("dim_overlap", repair_dim_overlap)
|
||||
|
||||
def register(self, focus: str, handler: RepairHandler) -> None:
|
||||
self._handlers[focus] = handler
|
||||
|
||||
def get(self, focus: str) -> RepairHandler | None:
|
||||
return self._handlers.get(focus)
|
||||
|
||||
|
||||
def issue_fingerprint(issue: Issue) -> str:
|
||||
payload = {
|
||||
"focus": issue.focus,
|
||||
"handles": sorted(issue.handles),
|
||||
"detail": issue.detail,
|
||||
}
|
||||
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _metrics(issues: list[Issue]) -> tuple[float, int]:
|
||||
errors = sum(issue.severity == "error" for issue in issues)
|
||||
warnings = sum(issue.severity == "warning" for issue in issues)
|
||||
info = sum(issue.severity == "info" for issue in issues)
|
||||
return float(score_findings(errors, warnings, info)["score"]), errors
|
||||
|
||||
|
||||
async def refine_drawing(
|
||||
backend: AutoCADBackend,
|
||||
max_rounds: int = 3,
|
||||
min_score: float = 95.0,
|
||||
focus: list[str] | None = None,
|
||||
allowed_repairs: list[str] | None = None,
|
||||
dry_run: bool = False,
|
||||
*,
|
||||
registry: RepairRegistry | None = None,
|
||||
) -> RefineResult:
|
||||
"""Apply deterministic repairs without allowing quality to regress."""
|
||||
max_rounds = max(1, min(3, int(max_rounds)))
|
||||
min_score = max(0.0, min(100.0, float(min_score)))
|
||||
registry = registry or RepairRegistry(defaults=True)
|
||||
allowed = set(allowed_repairs) if allowed_repairs is not None else None
|
||||
|
||||
issues = await backend.drawing_critique(focus)
|
||||
initial_score, _initial_errors = _metrics(issues)
|
||||
rounds: list[RefineRound] = []
|
||||
|
||||
if initial_score >= min_score:
|
||||
return RefineResult(
|
||||
"threshold_met", initial_score, initial_score, [], [item.to_dict() for item in issues]
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
actions = []
|
||||
for issue in issues:
|
||||
handler = registry.get(issue.focus)
|
||||
permitted = allowed is None or issue.focus in allowed
|
||||
actions.append(
|
||||
RepairAction(
|
||||
issue.focus,
|
||||
issue_fingerprint(issue),
|
||||
list(issue.handles),
|
||||
"planned" if handler is not None and permitted else "manual_required",
|
||||
)
|
||||
)
|
||||
score, errors = _metrics(issues)
|
||||
rounds.append(RefineRound(1, score, score, errors, errors, "not_started", actions))
|
||||
return RefineResult("dry_run", initial_score, score, rounds, [i.to_dict() for i in issues])
|
||||
|
||||
status = "max_rounds"
|
||||
for number in range(1, max_rounds + 1):
|
||||
score_before, errors_before = _metrics(issues)
|
||||
candidates: list[tuple[Issue, RepairHandler]] = []
|
||||
for issue in issues:
|
||||
handler = registry.get(issue.focus)
|
||||
if handler is not None and (allowed is None or issue.focus in allowed):
|
||||
candidates.append((issue, handler))
|
||||
if not candidates:
|
||||
status = "manual_required"
|
||||
break
|
||||
|
||||
transaction = await backend.transaction_begin()
|
||||
actions: list[RepairAction] = []
|
||||
if not isinstance(transaction, dict) or not transaction.get("ok"):
|
||||
detail = transaction if isinstance(transaction, dict) else {"result": transaction}
|
||||
actions.append(RepairAction("internal", "", [], "failed", detail))
|
||||
rounds.append(
|
||||
RefineRound(
|
||||
number,
|
||||
score_before,
|
||||
score_before,
|
||||
errors_before,
|
||||
errors_before,
|
||||
"not_started",
|
||||
actions,
|
||||
)
|
||||
)
|
||||
status = "transaction_unavailable"
|
||||
break
|
||||
try:
|
||||
for issue, handler in candidates:
|
||||
detail = await handler(backend, issue)
|
||||
actions.append(
|
||||
RepairAction(
|
||||
issue.focus,
|
||||
issue_fingerprint(issue),
|
||||
list(issue.handles),
|
||||
"applied",
|
||||
detail,
|
||||
)
|
||||
)
|
||||
after = await backend.drawing_critique(focus)
|
||||
score_after, errors_after = _metrics(after)
|
||||
except Exception as exc:
|
||||
await backend.transaction_rollback()
|
||||
actions.append(RepairAction("internal", "", [], "failed", {"error": str(exc)}))
|
||||
rounds.append(
|
||||
RefineRound(
|
||||
number,
|
||||
score_before,
|
||||
score_before,
|
||||
errors_before,
|
||||
errors_before,
|
||||
"rolled_back",
|
||||
actions,
|
||||
)
|
||||
)
|
||||
status = "repair_failed"
|
||||
issues = await backend.drawing_critique(focus)
|
||||
break
|
||||
|
||||
if score_after < score_before or errors_after > errors_before:
|
||||
await backend.transaction_rollback()
|
||||
rounds.append(
|
||||
RefineRound(
|
||||
number,
|
||||
score_before,
|
||||
score_before,
|
||||
errors_before,
|
||||
errors_before,
|
||||
"rolled_back",
|
||||
actions,
|
||||
)
|
||||
)
|
||||
status = "rolled_back"
|
||||
issues = await backend.drawing_critique(focus)
|
||||
break
|
||||
|
||||
await backend.transaction_commit()
|
||||
rounds.append(
|
||||
RefineRound(
|
||||
number,
|
||||
score_before,
|
||||
score_after,
|
||||
errors_before,
|
||||
errors_after,
|
||||
"committed",
|
||||
actions,
|
||||
)
|
||||
)
|
||||
before_fingerprints = {issue_fingerprint(item) for item in issues}
|
||||
after_fingerprints = {issue_fingerprint(item) for item in after}
|
||||
issues = after
|
||||
if score_after >= min_score:
|
||||
status = "threshold_met"
|
||||
break
|
||||
if after_fingerprints == before_fingerprints:
|
||||
status = "no_progress"
|
||||
break
|
||||
|
||||
final_score, _final_errors = _metrics(issues)
|
||||
return RefineResult(
|
||||
status,
|
||||
initial_score,
|
||||
final_score,
|
||||
rounds,
|
||||
[item.to_dict() for item in issues],
|
||||
)
|
||||
Reference in New Issue
Block a user