feat(simulation): the demo's gate is anchored to real cost lines (P4 pkt. 0)
The validator can reconcile a proposal against the project's actual cost lines (S4.0 stage 0), but only when the knowledge base ships a cost-baseline.json — and no bundle under shared/ has one. So on stage the gate reasoned only about numbers the proposal supplied itself. The reserve can never receive the file in shared/ (pull-only subtree, and demo criterion 8 requires the goldens byte-unchanged). That is a placement constraint, not an impossibility: materialize_anchored_bundle copies the bundle and adds the file outside shared/, and the run path reads it through exactly the seam a delivered bundle would use. The baseline is DERIVED IN CODE from the scripted register, never typed beside it — two sources of the same numbers drift, and drift is precisely what the 10 % probe models. On GO day the direction reverses (plan P3 b). Both scripted replies must state the same cost lines or ValueError: were they to differ, hypothesis #1 would be falsified by stage 0 instead of by P90 — the same REJECTED line on screen, a different mechanism behind it. 10 % probe, measured: baseline x 1.10 -> FORKASTET at stage 0, before the solver; corrected -> FORESLÅTT. Criterion 6 re-measured (stdout byte-identical across two runs); stderr unchanged at 6 lines. The ONLY diff against the un-anchored demo is the new KUNNSKAPSBASE block — everything else is byte-identical, which is the problem: an anchoring nobody can see is one nobody can check. Hence it is printed, and hence `provenance` is a required argument. 769 -> 775 passed. Five mutations red + green control. The measurement failed the TEST first: "ingen kostbaseline erklært" CONTAINS "kostbaseline erklært", and ENERGI-TOTAL-EL already appears in the Step-2 line, so both assertions survived the detach mutation. The two branches now share no wording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GD6Y2Y23NZZxPYtSRoCmst
This commit is contained in:
parent
1e11dcb96c
commit
1522e2aaaa
5 changed files with 364 additions and 4 deletions
|
|
@ -20,6 +20,7 @@ client is MAF-side scaffolding; it is NOT part of the framework-neutral ``shared
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -36,7 +37,8 @@ from agent_framework import (
|
|||
)
|
||||
from agent_framework_openai import OpenAIChatCompletionClient
|
||||
|
||||
from portfolio_optimiser.ir import SavingsProposal
|
||||
from portfolio_optimiser import okf
|
||||
from portfolio_optimiser.ir import AffectedItem, CostBaseline, CostBaselineLine, SavingsProposal
|
||||
from portfolio_optimiser.persona import load_persona_example
|
||||
from portfolio_optimiser.run import RunResult, run_project
|
||||
from portfolio_optimiser.shared_root import shared_root
|
||||
|
|
@ -167,6 +169,89 @@ _CANDIDATES: tuple[ScriptedCandidate, ...] = (
|
|||
|
||||
_proposer_reply = scripted_proposer(_CANDIDATES)
|
||||
|
||||
# The filename ``okf``'s loaders default to. Kept local rather than reaching into ``okf``'s private
|
||||
# constant; the coupling is measured, not assumed — a drifted name makes the materialized bundle
|
||||
# un-anchored, which ``test_anchored_reserve_loadbearing`` turns red.
|
||||
_COST_BASELINE_FILE = "cost-baseline.json"
|
||||
_ANCHORED_DIR_NAME = "forankret-reserve"
|
||||
_RESERVE_PROVENANCE = "tallene er syntetiske — avledet av demo-manuset, ikke levert av et fagmiljø"
|
||||
|
||||
|
||||
def baseline_from_scripted_candidate(candidate: ScriptedCandidate) -> CostBaseline:
|
||||
"""Derive a project's cost baseline FROM the scripted register's own cost lines (P4 pkt. 0).
|
||||
|
||||
The reserve bundle's numbers are synthetic, so the script is the only ground truth there is;
|
||||
deriving in code rather than typing the same numbers into a second file is what keeps the two
|
||||
from drifting apart. **On GO day the direction reverses** (plan P3 b): the register is written
|
||||
FROM the delivered ``cost-baseline.json``, and this function is not used.
|
||||
|
||||
Both scripted replies must state the SAME cost lines, or ``ValueError``. Validation, never
|
||||
repair: were they to differ, hypothesis #1 would be falsified by the reconciliation stage
|
||||
instead of by the P90 stage, and the demo's REJECTED line would come from another mechanism
|
||||
than the one it narrates — visible on screen as the same line either way.
|
||||
"""
|
||||
lines = {
|
||||
item.code: CostBaselineLine(quantity=item.quantity, unit_cost=item.unit_cost)
|
||||
for item in (
|
||||
AffectedItem.model_validate(raw)
|
||||
for raw in json.loads(candidate.corrected)["affected_items"]
|
||||
)
|
||||
}
|
||||
overclaimed = {
|
||||
raw["code"]: (raw["quantity"], raw["unit_cost"])
|
||||
for raw in json.loads(candidate.overclaimed)["affected_items"]
|
||||
}
|
||||
if overclaimed != {code: (line.quantity, line.unit_cost) for code, line in lines.items()}:
|
||||
raise ValueError(
|
||||
f"scripted candidate {candidate.project_id} states different cost lines in its two "
|
||||
"replies; a baseline derived from one of them would falsify the other at stage 0"
|
||||
)
|
||||
return CostBaseline(project_id=candidate.project_id, items=lines)
|
||||
|
||||
|
||||
def _reserve_baseline() -> CostBaseline:
|
||||
"""The reserve bundle's baseline: the registry entry for the reserve's project, never
|
||||
``_CANDIDATES[0]`` — the registry is a set of DATA entries whose order carries no meaning, and
|
||||
an index would silently anchor the demo to another project once a second entry lands."""
|
||||
(candidate,) = [c for c in _CANDIDATES if c.project_id == _PROJECT_ID]
|
||||
return baseline_from_scripted_candidate(candidate)
|
||||
|
||||
|
||||
def materialize_anchored_bundle(
|
||||
dest: str | Path,
|
||||
*,
|
||||
source: str | Path | None = None,
|
||||
baseline: CostBaseline | None = None,
|
||||
) -> Path:
|
||||
"""Copy the reserve bundle and ADD the ``cost-baseline.json`` it cannot be given in place — the
|
||||
copy-and-extend variant that anchors the deterministic gate (S4.0 stage 0) for the demo.
|
||||
|
||||
``shared/`` is a pull-only subtree and demo criterion 8 requires the commons-owned goldens
|
||||
byte-unchanged, so the reserve can never ship the file itself. That is a PLACEMENT constraint,
|
||||
not an impossibility: the run already reads the baseline from whichever bundle directory it is
|
||||
handed (``run.py`` -> ``okf.load_optional_cost_baseline``), so an extended copy outside
|
||||
``shared/`` is anchored by exactly the same seam a delivered bundle would use.
|
||||
|
||||
``baseline`` defaults to the one derived from the scripted register; a caller passes its own to
|
||||
model a DELIVERED baseline that disagrees with the script (the 10 % test).
|
||||
"""
|
||||
src = Path(source) if source is not None else _default_bundle_dir()
|
||||
out = Path(dest)
|
||||
shutil.copytree(src, out)
|
||||
resolved = baseline if baseline is not None else _reserve_baseline()
|
||||
payload = {
|
||||
"_note": (
|
||||
"SYNTHETIC cost baseline, materialized for the demo (P4 pkt. 0) — NOT delivered data. "
|
||||
"Derived from the scripted register in portfolio_optimiser.simulation so the two "
|
||||
"cannot drift. The source bundle is never modified."
|
||||
),
|
||||
**resolved.model_dump(),
|
||||
}
|
||||
(out / _COST_BASELINE_FILE).write_text(
|
||||
json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# The checker's debate turn ends with the gate marker the run parses (run._checker_verdict).
|
||||
_CHECKER_APPROVE = "Tallene er innenfor feasibelt område og resonnementet holder. VERDICT: APPROVE"
|
||||
|
|
@ -550,6 +635,37 @@ def _run_trace_lines(result: RunResult, *, marker: str, marker_in_prompt: bool)
|
|||
return lines
|
||||
|
||||
|
||||
def _baseline_lines(bundle_dir: Path, provenance: str) -> list[str]:
|
||||
"""What the knowledge base DECLARES about the project's own cost lines.
|
||||
|
||||
Read off the BUNDLE, not off a ``RunResult`` — which is why it is printed by ``main`` and not by
|
||||
``_run_trace_lines`` (whose contract is that every value comes from the run). Whether the run
|
||||
then USES the baseline is not something a screen can show: every other line of the demo is
|
||||
byte-identical anchored or not, so that property is measured by
|
||||
``tests/test_anchored_reserve_loadbearing.py`` instead.
|
||||
|
||||
``provenance`` is a required argument, not a default: the caller who chooses the bundle is the
|
||||
only one who knows where its numbers came from, and saying so is the honesty claim itself.
|
||||
|
||||
The two branches share no wording (measured: an "ingen kostbaseline erklært" phrasing CONTAINS
|
||||
"kostbaseline erklært", which made the entry-point test pass with the anchoring detached)."""
|
||||
baseline = okf.load_optional_cost_baseline(str(bundle_dir))
|
||||
if baseline is None:
|
||||
return [
|
||||
f"KUNNSKAPSBASE: {bundle_dir.name} — uten kostbaseline",
|
||||
" validatoren regner kun på tallene forslaget selv oppgir",
|
||||
]
|
||||
items = ", ".join(
|
||||
f"{code} {line.quantity:g} x {line.unit_cost:g}"
|
||||
for code, line in sorted(baseline.items.items())
|
||||
)
|
||||
return [
|
||||
f"KUNNSKAPSBASE: {bundle_dir.name} — kostbaseline erklært ({items})",
|
||||
" validatorens stage 0 avstemmer forslagets kostlinjer mot disse, FØR løseren",
|
||||
f" {provenance}",
|
||||
]
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int: # pragma: no cover - console trace
|
||||
"""Run the simulation against the energi bundle in a throwaway temp dir and print an honest,
|
||||
readable trace. Invoke: ``uv run python -m portfolio_optimiser.simulation``."""
|
||||
|
|
@ -558,7 +674,12 @@ def main(argv: list[str] | None = None) -> int: # pragma: no cover - console tr
|
|||
import tempfile
|
||||
|
||||
work = tempfile.mkdtemp(prefix="po-sim-")
|
||||
result = asyncio.run(simulate_learning_loop(str(_default_bundle_dir()), work))
|
||||
# THE call site (P4 pkt. 0): the demo runs the ANCHORED reserve — the shared bundle plus the
|
||||
# cost baseline it cannot be given in place. On GO day these two lines point at the delivered
|
||||
# bundle and state ITS provenance instead; the run path's seam is the same either way.
|
||||
bundle = materialize_anchored_bundle(Path(work) / _ANCHORED_DIR_NAME)
|
||||
provenance = _RESERVE_PROVENANCE
|
||||
result = asyncio.run(simulate_learning_loop(str(bundle), work))
|
||||
|
||||
print("=" * 78)
|
||||
print("OFFLINE SIMULERING — skriptede agent-svar, INGEN ekte modell.")
|
||||
|
|
@ -566,6 +687,10 @@ def main(argv: list[str] | None = None) -> int: # pragma: no cover - console tr
|
|||
print("Beviser IKKE at en levende modell ville produsert dette — forslag og dom er skriptet.")
|
||||
print("=" * 78)
|
||||
|
||||
print()
|
||||
for line in _baseline_lines(bundle, provenance):
|
||||
print(line)
|
||||
|
||||
# Run A walks steps 1-7 of the method; the promotion between the runs IS step 8. Run B is not
|
||||
# re-numbered — it re-runs the same eight steps, and what the demo needs from it is the ONE
|
||||
# thing that changed: the marker now reaches the hypothesis prompt.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue