summaryrefslogtreecommitdiff
path: root/src/gap_pipeline/models.py
diff options
context:
space:
mode:
authorAnonymous Authors <anonymous@invalid.example>2026-07-24 13:24:36 -0500
committerAnonymous Authors <anonymous@invalid.example>2026-07-24 13:24:36 -0500
commitdb293f3606a97b3e417de27124858e134005acbd (patch)
tree8efeedcd2033b82d1c90eb0cb84e134421ff1a8f /src/gap_pipeline/models.py
Add minimal GAP reproduction package
Diffstat (limited to 'src/gap_pipeline/models.py')
-rw-r--r--src/gap_pipeline/models.py294
1 files changed, 294 insertions, 0 deletions
diff --git a/src/gap_pipeline/models.py b/src/gap_pipeline/models.py
new file mode 100644
index 0000000..fe7aeb7
--- /dev/null
+++ b/src/gap_pipeline/models.py
@@ -0,0 +1,294 @@
+"""Validated schemas for every GAP pipeline boundary."""
+
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from typing import Any, Literal
+
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+
+SCHEMA_VERSION = "gap-reference-v1"
+
+
+class StrictModel(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+
+class CanonicalItem(StrictModel):
+ item_id: str
+ problem: str
+ solution: str
+ problem_type: Literal["proof", "calculation"] | str = "proof"
+ variables: list[str] = Field(default_factory=list)
+ parameters: list[str] = Field(default_factory=list)
+ scientific_constants: list[str] = Field(default_factory=list)
+ metadata: dict[str, Any] = Field(default_factory=dict)
+
+ @classmethod
+ def from_public_record(cls, record: dict[str, Any]) -> "CanonicalItem":
+ excluded = {
+ "index",
+ "question",
+ "solution",
+ "problem_type",
+ "vars",
+ "params",
+ "sci_consts",
+ "variants",
+ }
+ return cls(
+ item_id=str(record["index"]),
+ problem=str(record["question"]),
+ solution=str(record["solution"]),
+ problem_type=str(record.get("problem_type", "proof")),
+ variables=list(record.get("vars", [])),
+ parameters=list(record.get("params", [])),
+ scientific_constants=list(record.get("sci_consts", [])),
+ metadata={key: value for key, value in record.items() if key not in excluded},
+ )
+
+
+class ProofNode(StrictModel):
+ node_id: str
+ claim: str
+ derivation: str
+ dependencies: list[str] = Field(default_factory=list)
+ source_span: str = ""
+ mathematical_objects: list[str] = Field(default_factory=list)
+
+
+class ProofDAG(StrictModel):
+ nodes: list[ProofNode]
+ terminal_node_id: str
+
+ @model_validator(mode="after")
+ def validate_graph(self) -> "ProofDAG":
+ if not self.nodes:
+ raise ValueError("proof DAG must contain at least one node")
+ ids = [node.node_id for node in self.nodes]
+ if len(ids) != len(set(ids)):
+ raise ValueError("proof DAG node IDs must be unique")
+ known = set(ids)
+ if self.terminal_node_id not in known:
+ raise ValueError("terminal node is absent from proof DAG")
+ for node in self.nodes:
+ if node.node_id in node.dependencies:
+ raise ValueError(f"node {node.node_id} depends on itself")
+ unknown = set(node.dependencies) - known
+ if unknown:
+ raise ValueError(
+ f"node {node.node_id} has unknown dependencies {sorted(unknown)}"
+ )
+
+ dependencies = {node.node_id: set(node.dependencies) for node in self.nodes}
+ remaining = set(known)
+ resolved: set[str] = set()
+ while remaining:
+ ready = {node_id for node_id in remaining if dependencies[node_id] <= resolved}
+ if not ready:
+ raise ValueError("proof graph contains a cycle")
+ resolved.update(ready)
+ remaining.difference_update(ready)
+
+ ancestors = {self.terminal_node_id}
+ frontier = [self.terminal_node_id]
+ while frontier:
+ node_id = frontier.pop()
+ for dependency in dependencies[node_id]:
+ if dependency not in ancestors:
+ ancestors.add(dependency)
+ frontier.append(dependency)
+ disconnected = known - ancestors
+ if disconnected:
+ raise ValueError(
+ "proof DAG contains nodes disconnected from the terminal "
+ f"conclusion: {sorted(disconnected)}"
+ )
+ return self
+
+ def topological_nodes(self) -> list[ProofNode]:
+ by_id = {node.node_id: node for node in self.nodes}
+ remaining = set(by_id)
+ resolved: set[str] = set()
+ ordered: list[ProofNode] = []
+ while remaining:
+ ready = sorted(
+ node_id
+ for node_id in remaining
+ if set(by_id[node_id].dependencies) <= resolved
+ )
+ for node_id in ready:
+ ordered.append(by_id[node_id])
+ resolved.add(node_id)
+ remaining.remove(node_id)
+ return ordered
+
+ def leaf_node_ids(self) -> list[str]:
+ return sorted(node.node_id for node in self.nodes if not node.dependencies)
+
+
+class MethodStep(StrictModel):
+ node_id: str
+ method_label: str
+ input_roles: list[str] = Field(default_factory=list)
+ output_role: str
+ invariants: list[str] = Field(default_factory=list)
+
+
+class MethodPlan(StrictModel):
+ steps: list[MethodStep]
+ terminal_node_id: str
+
+ def validate_against(self, dag: ProofDAG) -> None:
+ dag_order = [node.node_id for node in dag.topological_nodes()]
+ plan_order = [step.node_id for step in self.steps]
+ if plan_order != dag_order:
+ raise ValueError(
+ "method plan must contain every DAG node in topological order; "
+ f"expected {dag_order}, received {plan_order}"
+ )
+ if self.terminal_node_id != dag.terminal_node_id:
+ raise ValueError("method-plan terminal does not match DAG terminal")
+ if any(not step.method_label.strip() for step in self.steps):
+ raise ValueError("method labels must be non-empty")
+
+
+class ReplacementSpec(StrictModel):
+ target_node_id: str
+ original_object: str
+ replacement_object: str
+ guard_conditions: list[str]
+ guard_evidence: list[str]
+ expected_downstream_changes: list[str]
+ rationale: str
+
+ def validate_against(self, dag: ProofDAG) -> None:
+ if self.target_node_id not in dag.leaf_node_ids():
+ raise ValueError(
+ f"replacement target {self.target_node_id} is not a DAG leaf"
+ )
+ if self.original_object.strip() == self.replacement_object.strip():
+ raise ValueError("replacement must genuinely change the leaf object")
+ if not self.guard_conditions or not self.guard_evidence:
+ raise ValueError("replacement needs explicit guards and guard evidence")
+
+
+class NodeInstantiation(StrictModel):
+ node_id: str
+ method_label: str
+ instantiated_claim: str
+ instantiated_derivation: str
+ dependencies: list[str] = Field(default_factory=list)
+
+
+class DiffusedProof(StrictModel):
+ node_instantiations: list[NodeInstantiation]
+ regenerated_proof: str
+ terminal_answer: str
+
+ def validate_against(self, dag: ProofDAG, plan: MethodPlan) -> None:
+ dag_order = [node.node_id for node in dag.topological_nodes()]
+ actual_order = [node.node_id for node in self.node_instantiations]
+ if actual_order != dag_order:
+ raise ValueError("diffused proof node order differs from proof DAG")
+ expected_labels = [step.method_label for step in plan.steps]
+ actual_labels = [node.method_label for node in self.node_instantiations]
+ if actual_labels != expected_labels:
+ raise ValueError("diffused proof changes the method-label sequence")
+ expected_dependencies = {
+ node.node_id: node.dependencies for node in dag.topological_nodes()
+ }
+ for node in self.node_instantiations:
+ if node.dependencies != expected_dependencies[node.node_id]:
+ raise ValueError(
+ f"diffused proof changes dependencies for {node.node_id}"
+ )
+
+
+class KernelCandidate(StrictModel):
+ problem: str
+ proof: str
+ terminal_answer: str
+ node_instantiations: list[NodeInstantiation]
+ replacement: ReplacementSpec
+
+ def validate_against(self, dag: ProofDAG, plan: MethodPlan) -> None:
+ DiffusedProof(
+ node_instantiations=self.node_instantiations,
+ regenerated_proof=self.proof,
+ terminal_answer=self.terminal_answer,
+ ).validate_against(dag, plan)
+ self.replacement.validate_against(dag)
+
+
+class JudgeVerdict(StrictModel):
+ verdict: Literal["accept", "reject"]
+ step_by_step_check: str
+ blocking_issues: str = ""
+ patch_suggestion: str = ""
+
+ @model_validator(mode="after")
+ def validate_verdict(self) -> "JudgeVerdict":
+ if self.verdict == "accept":
+ if self.blocking_issues.strip():
+ raise ValueError("accept verdict cannot contain blocking issues")
+ else:
+ if not self.blocking_issues.strip():
+ raise ValueError("reject verdict must identify a blocking issue")
+ if not self.patch_suggestion.strip():
+ raise ValueError("reject verdict must include a patch suggestion")
+ return self
+
+
+class IterationRecord(StrictModel):
+ iteration: int
+ candidate_sha256: str
+ verdicts: list[JudgeVerdict]
+ unanimous: bool
+ pass_streak_after: int
+ repaired: bool = False
+ repaired_candidate_sha256: str | None = None
+
+
+class KernelRunResult(StrictModel):
+ item_id: str
+ status: Literal["accepted", "rejected"]
+ accepted_candidate: KernelCandidate | None = None
+ iterations: list[IterationRecord]
+ rejection_reason: str = ""
+ schema_version: str = SCHEMA_VERSION
+
+ @model_validator(mode="after")
+ def validate_terminal_state(self) -> "KernelRunResult":
+ if self.status == "accepted" and self.accepted_candidate is None:
+ raise ValueError("accepted run must include its accepted candidate")
+ if self.status == "rejected" and not self.rejection_reason:
+ raise ValueError("rejected run must state a reason")
+ return self
+
+
+class ModelCallRecord(StrictModel):
+ request_id: str
+ model: str
+ system_prompt: str
+ user_prompt: str
+ response_data: dict[str, Any]
+ raw_text: str
+ provider_response_id: str | None = None
+ usage: dict[str, Any] = Field(default_factory=dict)
+ created_at: str = Field(
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
+ )
+
+
+class StageArtifact(StrictModel):
+ item_id: str
+ stage: str
+ payload_sha256: str
+ payload: dict[str, Any]
+ request_id: str | None = None
+ schema_version: str = SCHEMA_VERSION
+ created_at: str = Field(
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
+ )