"""What the MODEL is asked to return. Deliberately separate from `models.py`. The model never supplies `doc_id` (we know it), never sets `extraction_status`, and never sets the conflict or diff fields — validation owns those. **Asking a model for fields it cannot know is an invitation to fabricate**, so the request schema is narrower than the stored one. v2 narrowed it further, and every removal has a reason of that kind: - **ids and links** (`term_id`, `formula_id`, `rule_id`, `domain_id`, `defining_formula_id`, `formula_ids`, `term_ids`, `variables[].term_id`) are minted deterministically. A model cannot produce a stable identifier — it has no memory of what it emitted last run — and `rule_id` being model-written was a real defect, not a style choice. - **`domain` / `company`** were never in any prompt's field guide, so the model filled them purely by copying the worked examples, and neither was span-guarded — a wrong value was undetectable. - **`formula_latex` on the glossary draft** duplicated the formula branch. One truth, one place; the glossary now references it. - **`interpretation`** moved to the Interpretation Pack, reached via `term_id`. """ from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field from ..models import SubdomainEnum RuleTypeDraft = Literal["interpretation", "calculation"] class ProvenanceDraft(BaseModel): section_no: str | None = None page: int span: str class GlossaryDraft(BaseModel): term: str full_name: str | None = None # The wording exactly as the document writes it, even when "wrong" — the # standard heads its section "Physical of Availability (PA)". Surfacing the # discrepancy is the point; normalising it hides a decision the expert owns. # # Still requested, though the stored value is taken deterministically from # the section heading: the model's answer is the fallback when no evidence # chunk carries a heading naming the term. source_wording: str | None = None definition: str | None = None subdomain_tags: list[SubdomainEnum] = Field(default_factory=list) provenance: ProvenanceDraft class RuleDraft(BaseModel): # `rule_id` is NOT requested — it is minted from the document position. The # model previously wrote it as SCREAMING_SNAKE_CASE, which meant the same # rule could come back under a different id on the next run. rule_type: RuleTypeDraft = "calculation" statement: str | None = None condition: str | None = None consequence: str | None = None provenance: ProvenanceDraft class VariableDraft(BaseModel): symbol: str meaning: str | None = None class FormulaDraft(BaseModel): name: str | None = None formula_latex: str | None = None variables: list[VariableDraft] = Field(default_factory=list) unit: str | None = None provenance: ProvenanceDraft class SummaryDraft(BaseModel): title: str | None = None # Located, not composed: the document's own statement of purpose, copied # verbatim, so the span check can prove it. See prompts/summary.txt. purpose_verbatim: str | None = None key_parameters: list[str] = Field(default_factory=list) provenance: ProvenanceDraft DRAFTS = { "glossary": GlossaryDraft, "rule": RuleDraft, "formula": FormulaDraft, "summary": SummaryDraft, } def schema_for(branch: str) -> dict: return DRAFTS[branch].model_json_schema()