From cebba7d954ea9631264cb9be8cbb285345316169 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 3 Jul 2026 01:10:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(shared-root):=20S3=20=E2=80=94=20configura?= =?UTF-8?q?ble=20shared-root=20resolver=20with=20load-bearing=20override?= =?UTF-8?q?=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One call-time resolver (env PORTFOLIO_SHARED_ROOT, default the in-repo shared/) consumed by both MAF-side readers of the shared core: persona._example_path() (the _EXAMPLE_PATH monkeypatch seam is kept) and simulation._default_bundle_dir() (replaces the _BUNDLE_DIR module global). De-risks the S4 extraction: re-pointing the commons becomes an env var, not a code change. Override test proves the marker follows a tmp copy of the whole shared tree; both detach points proven RED. Suite 155->157. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi --- src/portfolio_optimiser/persona.py | 30 +++++++------ src/portfolio_optimiser/shared_root.py | 23 ++++++++++ src/portfolio_optimiser/simulation.py | 11 ++++- tests/test_shared_root_loadbearing.py | 61 ++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 src/portfolio_optimiser/shared_root.py create mode 100644 tests/test_shared_root_loadbearing.py diff --git a/src/portfolio_optimiser/persona.py b/src/portfolio_optimiser/persona.py index 477d659..647e0ce 100644 --- a/src/portfolio_optimiser/persona.py +++ b/src/portfolio_optimiser/persona.py @@ -17,16 +17,20 @@ import json from dataclasses import dataclass from pathlib import Path -# Module-global so tests can monkeypatch it; read at CALL time inside the loader (never frozen into -# a default argument), which is what makes the simulation's persona genuinely artifact-driven. -_EXAMPLE_PATH = ( - Path(__file__).resolve().parents[2] - / "shared" - / "skills" - / "expert-reviewer" - / "references" - / "example-verdict.json" -) +from portfolio_optimiser.shared_root import shared_root + +# Test seam: when set (monkeypatched), wins over the resolver. Read at CALL time inside the loader +# (never frozen into a default argument), which is what makes the simulation's persona genuinely +# artifact-driven. +_EXAMPLE_PATH: Path | None = None + +_EXAMPLE_SUBPATH = Path("skills") / "expert-reviewer" / "references" / "example-verdict.json" + + +def _example_path() -> Path: + """The persona example's location: the test seam if set, else resolved under ``shared_root()`` + (env ``PORTFOLIO_SHARED_ROOT`` re-points it — the S4 extraction seam).""" + return _EXAMPLE_PATH if _EXAMPLE_PATH is not None else shared_root() / _EXAMPLE_SUBPATH @dataclass(frozen=True) @@ -42,9 +46,9 @@ class PersonaExample: def load_persona_example() -> PersonaExample: """Read the persona's canonical example verdict. Fail-fast: a missing file raises - ``FileNotFoundError`` and a missing key raises ``KeyError`` (required input). Reads - ``_EXAMPLE_PATH`` at call time so the path is patchable.""" - data = json.loads(_EXAMPLE_PATH.read_text(encoding="utf-8")) + ``FileNotFoundError`` and a missing key raises ``KeyError`` (required input). Resolves the + path at call time (``_example_path``) so both the test seam and the env override are live.""" + data = json.loads(_example_path().read_text(encoding="utf-8")) return PersonaExample( decision=data["decision"], rationale=data["rationale"], diff --git a/src/portfolio_optimiser/shared_root.py b/src/portfolio_optimiser/shared_root.py new file mode 100644 index 0000000..b6ac9e9 --- /dev/null +++ b/src/portfolio_optimiser/shared_root.py @@ -0,0 +1,23 @@ +"""Resolver for the location of the shared framework-neutral core (S3, R1-forberedelse). + +``shared/`` is today an in-repo directory, but is slated for extraction into its own commons repo +(R1/S4). Every MAF-side consumer resolves its location through this ONE seam so the extraction is +a re-point (env var), not a code change. Pure stdlib — the shared core itself stays framework-free. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +ENV_VAR = "PORTFOLIO_SHARED_ROOT" + +_DEFAULT = Path(__file__).resolve().parents[2] / "shared" + + +def shared_root() -> Path: + """Resolve the shared-core root at CALL time: ``PORTFOLIO_SHARED_ROOT`` if set (non-empty), + else the in-repo default. The call-time read is what keeps the override testable and the S4 + extraction re-pointable without touching consumers.""" + override = os.environ.get(ENV_VAR) + return Path(override) if override else _DEFAULT diff --git a/src/portfolio_optimiser/simulation.py b/src/portfolio_optimiser/simulation.py index fc0fbd7..894f5c4 100644 --- a/src/portfolio_optimiser/simulation.py +++ b/src/portfolio_optimiser/simulation.py @@ -38,12 +38,19 @@ from agent_framework_openai import OpenAIChatCompletionClient from portfolio_optimiser.persona import load_persona_example from portfolio_optimiser.run import RunResult, run_project +from portfolio_optimiser.shared_root import shared_root from portfolio_optimiser.validator import ValidatedProposal from portfolio_optimiser.verdicts import VerdictStore, promote_verdict, seed_store_from_bundle -_BUNDLE_DIR = Path(__file__).resolve().parents[2] / "shared" / "examples" / "bygg-energi-mikro" _PROJECT_ID = "BYGG-KONTOR-NORD" + +def _default_bundle_dir() -> Path: + """The demo bundle under the shared core, resolved at CALL time via ``shared_root()`` (env + ``PORTFOLIO_SHARED_ROOT`` re-points it — the S4 extraction seam).""" + return shared_root() / "examples" / "bygg-energi-mikro" + + # A VALID SavingsProposal for BYGG-KONTOR-NORD: total = 300000 x 1.0, P90 = 0.30 x 300000 = 90000, # claimed 30000 <= 90000 -> validates on the first attempt (no `assumptions` -> degenerate MC). _VALID_PROPOSAL = ( @@ -246,7 +253,7 @@ 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(_BUNDLE_DIR), work)) + result = asyncio.run(simulate_learning_loop(str(_default_bundle_dir()), work)) print("=" * 78) print("OFFLINE SIMULATION — scripted agent replies, NO real model.") diff --git a/tests/test_shared_root_loadbearing.py b/tests/test_shared_root_loadbearing.py new file mode 100644 index 0000000..fcee5d8 --- /dev/null +++ b/tests/test_shared_root_loadbearing.py @@ -0,0 +1,61 @@ +"""Load-bearing tests for the configurable shared-root resolver (S3, R1-forberedelse). + +Both MAF-side consumers of the shared framework-neutral core resolve its location through ONE +seam — ``shared_root()`` (env ``PORTFOLIO_SHARED_ROOT``, default: the in-repo ``shared/``) — read +at CALL time. That seam is what makes the S4 extraction (``shared/`` -> commons repo) a re-point, +not a code change. The override test goes RED the moment a consumer stops resolving through the +seam: a hardcoded path keeps reading the repo tree, so the tmp copy's swapped marker never +surfaces. +""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +from portfolio_optimiser import persona, simulation +from portfolio_optimiser.shared_root import shared_root +from portfolio_optimiser.simulation import simulate_learning_loop + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def test_shared_root_defaults_to_in_repo_tree(monkeypatch) -> None: + """Without the env override, the resolver yields today's in-repo ``shared/`` — the S3 change is + behaviour-preserving for every existing caller.""" + monkeypatch.delenv("PORTFOLIO_SHARED_ROOT", raising=False) + assert shared_root() == REPO_ROOT / "shared" + + +async def test_shared_root_override_redirects_both_consumers(tmp_path: Path, monkeypatch) -> None: + """Point ``PORTFOLIO_SHARED_ROOT`` at a tmp COPY of ``shared/`` carrying a swapped persona + marker and confirm (a) the persona loader reads the copy, (b) the simulation's default bundle + resolves under the copy, and (c) the end-to-end sim traces the COPY's marker across the + promotion loop. RED without the resolver: the env var is ignored and the repo artifact's + marker is read instead.""" + copy_root = tmp_path / "shared-copy" + shutil.copytree(REPO_ROOT / "shared", copy_root) + + swapped_marker = "realiseringsgrad=0.57" + example_path = copy_root / "skills" / "expert-reviewer" / "references" / "example-verdict.json" + data = json.loads(example_path.read_text(encoding="utf-8")) + assert data["marker"] != swapped_marker, "control: the swap must differ from the repo artifact" + data["marker"] = swapped_marker + data["rationale"] = f"Godkjent. I drift realiseres erfaringsvis ~57% ({swapped_marker})." + example_path.write_text(json.dumps(data), encoding="utf-8") + + monkeypatch.setenv("PORTFOLIO_SHARED_ROOT", str(copy_root)) + + example = persona.load_persona_example() + assert example.marker == swapped_marker, "persona loader did not resolve via shared_root()" + + bundle_dir = simulation._default_bundle_dir() + assert bundle_dir == copy_root / "examples" / "bygg-energi-mikro", ( + "simulation default bundle did not resolve via shared_root()" + ) + + result = await simulate_learning_loop(str(bundle_dir), str(tmp_path / "work")) + assert result.marker == swapped_marker + assert not result.marker_in_run_a_prompt, "swapped marker leaked into Run A (causality broken)" + assert result.marker_in_run_b_prompt, "the copy's marker did not cross into Run B's prompt"