P6 (økt 108) ended in ValidatedProposal (verdict 5fd6272e3725fe68) on two cost codes -- M-04-01 / M-04-03 -- that appear in NO prompt of that run. Measured here first, verbatim: validate_proposal(p, baseline=None) validates it; the same proposal against any non-empty CostBaseline is rejected naming both codes. So the hole was never "fabrication goes uncaught" -- _reconcile_against_baseline exists and is right -- but that the falsifier is reached only through `if baseline is not None`. The input always exists; the baseline does not. New stage 0b (_ground_against_input), OUTSIDE the baseline branch, after stage 0 so an anchored run's message is byte-identical to before. ONE Rejection, the validator's own type, naming EVERY ungrounded identifier "; "-joined in the proposal's own order (økt 94's completeness reason). The rule has NO pattern -- `code in grounding`, exact substring -- and that is a measurement: over the delivered corpora (K2 1108 files / 2 005 561 chars, the three N payloads 8 excerpts each) the identifier forms are heterogeneous, and a pattern chosen to cover them would be a rule about shapes. Bare numerals are the one inert class (46 394 occurrences / 2 117 distinct in K2); the rule fails OPEN there, never closed. Evidence is three non-model-authored sources: what run_project DELIVERED (the rendered cut/pointer/chunks plus the base's context_files -- never files, which would make the type: verdict layer evidence), the project's own cost lines, and the baseline's codes when anchored. The rendered PROMPT is deliberately NOT evidence, on two measurements: gen_context IS the debate output on the S2c path, and from attempt 2 the prompt carries the previous Rejection.reason verbatim -- which for this stage QUOTES the identifier it just refused. Grounding in the prompt would let the gate's own refusal disarm it on its second round. Prose scanning was chosen against WITH THE NUMBERS: a typed gate catches 2/2 (P6) and 2/2 (S7c) -- 100% of what reached a verdict. What stays uncaught, said plainly: an ungrounded identifier that lives only in agent/debate prose and never becomes an affected_item code (2 of 4 P6, 2 of 4 S7c, 1 of 2 P4). Iron Law: 9 red / 2 green before the rule existed. Ten mutations all red against the whole suite, green control 1558 passed / 5 skipped (from 1543/5, superset, 0 removed), golden demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). Three existing fixtures changed, no gate weakened -- most of all test_pre_amendment_bundle_runs_unchanged, which sent the SAME FABRICATED code and asserted it validated: the økt-108 hole written down as an expectation. No paid run. Order 20260909T113641Z-38938691-from-.claude. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
292 lines
13 KiB
Python
292 lines
13 KiB
Python
"""S4.0 load-bearing seam: the deterministic gate is ANCHORED to the project's real cost baseline.
|
|
|
|
Review finding F3: every stage of ``validate_proposal`` reasoned about the numbers the *proposal
|
|
itself* supplied, so a hallucinated cost line (an invented code, or a real code at an invented
|
|
magnitude) could clear the whole gate as long as its own arithmetic was internally consistent. The
|
|
reconciliation stage closes that: each ``affected_item`` must correspond to a line in the project's
|
|
cost baseline, within a configured tolerance.
|
|
|
|
Every RED here is a genuine OUTCOME FLIP, not a reason-string check: the fabricated proposals are
|
|
deliberately built to pass the P90 / nominal / method stages, so detaching the reconciliation makes
|
|
them ``ValidatedProposal`` again. Controls prove causality (a real baseline line, same shape,
|
|
validates), and the no-baseline arm proves the argument stays OPTIONAL (``None`` = pre-S4.0
|
|
behaviour, which is why the existing suite stands).
|
|
|
|
Measured detach points (see the session log): the reconciliation stage · the magnitude tolerance ·
|
|
the road-path wiring in ``run.py`` · the bundle-path wiring · the method-cap registry key (F8).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from conftest import SyntheticUsageChatClient
|
|
from pydantic import ValidationError
|
|
|
|
from portfolio_optimiser import okf
|
|
from portfolio_optimiser.ir import AffectedItem, CostBaseline, CostBaselineLine, SavingsProposal
|
|
from portfolio_optimiser.reference_domain import load_reference_projects
|
|
from portfolio_optimiser.run import run_project
|
|
from portfolio_optimiser.validator import (
|
|
Rejection,
|
|
ValidatedProposal,
|
|
baseline_from_project,
|
|
validate_proposal,
|
|
)
|
|
|
|
# The repo-local S4.0 fixture bundle: the ONLY bundle carrying a ``cost-baseline.json`` (the pre-
|
|
# amendment bundles deliberately have none — that is the optional-argument control below).
|
|
_DATA = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "data" / "bundles"
|
|
BASELINE_BUNDLE = _DATA / "bygg-energi-baseline-mikro"
|
|
PRE_AMENDMENT_BUNDLE = _DATA / "bygg-energi-mikro-a"
|
|
|
|
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (sim)"}
|
|
|
|
# FV42-GSV-E1's real cost line 05.2 (Asfalt Ab11): 4300 m2 x 215 NOK. Affected total 924500 ->
|
|
# degenerate P90 = 0.30 x 924500 = 277350, so claimed 200000 clears every pre-S4.0 stage.
|
|
_REAL_CODE = "05.2"
|
|
_REAL_QTY = 4300.0
|
|
_REAL_UNIT_COST = 215.0
|
|
_REAL_CLAIM = 200000.0
|
|
|
|
# The F3 scenario: an invented code carrying a 10 MNOK line. The claim is set at exactly the generic
|
|
# feasible (0.30 x 10 MNOK) so the fabrication is numerically IMPECCABLE — every pre-S4.0 stage
|
|
# passes it. Only the baseline reconciliation can reject it, which is what makes the detach a flip.
|
|
_FAKE_CODE = "XX"
|
|
_FAKE_UNIT_COST = 10_000_000.0
|
|
_FAKE_CLAIM = 3_000_000.0
|
|
|
|
|
|
def _fv42_baseline() -> CostBaseline:
|
|
return baseline_from_project(
|
|
next(p for p in load_reference_projects() if p.id == "FV42-GSV-E1")
|
|
)
|
|
|
|
|
|
def _proposal(code: str, quantity: float, unit_cost: float, claimed: float) -> SavingsProposal:
|
|
return SavingsProposal(
|
|
project_id="FV42-GSV-E1",
|
|
measure="Reduce scope",
|
|
affected_items=[AffectedItem(code=code, quantity=quantity, unit_cost=unit_cost)],
|
|
claimed_saving_nok=claimed,
|
|
assumptions={},
|
|
)
|
|
|
|
|
|
# --- Arm 1: the reconciliation stage itself -------------------------------------------------------
|
|
|
|
|
|
def test_fabricated_cost_code_is_rejected() -> None:
|
|
"""RED (F3): a proposal citing a cost code that exists nowhere in the project's baseline is
|
|
rejected, even though its own arithmetic clears the LP/P90/nominal stages. Detach the
|
|
reconciliation stage and the SAME proposal validates."""
|
|
result = validate_proposal(
|
|
_proposal(_FAKE_CODE, 1.0, _FAKE_UNIT_COST, _FAKE_CLAIM), baseline=_fv42_baseline()
|
|
)
|
|
assert isinstance(result, Rejection), "a hallucinated cost code must never reach validated"
|
|
assert "unknown cost code" in result.reason
|
|
assert _FAKE_CODE in result.reason
|
|
|
|
|
|
def test_real_baseline_line_still_validates() -> None:
|
|
"""Causality control: the same shape of proposal on a REAL baseline line validates — so the
|
|
rejection above is caused by the code being absent from the baseline, not by the new stage
|
|
rejecting everything."""
|
|
result = validate_proposal(
|
|
_proposal(_REAL_CODE, _REAL_QTY, _REAL_UNIT_COST, _REAL_CLAIM), baseline=_fv42_baseline()
|
|
)
|
|
assert isinstance(result, ValidatedProposal)
|
|
|
|
|
|
def test_baseline_is_optional_and_none_is_pre_s40_behaviour() -> None:
|
|
"""The baseline argument is OPTIONAL: with ``None`` the fabricated proposal validates exactly as
|
|
it did before S4.0. This is the property the existing suite rests on — and the reason the RED
|
|
above is a flip rather than a tightening of an already-rejecting path."""
|
|
result = validate_proposal(_proposal(_FAKE_CODE, 1.0, _FAKE_UNIT_COST, _FAKE_CLAIM))
|
|
assert isinstance(result, ValidatedProposal)
|
|
|
|
|
|
# --- Arm 2: the magnitude tolerance ---------------------------------------------------------------
|
|
|
|
|
|
def test_inflated_unit_cost_on_a_real_code_is_rejected() -> None:
|
|
"""RED: a REAL cost code at an invented unit_cost (+20%, well past the 5% default tolerance) is
|
|
rejected. Detach the tolerance check and only the code-membership test remains — the inflated
|
|
line then validates, because the code itself is genuine."""
|
|
inflated = _REAL_UNIT_COST * 1.20
|
|
result = validate_proposal(
|
|
_proposal(_REAL_CODE, _REAL_QTY, inflated, _REAL_CLAIM), baseline=_fv42_baseline()
|
|
)
|
|
assert isinstance(result, Rejection)
|
|
assert "unit_cost" in result.reason and _REAL_CODE in result.reason
|
|
|
|
|
|
def test_inflated_quantity_on_a_real_code_is_rejected() -> None:
|
|
"""RED: the same for quantity — a real code at an invented quantity (+20%) is rejected."""
|
|
result = validate_proposal(
|
|
_proposal(_REAL_CODE, _REAL_QTY * 1.20, _REAL_UNIT_COST, _REAL_CLAIM),
|
|
baseline=_fv42_baseline(),
|
|
)
|
|
assert isinstance(result, Rejection)
|
|
assert "quantity" in result.reason
|
|
|
|
|
|
def test_within_tolerance_deviation_is_admitted() -> None:
|
|
"""Causality control for the tolerance: a 2% deviation (rounding-scale, inside the 5% default)
|
|
validates — the rejections above are caused by the SIZE of the deviation, not by any deviation
|
|
at all."""
|
|
result = validate_proposal(
|
|
_proposal(_REAL_CODE, _REAL_QTY, _REAL_UNIT_COST * 1.02, _REAL_CLAIM),
|
|
baseline=_fv42_baseline(),
|
|
)
|
|
assert isinstance(result, ValidatedProposal)
|
|
|
|
|
|
def test_tolerance_is_configurable() -> None:
|
|
"""The tolerance is config, not a constant: the same 2% deviation is rejected under a stricter
|
|
caller-supplied tolerance."""
|
|
result = validate_proposal(
|
|
_proposal(_REAL_CODE, _REAL_QTY, _REAL_UNIT_COST * 1.02, _REAL_CLAIM),
|
|
baseline=_fv42_baseline(),
|
|
tolerance=0.001,
|
|
)
|
|
assert isinstance(result, Rejection)
|
|
|
|
|
|
# --- Arm 3: the loader (fail-fast, mirroring ``load_ir_projection``) -------------------------------
|
|
|
|
|
|
def test_bundle_baseline_loads_from_the_fixture() -> None:
|
|
baseline = okf.load_cost_baseline(str(BASELINE_BUNDLE))
|
|
assert baseline.project_id == "BYGG-ENERGI-BASELINE-MIKRO"
|
|
assert baseline.items["ENERGI-TOTAL-EL"] == CostBaselineLine(quantity=180000, unit_cost=1.0)
|
|
|
|
|
|
def test_missing_baseline_is_fail_fast_but_optional_loader_returns_none() -> None:
|
|
"""Two deliberately different contracts over the same absence: the fail-fast loader raises (it
|
|
is authoritative startup input, like ``load_ir_projection``), while the OPTIONAL loader the run
|
|
path uses returns ``None`` — a bundle written before the amendment is not an error, it is simply
|
|
un-anchored."""
|
|
with pytest.raises(FileNotFoundError):
|
|
okf.load_cost_baseline(str(PRE_AMENDMENT_BUNDLE))
|
|
assert okf.load_optional_cost_baseline(str(PRE_AMENDMENT_BUNDLE)) is None
|
|
|
|
|
|
def test_malformed_baseline_raises_even_on_the_optional_path(tmp_path) -> None:
|
|
"""Fail-closed where it matters: a baseline that EXISTS but is malformed raises on BOTH loaders.
|
|
Tolerating it would silently un-anchor the gate — the RAW-inbox skip rule stops at this layer."""
|
|
(tmp_path / "cost-baseline.json").write_text(
|
|
json.dumps({"project_id": "P", "items": {"01.1": {"quantity": 1}}}), encoding="utf-8"
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
okf.load_optional_cost_baseline(str(tmp_path))
|
|
|
|
|
|
# --- Arm 4: the run-path wiring (road + bundle) ---------------------------------------------------
|
|
|
|
|
|
def _reply(code: str, quantity: float, unit_cost: float, claimed: float) -> str:
|
|
return json.dumps(
|
|
{
|
|
"measure": "Reduce scope",
|
|
"affected_items": [{"code": code, "quantity": quantity, "unit_cost": unit_cost}],
|
|
"claimed_saving_nok": claimed,
|
|
}
|
|
)
|
|
|
|
|
|
def _factory(reply: str):
|
|
def factory(role: str):
|
|
return SyntheticUsageChatClient(default_reply=reply)
|
|
|
|
return factory
|
|
|
|
|
|
async def test_road_path_anchors_the_gate_to_the_reference_baseline(docs_dir, fresh_store) -> None:
|
|
"""RED (road wiring): a numerically-impeccable fabricated cost line is REJECTED end-to-end
|
|
through ``run_project``. Detach the road-path baseline (stop passing it) and the same run
|
|
returns a ValidatedProposal."""
|
|
result = await run_project(
|
|
"FV42-GSV-E1",
|
|
"local",
|
|
docs_dir=docs_dir,
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_factory(_reply(_FAKE_CODE, 1.0, _FAKE_UNIT_COST, _FAKE_CLAIM)),
|
|
store=fresh_store,
|
|
)
|
|
assert isinstance(result.outcome, Rejection)
|
|
assert "unknown cost code" in result.outcome.reason
|
|
|
|
|
|
async def test_road_path_control_real_line_validates(docs_dir, fresh_store) -> None:
|
|
"""Causality control for the road wiring: the real 05.2 line validates through the same path."""
|
|
result = await run_project(
|
|
"FV42-GSV-E1",
|
|
"local",
|
|
docs_dir=docs_dir,
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_factory(_reply(_REAL_CODE, _REAL_QTY, _REAL_UNIT_COST, _REAL_CLAIM)),
|
|
store=fresh_store,
|
|
)
|
|
assert isinstance(result.outcome, ValidatedProposal)
|
|
|
|
|
|
async def test_bundle_path_anchors_when_the_bundle_declares_a_baseline(fresh_store) -> None:
|
|
"""RED (bundle wiring): a bundle that ships ``cost-baseline.json`` anchors its run — the
|
|
fabricated line is rejected. Detach the bundle-path load and it validates again."""
|
|
result = await run_project(
|
|
"BYGG-ENERGI-BASELINE-MIKRO",
|
|
"local",
|
|
docs_dir=str(BASELINE_BUNDLE),
|
|
bundle_dir=str(BASELINE_BUNDLE),
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_factory(_reply(_FAKE_CODE, 1.0, 300000.0, 90000.0)),
|
|
store=fresh_store,
|
|
)
|
|
assert isinstance(result.outcome, Rejection)
|
|
assert "unknown cost code" in result.outcome.reason
|
|
|
|
|
|
async def test_pre_amendment_bundle_runs_unchanged(fresh_store) -> None:
|
|
"""Control + backward compatibility: an unanchored bundle still runs to a VALIDATED outcome, so
|
|
anchoring stays opt-in per bundle and every pre-S4.0 bundle (including the commons-owned
|
|
goldens) is unaffected.
|
|
|
|
**NARROWED BY P7, deliberately.** This arm used to send the SAME FABRICATED code and assert
|
|
that it validated — which was, stated plainly, the økt-108 hole written down as an expectation:
|
|
with no baseline there was no falsifier for an invented cost line at all. The reply now carries
|
|
a code the delivered base actually names, so the arm proves what it claims (an unanchored run
|
|
still reaches a verdict) without also promising that fabrication clears it. The fabricated half
|
|
lives in ``test_identifier_grounding_loadbearing``."""
|
|
result = await run_project(
|
|
"BYGG-ENERGI-MIKRO-A",
|
|
"local",
|
|
docs_dir=str(PRE_AMENDMENT_BUNDLE),
|
|
bundle_dir=str(PRE_AMENDMENT_BUNDLE),
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_factory(_reply("ENERGI-TOTAL-EL", 1.0, 300000.0, 90000.0)),
|
|
store=fresh_store,
|
|
)
|
|
assert isinstance(result.outcome, ValidatedProposal)
|
|
|
|
|
|
# --- Arm 5: F8 — method caps keyed by config, not the literal measure string ----------------------
|
|
|
|
|
|
def test_method_cap_is_keyed_by_config_not_a_hardcoded_string() -> None:
|
|
"""F8: the method-specific cap comes from a REGISTRY the caller can supply. A caller-configured
|
|
cap for a measure with no built-in entry rejects a proposal the generic P90 stage passes — so
|
|
the rule is keyed by configuration, not by the ``energy_efficiency`` literal."""
|
|
proposal = SavingsProposal(
|
|
project_id="P-ASFALT",
|
|
measure="asfalt_reduction",
|
|
affected_items=[AffectedItem(code="05.2", quantity=100000, unit_cost=1.0)],
|
|
claimed_saving_nok=20000,
|
|
assumptions={},
|
|
)
|
|
assert isinstance(validate_proposal(proposal), ValidatedProposal) # generic P90 = 30000
|
|
capped = validate_proposal(proposal, method_caps={"asfalt_reduction": 0.10})
|
|
assert isinstance(capped, Rejection)
|
|
assert "method cap" in capped.reason
|