feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0)
Every stage of validate_proposal reasoned only about numbers the proposal itself supplied, so an internally-consistent hallucination cleared the whole gate (F3). A new stage 0 reconciles each affected_item against the project's CostBaseline before the CBC solve: an unknown cost code is rejected, and a real code carrying a quantity/unit_cost outside the configured tolerance (5% default, relative to the baseline value) is rejected. Validation, never repair. The baseline argument is OPTIONAL (None = pre-S4.0 behaviour), but both run paths set it: the road path projects project.cost_items, the bundle path loads cost-baseline.json when the bundle ships one. Bundles written before the amendment stay un-anchored, so the commons-owned goldens run byte-identically; a baseline that exists but is malformed still raises on both loaders. F8: the method-specific cap now comes from the METHOD_CAPS registry (measure type -> fraction, injectable) instead of an energy_efficiency string comparison. The baseline format and tolerance semantics were decided locally — the commons amendment (D-A pt. 2) never arrived, exactly as in S3.2. D7 mirroring stays open. Three portfolio fixtures quoted cost codes belonging to OTHER projects; the new gate caught them. They now quote each project's own lines, and the two copied REPLIES tables import the single source instead of drifting from it. Load-bearing measured (tests/test_s40_cost_baseline_loadbearing.py), six mutations all red: detach the reconciliation stage; detach the magnitude tolerance; detach the road wiring; detach the bundle wiring; ignore the injected cap registry; make the optional loader tolerant of malformed content. Control: with the road wiring detached the repaired portfolio fixtures still pass, so they are not masking the seam. 597 -> 612 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JdwK7bQ4BZkWH4t8MRDKb4
This commit is contained in:
parent
012adc0a3c
commit
126807aee7
16 changed files with 645 additions and 69 deletions
|
|
@ -3,7 +3,8 @@
|
|||
Reads a bundle the way OKF intends (progressive disclosure): start at ``index.md``, follow
|
||||
intra-bundle cross-links **recursively, depth-first in first-seen link order**, parse each file's
|
||||
YAML frontmatter, classify by the one required ``type`` field. **NO** ``agent_framework``, **NO**
|
||||
``mcp`` — pure stdlib, so the SAME navigation serves both the MAF and the Claude-SDK
|
||||
``mcp`` — stdlib + ``pydantic`` only (as ``dimension.py``; the typed contracts this module loads
|
||||
live in ``ir.py``), so the SAME navigation serves both the MAF and the Claude-SDK
|
||||
implementations unchanged (målbilde §4 vendor-neutrality).
|
||||
|
||||
Link resolution follows ``shared/method-spec.md`` §3 Step 1: a leading ``/`` denotes the **bundle
|
||||
|
|
@ -29,10 +30,12 @@ from dataclasses import dataclass
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from portfolio_optimiser.ir import CostBaseline
|
||||
from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve
|
||||
|
||||
_INDEX_NAME = "index.md"
|
||||
_IR_PROJECTION = "validator-input.json"
|
||||
_COST_BASELINE = "cost-baseline.json"
|
||||
# Intra-bundle markdown cross-links: ``](target.md)``. A path separator is NOT a rejection reason —
|
||||
# ``_resolve_target`` decides in-/out-of-bundle, and only escape is refused (method-spec §3 Step 1).
|
||||
_LINK_RE = re.compile(r"\]\(([^)]+\.md)\)")
|
||||
|
|
@ -285,6 +288,39 @@ def link_in_index(bundle_dir: str, target_name: str, label: str) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def load_cost_baseline(bundle_dir: str, name: str = _COST_BASELINE) -> CostBaseline:
|
||||
"""Load the bundle's cost baseline (``cost-baseline.json`` by default): the project's ACTUAL
|
||||
cost lines (``{code: {quantity, unit_cost}}``), which the deterministic validator reconciles a
|
||||
proposal's ``affected_items`` against (S4.0, F3).
|
||||
|
||||
Fail-fast, mirroring ``load_ir_projection`` and ``dimension.load_dimension``: a missing file
|
||||
raises ``FileNotFoundError`` and malformed content raises ``pydantic.ValidationError``. A cost
|
||||
baseline is authoritative gate input — a tolerantly-degraded one would silently un-anchor the
|
||||
gate, which is precisely the failure this stage exists to prevent. (The tolerant skip rule
|
||||
belongs to the RAW verdict-inbox layer, never here.)
|
||||
|
||||
Use ``load_optional_cost_baseline`` where the ABSENCE of the file is legitimate."""
|
||||
resolved = Path(safe_resolve(bundle_dir, name))
|
||||
if not resolved.is_file():
|
||||
raise FileNotFoundError(f"cost baseline not found in bundle: {name!r}")
|
||||
return CostBaseline.model_validate_json(resolved.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_optional_cost_baseline(bundle_dir: str, name: str = _COST_BASELINE) -> CostBaseline | None:
|
||||
"""``load_cost_baseline`` where a MISSING file is legitimate: returns ``None`` instead of
|
||||
raising. This is the run path's loader — a bundle authored before the baseline amendment is
|
||||
simply un-anchored (``None`` = pre-S4.0 behaviour), not an error, which is what keeps every
|
||||
existing bundle (including the commons-owned goldens) running byte-identically.
|
||||
|
||||
The tolerance stops at absence: a baseline that EXISTS but is malformed still raises. Reading a
|
||||
corrupt baseline as "no baseline" would hand back an un-anchored gate under the appearance of an
|
||||
anchored one (the same reasoning as ``budget.read_spend``)."""
|
||||
try:
|
||||
return load_cost_baseline(bundle_dir, name)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def load_ir_projection(bundle_dir: str, name: str = _IR_PROJECTION) -> dict[str, Any]:
|
||||
"""Load the bundle's IR projection (``validator-input.json`` by default): the candidate
|
||||
measure's cost-IR (``measure``, ``affected_items``, ``claimed_saving_nok``) — the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue