The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 lisensomfang",
|
|
claimed_saving_nok=900000,
|
|
),
|
|
decision="approved",
|
|
rationale=f"lisenstiltak 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 lisensomfang"
|
|
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 lisensomfang") 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"
|
|
)
|