feat(fase1): GoalContract + GoalConfig standalone fail-fast loader (F1)
This commit is contained in:
parent
9720acb18c
commit
c6f62d41db
2 changed files with 92 additions and 1 deletions
|
|
@ -18,6 +18,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
|
@ -62,6 +63,44 @@ class FeedbackContract(BaseModel):
|
|||
rationale: str = Field(min_length=1)
|
||||
|
||||
|
||||
class GoalContract(BaseModel):
|
||||
"""A configurable savings goal for a portfolio run: an absolute *øre* target and/or a percent
|
||||
target, in ``hard`` or ``soft`` mode (default hard — brief §4.3). At least one of ``absolute_ore``
|
||||
/ ``percent`` must be set. Distinct from ``TerminationContract`` (the token/round budget cap):
|
||||
this is a domain GOAL (savings reached), not resource exhaustion."""
|
||||
|
||||
absolute_ore: int | None = Field(default=None, ge=0)
|
||||
percent: float | None = Field(default=None, ge=0, le=100)
|
||||
mode: Literal["hard", "soft"] = "hard"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _at_least_one_target(self) -> GoalContract:
|
||||
if self.absolute_ore is None and self.percent is None:
|
||||
raise ValueError("GoalContract requires at least one of absolute_ore or percent")
|
||||
return self
|
||||
|
||||
|
||||
class GoalConfig(BaseModel):
|
||||
"""Both goal levels at once: an optional portfolio-wide goal plus per-project goals keyed by
|
||||
``project_id`` — so 'this project's goal stops THAT project' is unambiguously keyed (SC6 exercises
|
||||
both branches together)."""
|
||||
|
||||
portfolio: GoalContract | None = None
|
||||
per_project: dict[str, GoalContract] = Field(default_factory=dict)
|
||||
|
||||
|
||||
def load_goal_config(path: str) -> GoalConfig:
|
||||
"""Fail-fast standalone loader (mirrors ``okf.load_ir_projection``): a missing file raises
|
||||
``FileNotFoundError``, a malformed config raises ``pydantic.ValidationError``. Deliberately NOT
|
||||
folded into ``load_contracts`` — that would break its positional callers (``run.py:223``); the
|
||||
goal config is loaded separately at the orchestration entry point (Step 8)."""
|
||||
p = Path(path)
|
||||
if not p.is_file():
|
||||
raise FileNotFoundError(f"goal config not found: {path!r}")
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
return GoalConfig(**data)
|
||||
|
||||
|
||||
class Contracts(BaseModel):
|
||||
"""The validated bundle of all startup contracts."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue