Point 2 of the vacuity sweep is now MEASURED, not paired-by-reading. Every one of the 8 got a mutation that detaches the seam it claims to guard, run through a harness that asserts the anchor is unique before mutating, restores in `finally`, and sha256-verifies the restore. Six were value-proven — the negative itself went RED under its detach: hitl :260 load_routing invents a default -> RED hitl :298 route_pending hardcodes a fallback -> RED step7 :145 the is_dir() guard deleted -> RED step7 :171 the §4.2 vocabulary filter deleted -> RED step7 :253 the id grammar off the model -> RED prov :111 sdk_version becomes required -> RED Two did not, and both are fixed here. hitl :197 — the guard it appeared to prove is DEAD. Deleting the `is_dir()` early-out from load_outbox_proposals leaves all 711 tests green: the tolerance comes from `Path.glob`, which yields nothing on a missing directory and never raises. The contrast is the finding: load_inbox carries an identically-shaped guard that IS load-bearing, because it walks with `Path.iterdir`, which DOES raise (measured both ways). Same guard, opposite verdict, and the difference is the stdlib call behind it — the point-3 lesson one level out, where the default being pinned belongs to the standard library rather than the SDK. The stdlib baseline is now anchored explicitly, so a Python that makes glob raise turns this red instead of quietly promoting a dead line to a seam. What the test always did prove is kept and stated: replacing the early-out with a raise turns it red, so it does hold tolerance. portfolio :175 — the negative asserted over an unheld population. Measured, it is real today (6 prompts), so the test is not vacuous now; nothing in it says so, and a run_portfolio that stopped prompting would leave it green while proving nothing. A positive control now runs first. Value-proven: green before, red after the same mutation (iterate no projects), and it is that assertion which fails, not an import. The sibling repo sent the same rule from the other stack this week, arrived at independently via its B4 empty-negative: on a negative assert, prove FIRST that the event happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qr6TwWrHDHeukHy3bL4hgb
229 lines
10 KiB
Python
229 lines
10 KiB
Python
"""Portfolio learning loop — LOAD-BEARING (K3; method-spec §3 Step 1, §5, §11; paritetsrad 5).
|
|
|
|
The seam this file keeps alive: within ONE portfolio pass, a verdict available
|
|
when project k is composed SURVIVES into project k+1's hypothesis prompt. The
|
|
carrier is a SINGLE ``VerdictStore`` threaded through every project (§5: "a
|
|
passed-in store's existing verdicts survive — cross-project threading") plus a
|
|
portfolio-level ``verdict_dir`` (the expert inbox, merged read-only before each
|
|
run's fold). Honesty rule (§1): the crossing carries EXPERT / bundle-seed
|
|
verdicts only — never a run's own captured output. The role split is unwaivable
|
|
(§3 Step 7): the portfolio READS ``verdict_dir``; it NEVER writes a run's verdict
|
|
back into it, and the promotion gate / authoring primitive remain the only
|
|
writers.
|
|
|
|
Detach proofs (each restored from a copy of the implemented version, never
|
|
``git checkout`` — that would go to HEAD and erase the uncommitted src):
|
|
|
|
* Shared store (§5, cross-project threading): make ``compose_run_context`` ignore
|
|
its passed-in ``store`` and always build a fresh one (``store = VerdictStore()``
|
|
unconditionally) → project 2 seeds only its OWN bundle → project 1's seed id no
|
|
longer reaches project 2's prompt → ``test_earlier_project_seed_survives_into_
|
|
later_project`` goes red.
|
|
* Inbox threading (§5): drop the ``verdict_dir`` merge in ``run_portfolio`` (or stop
|
|
passing the shared store into ``compose_run_context``) → the portfolio inbox
|
|
verdict never reaches any fold → ``test_portfolio_inbox_verdict_folds_into_
|
|
project_prompt`` goes red.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from _scripted import ScriptedClient, reply
|
|
|
|
from portfolio_optimiser_claude.budget import BudgetMeter
|
|
from portfolio_optimiser_claude.contracts import (
|
|
ReferenceProjectsContract,
|
|
TerminationContract,
|
|
load_reference_projects,
|
|
)
|
|
from portfolio_optimiser_claude.experience import (
|
|
CandidateFeatures,
|
|
mint_verdict_id,
|
|
)
|
|
from portfolio_optimiser_claude.inbox import VerdictDocument, write_verdict
|
|
from portfolio_optimiser_claude.ir import load_validator_input
|
|
from portfolio_optimiser_claude.portfolio import run_portfolio
|
|
|
|
_REPO = Path(__file__).resolve().parents[1]
|
|
LED_BUNDLE = _REPO / "shared" / "examples" / "bygg-energi-mikro"
|
|
VFD_BUNDLE = _REPO / "tests" / "data" / "mini-bundle"
|
|
|
|
VFD_MARKER = "K2-VFD-CONTEXT-MARKER" # in the repo-local fixture's rendered context
|
|
# A token that lives NOWHERE in either bundle — its presence in a prompt can only
|
|
# come from the portfolio ``verdict_dir`` fold, never from context or seeding.
|
|
INBOX_MARKER = "K3-PORTFOLIO-LEARNING-MARKER"
|
|
|
|
|
|
def _meter(*, max_rounds: int = 100, max_tokens: int = 10_000) -> BudgetMeter:
|
|
return BudgetMeter(TerminationContract(max_rounds=max_rounds, max_tokens=max_tokens))
|
|
|
|
|
|
def _happy_replies(bundles: list[Path]) -> list[object]:
|
|
"""Three scripted replies per project: debate reasoning → APPROVE → candidate JSON."""
|
|
replies: list[object] = []
|
|
for bundle in bundles:
|
|
replies += [
|
|
reply("debate reasoning"),
|
|
reply("VERDICT: APPROVE"),
|
|
reply(json.dumps(load_validator_input(bundle).model_dump())),
|
|
]
|
|
return replies
|
|
|
|
|
|
def _config(entries: list[tuple[str, Path]]) -> ReferenceProjectsContract:
|
|
return load_reference_projects(
|
|
{"projects": [{"project_id": pid, "bundle_dir": str(path)} for pid, path in entries]}
|
|
)
|
|
|
|
|
|
def _seed_id(bundle: Path) -> str:
|
|
"""The id the bundle's ``type: verdict`` seed is keyed on (§3 Step 1 / §4.2)."""
|
|
return mint_verdict_id(CandidateFeatures.from_proposal(load_validator_input(bundle)))
|
|
|
|
|
|
def _author_inbox_verdict(verdict_dir: Path, marker: str) -> VerdictDocument:
|
|
"""Drop an EXPERT verdict into the portfolio inbox (§5 write side, expert-authored).
|
|
|
|
Keyed on features similar to the VFD candidate (same code + measure type, a
|
|
distinct saving so its id does not collide with the VFD seed), so it ranks
|
|
high for the VFD project. The marker rides in the rationale — the fold's
|
|
learning-signal carrier (§3 Step 1).
|
|
"""
|
|
vfd = CandidateFeatures.from_proposal(load_validator_input(VFD_BUNDLE))
|
|
features = CandidateFeatures(
|
|
affected_codes=vfd.affected_codes,
|
|
measure_type=vfd.measure_type,
|
|
claimed_saving_nok=vfd.claimed_saving_nok + 3000.0, # same bucket, distinct id
|
|
)
|
|
verdict = VerdictDocument.from_candidate(
|
|
features,
|
|
decision="approved",
|
|
rationale=f"prior portfolio verdict — realiseringskorreksjon [{marker}]",
|
|
description="K3 portfolio inbox fixture (surface text, excluded from ranking)",
|
|
)
|
|
write_verdict(verdict_dir, verdict)
|
|
return verdict
|
|
|
|
|
|
class TestCrossProjectThreading:
|
|
"""§5 cross-project threading: a passed-in store's verdicts survive into the next run.
|
|
|
|
The shared ``VerdictStore`` ``run_portfolio`` owns carries project 1's bundle
|
|
seed forward, so it is still retrievable when project 2 composes — the
|
|
portfolio learns across projects in a single pass (S2.0, paritetsrad 5).
|
|
"""
|
|
|
|
def test_earlier_project_seed_survives_into_later_project(self) -> None:
|
|
projects = _config([("BYGG-KONTOR-NORD", LED_BUNDLE), ("PUMPE-SOR", VFD_BUNDLE)])
|
|
client = ScriptedClient(replies=_happy_replies([LED_BUNDLE, VFD_BUNDLE]))
|
|
run_portfolio(projects, client, _meter(), top_k=3, max_debate_rounds=3, max_attempts=3)
|
|
|
|
led_seed_id = _seed_id(LED_BUNDLE)
|
|
vfd_prompts = [p for p in client.prompts("proposer") if VFD_MARKER in p]
|
|
assert vfd_prompts, "project 2's own context must reach its prompts"
|
|
# The shared store carried project 1's seed into project 2's fold.
|
|
assert any(led_seed_id in p for p in vfd_prompts), (
|
|
"project 1's seed must survive into project 2's prompt via the shared store"
|
|
)
|
|
|
|
def test_threading_is_order_directional(self) -> None:
|
|
# Reverse the config: now VFD is project 1 → its seed threads into LED's prompt,
|
|
# and LED's seed (project 2, composed last) is absent from project 1's prompt.
|
|
led_seed_id = _seed_id(LED_BUNDLE)
|
|
vfd_seed_id = _seed_id(VFD_BUNDLE)
|
|
projects = _config([("PUMPE-SOR", VFD_BUNDLE), ("BYGG-KONTOR-NORD", LED_BUNDLE)])
|
|
client = ScriptedClient(replies=_happy_replies([VFD_BUNDLE, LED_BUNDLE]))
|
|
run_portfolio(projects, client, _meter(), top_k=3, max_debate_rounds=3, max_attempts=3)
|
|
|
|
vfd_prompts = [p for p in client.prompts("proposer") if VFD_MARKER in p]
|
|
assert vfd_prompts
|
|
# Project 1 (VFD) ran before LED was ever seeded → LED's seed cannot appear.
|
|
assert not any(led_seed_id in p for p in vfd_prompts), (
|
|
"a later project's seed must not reach an earlier project's prompt"
|
|
)
|
|
# VFD's own seed is of course present in its own fold.
|
|
assert any(vfd_seed_id in p for p in vfd_prompts)
|
|
|
|
|
|
class TestPortfolioInboxFold:
|
|
"""A portfolio-level ``verdict_dir`` (expert inbox) folds into each project (§5, §3 Step 1)."""
|
|
|
|
def test_portfolio_inbox_verdict_folds_into_project_prompt(self, tmp_path: Path) -> None:
|
|
verdict_dir = tmp_path / "portfolio-inbox"
|
|
inbox_verdict = _author_inbox_verdict(verdict_dir, INBOX_MARKER)
|
|
projects = _config([("BYGG-KONTOR-NORD", LED_BUNDLE), ("PUMPE-SOR", VFD_BUNDLE)])
|
|
client = ScriptedClient(replies=_happy_replies([LED_BUNDLE, VFD_BUNDLE]))
|
|
run_portfolio(
|
|
projects,
|
|
client,
|
|
_meter(),
|
|
top_k=3,
|
|
max_debate_rounds=3,
|
|
max_attempts=3,
|
|
verdict_dir=verdict_dir,
|
|
)
|
|
vfd_prompts = [p for p in client.prompts("proposer") if VFD_MARKER in p]
|
|
assert vfd_prompts
|
|
assert any(INBOX_MARKER in p and inbox_verdict.id in p for p in vfd_prompts), (
|
|
"the portfolio inbox verdict (id + marker) must reach the project's fold"
|
|
)
|
|
|
|
def test_no_verdict_dir_leaves_the_marker_absent(self) -> None:
|
|
# Control: without a portfolio inbox, the marker appears in NO prompt — it
|
|
# has no other channel into the context (seeding and bundle context never
|
|
# carry it).
|
|
projects = _config([("BYGG-KONTOR-NORD", LED_BUNDLE), ("PUMPE-SOR", VFD_BUNDLE)])
|
|
client = ScriptedClient(replies=_happy_replies([LED_BUNDLE, VFD_BUNDLE]))
|
|
run_portfolio(projects, client, _meter(), top_k=3, max_debate_rounds=3, max_attempts=3)
|
|
# Positive control FIRST — prove the event happened before asserting it
|
|
# carried nothing. A negative over an EMPTY population is green for the
|
|
# wrong reason: were run_portfolio to stop prompting the model at all,
|
|
# the assertion below would keep passing while proving nothing about
|
|
# the marker's channel. Measured today: 6 prompts.
|
|
assert client.calls, "no prompt was issued — the negative below would be vacuous"
|
|
assert not any(INBOX_MARKER in prompt for _role, prompt in client.calls)
|
|
|
|
|
|
class TestPortfolioMergeIdempotency:
|
|
"""§4.2 first-write-wins: repeated portfolio merges are idempotent (double-merge, §5).
|
|
|
|
``verdict_dir`` is merged before EVERY project's fold. Across a three-project
|
|
pass the same inbox verdict is merged three times, yet the shared store holds
|
|
it exactly ONCE — its fold line is not duplicated.
|
|
"""
|
|
|
|
def test_repeated_merges_keep_a_single_store_entry(self, tmp_path: Path) -> None:
|
|
verdict_dir = tmp_path / "portfolio-inbox"
|
|
inbox_verdict = _author_inbox_verdict(verdict_dir, INBOX_MARKER)
|
|
projects = _config(
|
|
[
|
|
("BYGG-KONTOR-NORD", LED_BUNDLE),
|
|
("PUMPE-SOR", VFD_BUNDLE),
|
|
("BYGG-KONTOR-NORD-2", LED_BUNDLE),
|
|
]
|
|
)
|
|
client = ScriptedClient(replies=_happy_replies([LED_BUNDLE, VFD_BUNDLE, LED_BUNDLE]))
|
|
run_portfolio(
|
|
projects,
|
|
client,
|
|
_meter(),
|
|
top_k=3,
|
|
max_debate_rounds=3,
|
|
max_attempts=3,
|
|
verdict_dir=verdict_dir,
|
|
)
|
|
vfd_prompts = [p for p in client.prompts("proposer") if VFD_MARKER in p]
|
|
assert vfd_prompts
|
|
# The verdict was merged three times (once before each project) but appears
|
|
# once per fold — first-write-wins deduplicates on id.
|
|
assert all(p.count(inbox_verdict.id) == 1 for p in vfd_prompts), (
|
|
"a verdict merged N times must fold exactly once (first-write-wins, §4.2)"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
raise SystemExit(pytest.main([__file__, "-q"]))
|