seed_store_from_bundle keyed EVERY `type: verdict` file on bundle_candidate_features — the single candidate the bundle's validator-input.json describes. A bundle carrying verdicts about several candidates collapsed them onto one key, so a verdict about candidate B scored a perfect structural match against candidate A's query and could be folded into A's hypothesis prompt. The ExpeL substrate was single-candidate by construction. A verdict file may now carry its own structural key in frontmatter (affected_codes / measure_type / claimed_saving_nok); absent, keying falls back to the bundle candidate, so every pre-S3.2 seed keeps working unchanged. promote_verdict writes the three fields, so a promoted verdict — frequently about a different candidate than the target bundle's projection — does not impersonate that candidate. Semantics decided HERE, not pulled: commons' seeding rule (method-spec §3 Steg 1 + bundle example) has not arrived; we said we would build locally first. D7 mirroring stays open. - ALL THREE fields or none. A partial declaration raises VerdictFrontmatterError rather than merging with the bundle candidate, which would mint a key belonging to NEITHER candidate. Validation, never repair (mirrors write_concept_file); the tolerant-skip rule belongs to the RAW inbox layer. - claimed_saving_nok parses via json.loads — the SAME literal rule the IR projection went through — and is written back with str() of the raw value. _mint_id hashes that value, so 30000 and 30000.0 are different keys; a normalising writer would split one candidate's signal across two ids. - The structural key is signal-free, so it does not weaken the Step-8 no-leak property (Test C green). Load-bearing MEASURED, five mutations all red: detach per-verdict keying · detach the fields promote_verdict writes · make a partial/unparseable key tolerant · normalise the magnitude on write · remove the fallback (control — breaks the step1 suite at collection, proving the fallback bears load). 589 -> 597 tests. Full gate green (pytest, ruff, mypy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkjvTTxrg9LTrmghebfiij
260 lines
12 KiB
Python
260 lines
12 KiB
Python
"""Step-8 load-bearing seam (målbilde §3 / §6 / §7 / §11 step 6): GATED wiki-promotion — when an
|
|
expert/persona APPROVES an outcome, it is promoted from the raw output layer into the context layer
|
|
(the OKF bundle) as a ``type: verdict`` concept file; a NON-approved verdict MUST NOT reach the wiki.
|
|
|
|
The gap (fot-i-bakken): an approved verdict never re-entered the context layer, so the loop learned
|
|
only via injected/dropped seeds. Step 8 adds ``promote_verdict`` — the public, opt-in (R4) promotion
|
|
primitive, deliberately NOT wired into ``run_project`` (the system reads context; the gate promotes).
|
|
|
|
Three load-bearing tests (per the Fase-2 green-but-dead trap):
|
|
- Test A (GATE, §7 mandatory): a rejected verdict — otherwise fully promotable — raises
|
|
``PromotionRefused`` and writes/links NOTHING. Goes RED the moment the gate (the ``raise``) is
|
|
detached: the rejected verdict's file + index link then appear (self-contamination, §6).
|
|
- Test B (NAVIGABLE): an approved verdict is written AND linked so ``navigate_bundle`` reaches it —
|
|
proving promotion is non-decorative (a file the loop cannot navigate never reaches the next run).
|
|
Goes RED if ``link_in_index`` is detached (the promoted file ∉ ``bundle.verdicts``).
|
|
- Test C (NO-LEAK): the promoted verdict's realization marker (0.57 — absent from every bundle file)
|
|
stays OUT of ``bundle_context`` — it reaches a prompt only via the gated ExpeL fold, never the
|
|
read-context. Goes RED if ``promote_verdict`` passes the rationale (not the neutral label) into
|
|
the index, leaking the signal into ``index_summary`` -> context (the BLOCKER the design closes).
|
|
|
|
Transitive coverage: ``test_step1_expel_loadbearing.py`` already proves
|
|
``seed_store_from_bundle`` -> ExpeL fold -> prompt; Test B proves the promoted verdict ∈
|
|
``bundle.verdicts`` (what that seeder reads). Together: promoted approved knowledge reaches the next
|
|
run's hypothesis — proven without re-running ``run_project``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser import okf
|
|
from portfolio_optimiser.verdicts import (
|
|
ExpeLContextProvider,
|
|
ProposalFeatures,
|
|
PromotionRefused,
|
|
Verdict,
|
|
bundle_candidate_features,
|
|
promote_verdict,
|
|
seed_store_from_bundle,
|
|
)
|
|
|
|
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
|
|
|
# A realization marker absent from EVERY bundle file (the seed is 0.82). It can reach a prompt only
|
|
# via the ingest -> ExpeL fold path; in the read-context it must NEVER appear (Test C).
|
|
_MARKER = "realiseringsgrad=0.57"
|
|
_APPROVED_ID = "STEG8-APPROVED"
|
|
|
|
|
|
def _copy_bundle(tmp_path: Path) -> str:
|
|
"""Promotion WRITES into the bundle, so every test works on a throwaway copy — the shared
|
|
framework-neutral fixture is never mutated."""
|
|
dst = tmp_path / "bundle"
|
|
shutil.copytree(BUNDLE_DIR, dst)
|
|
return str(dst)
|
|
|
|
|
|
def _approved_verdict(bundle_dir: str, *, vid: str = _APPROVED_ID) -> Verdict:
|
|
"""An approved verdict keyed on the bundle candidate, carrying the realization marker in its
|
|
rationale — the same key/payload shape the Step-1 fold consumes."""
|
|
return Verdict(
|
|
id=vid,
|
|
proposal_features=bundle_candidate_features(bundle_dir),
|
|
decision="approved",
|
|
rationale=f"LED-retrofit godkjent med realiseringskorreksjon ({_MARKER})",
|
|
)
|
|
|
|
|
|
def _promoted_files(bundle_dir: str) -> list[str]:
|
|
return [p.name for p in Path(bundle_dir).glob("promoted-verdict-*.md")]
|
|
|
|
|
|
# --- Test A: the gate ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_rejected_verdict_is_not_promoted(tmp_path) -> None:
|
|
"""LOAD-BEARING GATE (§7): a NON-approved verdict — otherwise fully promotable (valid id,
|
|
writable bundle) so detaching the gate WOULD produce a file+link — raises ``PromotionRefused``
|
|
and leaves the wiki untouched. RED if the gate is removed."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
index_before = (Path(bundle_dir) / "index.md").read_text(encoding="utf-8")
|
|
rejected = Verdict(
|
|
id="STEG8-REJECTED",
|
|
proposal_features=ProposalFeatures(
|
|
affected_codes=frozenset({"ENERGI-TOTAL-EL"}),
|
|
measure_type="LED-retrofit",
|
|
claimed_saving_nok=30000,
|
|
),
|
|
decision="rejected",
|
|
rationale="expert rejected: realiseringsgapet for stort i denne konteksten",
|
|
)
|
|
|
|
with pytest.raises(PromotionRefused):
|
|
promote_verdict(
|
|
bundle_dir, rejected, approver="persona", experiment="exp-A", timestamp="2026-06-30"
|
|
)
|
|
|
|
assert _promoted_files(bundle_dir) == [], "a rejected verdict must NOT be written to the wiki"
|
|
assert (Path(bundle_dir) / "index.md").read_text(encoding="utf-8") == index_before, (
|
|
"a rejected verdict must NOT be linked into the index"
|
|
)
|
|
|
|
|
|
# --- Test B: positive promotion is navigable ----------------------------------------------------
|
|
|
|
|
|
def test_approved_verdict_is_promoted_and_navigable(tmp_path) -> None:
|
|
"""LOAD-BEARING NAVIGABILITY: an approved verdict is written as a ``type: verdict`` file AND
|
|
linked so ``navigate_bundle`` reaches it (the seeder ``seed_store_from_bundle`` reads exactly
|
|
``bundle.verdicts``). RED if ``link_in_index`` is detached."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
path = promote_verdict(
|
|
bundle_dir,
|
|
_approved_verdict(bundle_dir),
|
|
approver="persona",
|
|
experiment="exp-B",
|
|
timestamp="2026-06-30",
|
|
)
|
|
|
|
assert path.name == "promoted-verdict-STEG8-APPROVED.md"
|
|
fm = okf.parse_frontmatter(path)
|
|
assert fm["type"] == "verdict"
|
|
assert fm["decision"] == "approved"
|
|
assert fm["verdict_id"] == _APPROVED_ID
|
|
assert "persona" in fm["provenance"] and "exp-B" in fm["provenance"]
|
|
|
|
verdict_names = {f.name for f in okf.navigate_bundle(bundle_dir).verdicts}
|
|
assert path.name in verdict_names, (
|
|
"the promoted verdict is not navigable — link_in_index did not make it reachable, so it "
|
|
"could never reach a later run (decorative promotion)"
|
|
)
|
|
|
|
|
|
def test_promote_sanitizes_unsafe_verdict_id(tmp_path) -> None:
|
|
"""A verdict id is read verbatim and can be an arbitrary author string; it must be sanitized
|
|
before it becomes a filename/link, or a ``/`` makes the link unnavigable and ``..`` escapes the
|
|
bundle. The original id is preserved in the ``verdict_id`` frontmatter."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
nasty = _approved_verdict(bundle_dir, vid="../../etc/evil id")
|
|
path = promote_verdict(bundle_dir, nasty, approver="p", experiment="e", timestamp="2026-06-30")
|
|
|
|
assert path.parent == Path(bundle_dir) # written INSIDE the bundle, not escaped
|
|
assert "/" not in path.name.replace("promoted-verdict-", "").replace(".md", "")
|
|
assert path.name in {f.name for f in okf.navigate_bundle(bundle_dir).verdicts}
|
|
assert okf.parse_frontmatter(path)["verdict_id"] == "../../etc/evil id" # original preserved
|
|
|
|
|
|
# --- Test C: the promoted signal does NOT leak into the read-context -----------------------------
|
|
|
|
|
|
def test_promoted_signal_stays_out_of_bundle_context(tmp_path) -> None:
|
|
"""LOAD-BEARING NO-LEAK (§3/§6): after promotion the realization marker is in the bundle (in the
|
|
promoted ``type: verdict`` file) but ABSENT from ``bundle_context`` — it reaches a prompt only
|
|
via the gated ExpeL fold, never the read-context. RED if ``promote_verdict`` passes the rationale
|
|
(not the neutral label) into the index link, leaking the marker into ``index_summary``."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
path = promote_verdict(
|
|
bundle_dir,
|
|
_approved_verdict(bundle_dir),
|
|
approver="persona",
|
|
experiment="exp-C",
|
|
timestamp="2026-06-30",
|
|
)
|
|
|
|
bundle = okf.navigate_bundle(bundle_dir)
|
|
context = okf.bundle_context(bundle)
|
|
assert _MARKER not in context, (
|
|
"the promoted verdict's realization signal leaked into the read-context — promotion must "
|
|
"pass a NEUTRAL index label, so the signal reaches a prompt only via the gated ExpeL fold"
|
|
)
|
|
# though it IS present in the bundle (just excluded from context, like the seed verdict):
|
|
assert _MARKER in okf.parse_frontmatter(path)["description"]
|
|
assert path.name in {f.name for f in bundle.verdicts}
|
|
|
|
|
|
# --- Test D (S3.2): the promoted verdict carries its OWN structural key --------------------------
|
|
|
|
|
|
def test_promoted_verdict_about_another_candidate_keeps_its_own_key(tmp_path) -> None:
|
|
"""S3.2 ROUND-TRIP: a promoted verdict is frequently about a DIFFERENT candidate than the one
|
|
the target bundle's IR projection describes. ``promote_verdict`` therefore writes the verdict's
|
|
own structural key, and ``seed_store_from_bundle`` reads it back — so the promoted verdict does
|
|
not impersonate the bundle candidate in the next run's retrieval.
|
|
|
|
RED if ``promote_verdict`` stops writing the three structural fields (the promoted verdict then
|
|
falls back to the bundle candidate's key and its marker reaches that candidate's prompt)."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
other_marker = "realiseringsgrad=0.19"
|
|
other_candidate = Verdict(
|
|
id="STEG8-OTHER-CANDIDATE",
|
|
proposal_features=ProposalFeatures(
|
|
affected_codes=frozenset({"05.2", "03.1"}),
|
|
measure_type="Redusert asfalttykkelse",
|
|
claimed_saving_nok=900000,
|
|
),
|
|
decision="approved",
|
|
rationale=f"asfalttiltak godkjent med kraftig realiseringskorreksjon ({other_marker})",
|
|
)
|
|
|
|
path = promote_verdict(
|
|
bundle_dir, other_candidate, approver="persona", experiment="exp-D", timestamp="2026-06-30"
|
|
)
|
|
|
|
fm = okf.parse_frontmatter(path)
|
|
assert fm["affected_codes"] == "[03.1, 05.2]" # sorted -> deterministic bytes
|
|
assert fm["measure_type"] == "Redusert asfalttykkelse"
|
|
assert fm["claimed_saving_nok"] == "900000"
|
|
|
|
# The round trip: the next run's seed keys it on ITS candidate, so it does not surface for the
|
|
# bundle's own (LED) candidate.
|
|
store = seed_store_from_bundle(bundle_dir)
|
|
query = bundle_candidate_features(bundle_dir)
|
|
fewshot = ExpeLContextProvider(store, query, k=1).format_fewshot()
|
|
assert other_marker not in fewshot, (
|
|
"a promoted verdict about a different candidate reached this candidate's hypothesis "
|
|
"prompt — the promoted file is not carrying its own structural key:\n" + fewshot
|
|
)
|
|
# Keyed on the promoted candidate's STRUCTURAL fields (``description`` is surface text, outside
|
|
# both the similarity score and the minted id — the seeder fills it from ``measure_type``).
|
|
keys = {
|
|
(v.proposal_features.affected_codes, v.proposal_features.measure_type)
|
|
for v in store.verdicts
|
|
}
|
|
assert (frozenset({"05.2", "03.1"}), "Redusert asfalttykkelse") in keys, (
|
|
f"the promoted verdict was seeded with the wrong structural key; got {keys}"
|
|
)
|
|
|
|
|
|
def test_promotion_round_trip_preserves_the_key_for_the_bundles_own_candidate(tmp_path) -> None:
|
|
"""S3.2 IDENTITY: promoting a verdict about the bundle's OWN candidate must re-seed to exactly
|
|
the key the pre-S3.2 fallback produced — same codes, same measure, same magnitude TYPE.
|
|
|
|
Otherwise a candidate's learning signal splits across two keys over time (the promoted verdicts
|
|
under one, the hand-authored seeds under the fallback), and neither retrieves the other. The
|
|
magnitude is asserted on its exact value AND type because ``_mint_id`` hashes the raw value:
|
|
``30000`` and ``30000.0`` are different ids."""
|
|
bundle_dir = _copy_bundle(tmp_path)
|
|
candidate = bundle_candidate_features(bundle_dir)
|
|
promote_verdict(
|
|
bundle_dir,
|
|
_approved_verdict(bundle_dir),
|
|
approver="persona",
|
|
experiment="exp-E",
|
|
timestamp="2026-06-30",
|
|
)
|
|
|
|
promoted = [v for v in seed_store_from_bundle(bundle_dir).verdicts if _MARKER in v.rationale]
|
|
assert len(promoted) == 1, f"expected exactly the promoted verdict, got {len(promoted)}"
|
|
seeded = promoted[0].proposal_features
|
|
|
|
assert seeded.affected_codes == candidate.affected_codes
|
|
assert seeded.measure_type == candidate.measure_type
|
|
assert seeded.claimed_saving_nok == candidate.claimed_saving_nok
|
|
assert type(seeded.claimed_saving_nok) is type(candidate.claimed_saving_nok), (
|
|
f"magnitude type changed across the round trip: {seeded.claimed_saving_nok!r} vs "
|
|
f"{candidate.claimed_saving_nok!r} — _mint_id hashes the raw value, so this splits the id"
|
|
)
|