"""领域模型。""" from __future__ import annotations from dataclasses import asdict, dataclass from typing import Any DIMENSIONS = ( "Clarity", "Structure", "Executability", "Completeness", "Constraint Salience", ) @dataclass class SkillUnit: unit_id: str level: str parent_id: str | None heading: str text: str order: int start: int end: int heading_depth: int | None = None @dataclass class CellScore: score: float evidence: list[str] reason: str @dataclass class Coordinate: unit_id: str dimension: str normalized_gap: float @dataclass class LocalEdit: unit_id: str dimension: str new_text: str edit_summary: str reason: str @classmethod def from_dict(cls, value: dict[str, Any]) -> "LocalEdit": required = {"unit_id", "dimension", "new_text", "edit_summary", "reason"} missing = sorted(required - value.keys()) if missing: raise ValueError(f"local edit missing fields: {', '.join(missing)}") if not all(isinstance(value[key], str) for key in required): raise ValueError("local edit fields must be strings") return cls(**{key: value[key] for key in cls.__dataclass_fields__}) @dataclass class ScoreMatrix: level: str units: list[SkillUnit] columns: dict[str, dict[str, CellScore]] def to_dict(self) -> dict[str, Any]: return { "level": self.level, "units": [asdict(unit) for unit in self.units], "columns": { dimension: {unit_id: asdict(cell) for unit_id, cell in column.items()} for dimension, column in self.columns.items() }, } @classmethod def from_dict(cls, value: dict[str, Any]) -> "ScoreMatrix": return cls( level=str(value["level"]), units=[SkillUnit(**item) for item in value["units"]], columns={ dimension: { unit_id: CellScore(float(cell["score"]), list(cell["evidence"]), str(cell["reason"])) for unit_id, cell in column.items() } for dimension, column in value["columns"].items() }, ) def unit(self, unit_id: str) -> SkillUnit: return next(unit for unit in self.units if unit.unit_id == unit_id) def normalized_gaps(self) -> dict[str, float]: gaps: dict[str, float] = {} for dimension in DIMENSIONS: values = [self.columns[dimension][unit.unit_id].score for unit in self.units] gaps[dimension] = (max(values) - min(values)) / 4.0 if values else 0.0 return gaps def select_coordinate( self, threshold: float, dimension: str | None = None, excluded: set[tuple[str, str]] | None = None, ) -> Coordinate | None: gaps = self.normalized_gaps() excluded = excluded or set() def weak_units(item: str) -> list[SkillUnit]: column = self.columns[item] maximum = max((cell.score for cell in column.values()), default=0.0) return [ unit for unit in self.units if (unit.unit_id, item) not in excluded and (maximum - column[unit.unit_id].score) / 4.0 > threshold ] available = [ item for item in ([dimension] if dimension else DIMENSIONS) if item is not None and gaps[item] > threshold and weak_units(item) ] if not available: return None dimension = max(available, key=lambda item: gaps[item]) target = min( weak_units(dimension), key=lambda unit: (self.columns[dimension][unit.unit_id].score, unit.order), ) return Coordinate( target.unit_id, dimension, gaps[dimension], )