feat(scaffold): S5 — D7 sibling scaffold: SDK dep, fail-fast startup contracts, CLAUDE.md
Claude Agent SDK verified against official docs + PyPI 2026-07-03 (0.2.110, CLI bundled, offline import without API key). Contracts mirror method-spec §10/§4.1/§8: data-source, model-map (per-profile default required), termination (positive caps), binary feedback decision. TDD: tests written red-first; suite 14/14 green without any API key; ruff + mypy --strict clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
This commit is contained in:
parent
eed2f02bbb
commit
4efb72943c
7 changed files with 1604 additions and 0 deletions
0
src/portfolio_optimiser_claude/__init__.py
Normal file
0
src/portfolio_optimiser_claude/__init__.py
Normal file
103
src/portfolio_optimiser_claude/contracts.py
Normal file
103
src/portfolio_optimiser_claude/contracts.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""Fail-fast startup-contract loaders (method-spec §10).
|
||||
|
||||
ALL configuration is schema-validated at startup, BEFORE any model client is
|
||||
constructed: the data source, the model map, the termination contract (§8), and the
|
||||
run-path feedback shape (§4.1). The first malformed contract raises
|
||||
``pydantic.ValidationError``; a run never starts on a bad config.
|
||||
|
||||
Four Pydantic contracts:
|
||||
|
||||
* ``DataSourceContract`` — the local-folder data source (docs dir + positive top-k).
|
||||
* ``ModelMapContract`` — role -> model id per backend profile (``anthropic`` today;
|
||||
the SDK reaches Bedrock/Vertex/Foundry via env switches, so extra profiles are
|
||||
config, not code), each profile REQUIRING a ``default`` entry. Validates the
|
||||
bundled ``data/model_map.json``.
|
||||
* ``TerminationContract`` — positive ``max_rounds`` + ``max_tokens`` caps required at
|
||||
startup (never an unbounded loop, §8).
|
||||
* ``FeedbackContract`` — the expert-verdict run-path shape: BINARY decision (§4.1;
|
||||
``approved_with_adjustment`` is seed-frontmatter/promotion-gate vocabulary only and
|
||||
MUST be rejected here) + non-empty rationale.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from importlib.resources import files
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
_MODEL_MAP_RESOURCE = "data/model_map.json"
|
||||
|
||||
|
||||
class DataSourceContract(BaseModel):
|
||||
"""The local-folder data source config (schema-validated, fail-fast)."""
|
||||
|
||||
docs_dir: str = Field(min_length=1)
|
||||
top_k: int = Field(gt=0)
|
||||
|
||||
|
||||
class ModelMapContract(BaseModel):
|
||||
"""Role -> model id per backend profile (validates data/model_map.json)."""
|
||||
|
||||
profiles: dict[str, dict[str, str]] = Field(min_length=1)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _each_profile_has_default(self) -> ModelMapContract:
|
||||
for name, mapping in self.profiles.items():
|
||||
if "default" not in mapping:
|
||||
raise ValueError(f"model_map profile '{name}' must include a 'default' model id")
|
||||
return self
|
||||
|
||||
|
||||
class TerminationContract(BaseModel):
|
||||
"""Stop criteria + budget cap required at startup (fail-fast, never unbounded)."""
|
||||
|
||||
max_rounds: int = Field(gt=0)
|
||||
max_tokens: int = Field(gt=0)
|
||||
|
||||
|
||||
class FeedbackContract(BaseModel):
|
||||
"""The expert-verdict run-path feedback shape (§4.1: binary decision)."""
|
||||
|
||||
decision: Literal["approved", "rejected"]
|
||||
rationale: str = Field(min_length=1)
|
||||
|
||||
|
||||
class Contracts(BaseModel):
|
||||
"""The validated bundle of all startup contracts."""
|
||||
|
||||
data_source: DataSourceContract
|
||||
model_map: ModelMapContract
|
||||
termination: TerminationContract
|
||||
feedback: FeedbackContract
|
||||
|
||||
|
||||
def _bundled_model_map() -> dict[str, Any]:
|
||||
raw: dict[str, Any] = json.loads(
|
||||
files("portfolio_optimiser_claude")
|
||||
.joinpath(_MODEL_MAP_RESOURCE)
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
return raw
|
||||
|
||||
|
||||
def load_contracts(
|
||||
data_source: dict[str, Any],
|
||||
termination: dict[str, Any],
|
||||
feedback: dict[str, Any],
|
||||
*,
|
||||
model_map: dict[str, Any] | None = None,
|
||||
) -> Contracts:
|
||||
"""Validate ALL contracts at startup (fail-fast, before any model client is built).
|
||||
|
||||
Raises ``pydantic.ValidationError`` on the first malformed contract. ``model_map``
|
||||
defaults to the bundled ``data/model_map.json``.
|
||||
"""
|
||||
raw_map = _bundled_model_map() if model_map is None else model_map
|
||||
return Contracts(
|
||||
data_source=DataSourceContract(**data_source),
|
||||
model_map=ModelMapContract(**raw_map),
|
||||
termination=TerminationContract(**termination),
|
||||
feedback=FeedbackContract(**feedback),
|
||||
)
|
||||
10
src/portfolio_optimiser_claude/data/model_map.json
Normal file
10
src/portfolio_optimiser_claude/data/model_map.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"_note": "Role -> Claude model id per backend profile. Validated by contracts.py (method-spec §10). Model ids verified against platform.claude.com/docs 2026-07-03; Haiku 4.5 is the cheapest generally-suitable model (cost discipline: the single live run in S10 uses it unless re-decided). Bedrock/Vertex/Foundry are reached via SDK env switches — extra profiles are config, not code.",
|
||||
"profiles": {
|
||||
"anthropic": {
|
||||
"default": "claude-haiku-4-5-20251001",
|
||||
"proposer": "claude-haiku-4-5-20251001",
|
||||
"checker": "claude-haiku-4-5-20251001"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue