From c7c42eeb753ffc5e61033be0527ef69a2118b5d2 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 26 Jun 2026 11:55:21 +0200 Subject: [PATCH] feat(fase3): config-driven docs_dir + verdict_input on Project loader --- .../data/docs/BRU-LAKS-REHAB/notes.txt | 5 +++++ .../data/docs/FV42-GSV-E1/notes.txt | 5 +++++ .../data/docs/RV13-RAS-TP/notes.txt | 5 +++++ src/portfolio_optimiser/data/reference_projects.json | 8 +++++++- src/portfolio_optimiser/reference_domain.py | 11 ++++++++++- tests/test_reference_domain.py | 12 ++++++++++++ 6 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/portfolio_optimiser/data/docs/BRU-LAKS-REHAB/notes.txt create mode 100644 src/portfolio_optimiser/data/docs/FV42-GSV-E1/notes.txt create mode 100644 src/portfolio_optimiser/data/docs/RV13-RAS-TP/notes.txt diff --git a/src/portfolio_optimiser/data/docs/BRU-LAKS-REHAB/notes.txt b/src/portfolio_optimiser/data/docs/BRU-LAKS-REHAB/notes.txt new file mode 100644 index 0000000..ada066f --- /dev/null +++ b/src/portfolio_optimiser/data/docs/BRU-LAKS-REHAB/notes.txt @@ -0,0 +1,5 @@ +SYNTHETIC fixture (D4) — AI-authored, not verified domain content. + +Cost saving measure candidate for Bru over Lakselva (rehabilitering): +Reducing the scope on the concrete-repair area lowers the cost. Scope reduction on the +rehabilitation works is the candidate cost-saving measure to validate. diff --git a/src/portfolio_optimiser/data/docs/FV42-GSV-E1/notes.txt b/src/portfolio_optimiser/data/docs/FV42-GSV-E1/notes.txt new file mode 100644 index 0000000..32bf8e2 --- /dev/null +++ b/src/portfolio_optimiser/data/docs/FV42-GSV-E1/notes.txt @@ -0,0 +1,5 @@ +SYNTHETIC fixture (D4) — AI-authored, not verified domain content. + +Cost saving measure candidate for Fv. 42 gang- og sykkelveg (etappe 1): +A unit-rate renegotiation on code 05.2 (Asfalt Ab11) reduces the paving cost on this +stretch. Reducing the asphalt scope is the candidate cost-saving measure to validate. diff --git a/src/portfolio_optimiser/data/docs/RV13-RAS-TP/notes.txt b/src/portfolio_optimiser/data/docs/RV13-RAS-TP/notes.txt new file mode 100644 index 0000000..5ee5104 --- /dev/null +++ b/src/portfolio_optimiser/data/docs/RV13-RAS-TP/notes.txt @@ -0,0 +1,5 @@ +SYNTHETIC fixture (D4) — AI-authored, not verified domain content. + +Cost saving measure candidate for Rv. 13 rassikring tunnelportal: +A material substitution on the rock-support scope reduces the cost. Substituting the +specified material is the candidate cost-saving measure to validate on the portal works. diff --git a/src/portfolio_optimiser/data/reference_projects.json b/src/portfolio_optimiser/data/reference_projects.json index 8e7d7a8..f99cf94 100644 --- a/src/portfolio_optimiser/data/reference_projects.json +++ b/src/portfolio_optimiser/data/reference_projects.json @@ -1,5 +1,5 @@ { - "_note": "SYNTHETIC fixture (D4). Fictional 'anleggskostnad' projects with dummy data — not real estimates. Used as the framework's bundled reference portfolio.", + "_note": "SYNTHETIC fixture (D4). Fictional 'anleggskostnad' projects with dummy data — not real estimates. Used as the framework's bundled reference portfolio. The per-project docs_dir contents and the verdict_input (decision/rationale) values are SYNTHETIC / AI-authored placeholders — NOT verified domain judgments; a production deployer replaces this reference domain with a real data source and supplies Layer-2 verdicts via real HITL (not static config).", "currency": "NOK", "projects": [ { @@ -7,6 +7,8 @@ "name": "Fv. 42 gang- og sykkelveg, etappe 1", "description": "Ny gang- og sykkelveg langs Fv. 42, 1,2 km. Fiktivt referanseprosjekt.", "currency": "NOK", + "docs_dir": "docs/FV42-GSV-E1", + "verdict_input": {"decision": "approved", "rationale": "Asphalt scope reduction is feasible within the estimate range (SYNTHETIC / not verified)."}, "cost_items": [ {"code": "01.1", "description": "Rigg og drift", "quantity": 1, "unit": "rs", "unit_cost": 850000}, {"code": "02.3", "description": "Masseutskifting bløt grunn", "quantity": 4200, "unit": "m3", "unit_cost": 420}, @@ -21,6 +23,8 @@ "name": "Rv. 13 rassikring tunnelportal", "description": "Rassikring og forlenget portal, Rv. 13. Fiktivt referanseprosjekt.", "currency": "NOK", + "docs_dir": "docs/RV13-RAS-TP", + "verdict_input": {"decision": "approved", "rationale": "Rock-support material substitution stays within the validated savings range (SYNTHETIC / not verified)."}, "cost_items": [ {"code": "01.1", "description": "Rigg og drift", "quantity": 1, "unit": "rs", "unit_cost": 1450000}, {"code": "21.2", "description": "Sprengning fjell", "quantity": 3600, "unit": "m3", "unit_cost": 540}, @@ -34,6 +38,8 @@ "name": "Bru over Lakselva — rehabilitering", "description": "Betongrehabilitering og ny fuktisolering, eksisterende bru. Fiktivt referanseprosjekt.", "currency": "NOK", + "docs_dir": "docs/BRU-LAKS-REHAB", + "verdict_input": {"decision": "approved", "rationale": "Concrete-repair scope reduction is feasible within the estimate range (SYNTHETIC / not verified)."}, "cost_items": [ {"code": "01.1", "description": "Rigg og drift", "quantity": 1, "unit": "rs", "unit_cost": 620000}, {"code": "84.1", "description": "Stillas og tilkomst", "quantity": 1, "unit": "rs", "unit_cost": 540000}, diff --git a/src/portfolio_optimiser/reference_domain.py b/src/portfolio_optimiser/reference_domain.py index c5d5964..ba496f5 100644 --- a/src/portfolio_optimiser/reference_domain.py +++ b/src/portfolio_optimiser/reference_domain.py @@ -44,6 +44,8 @@ class Project: description: str currency: str cost_items: tuple[CostItem, ...] + docs_dir: str # absolute path to this project's bundled cost-docs folder (config-driven) + verdict_input: dict[str, str] # SYNTHETIC Layer-2 expert decision/rationale (config-driven) @property def total_cost(self) -> float: @@ -51,7 +53,12 @@ class Project: def load_reference_projects() -> tuple[Project, ...]: - """Load the bundled synthetic reference projects (D4).""" + """Load the bundled synthetic reference projects (D4). + + Each project's ``docs_dir`` is stored in the JSON relative to the package ``data/`` root + and resolved here to an absolute filesystem path; ``verdict_input`` carries the SYNTHETIC + Layer-2 expert decision/rationale. Missing keys raise ``KeyError`` (fail-fast, matching the + existing loader contract).""" resource = files("portfolio_optimiser").joinpath(_DATA_RESOURCE) raw = json.loads(resource.read_text(encoding="utf-8")) return tuple( @@ -70,6 +77,8 @@ def load_reference_projects() -> tuple[Project, ...]: ) for c in p["cost_items"] ), + docs_dir=str(files("portfolio_optimiser").joinpath("data", p["docs_dir"])), + verdict_input=p["verdict_input"], ) for p in raw["projects"] ) diff --git a/tests/test_reference_domain.py b/tests/test_reference_domain.py index f586846..b058baa 100644 --- a/tests/test_reference_domain.py +++ b/tests/test_reference_domain.py @@ -26,6 +26,18 @@ def test_every_project_well_formed(projects: tuple[Project, ...]) -> None: assert len(p.cost_items) >= 1 +def test_every_project_has_docs_dir_and_verdict_input(projects: tuple[Project, ...]) -> None: + """Step 2 (SC1 prep): docs_dir + verdict_input are config-driven per project. Each loaded + Project exposes a docs_dir resolved to an existing bundled directory, and a verdict_input + dict with exactly the decision + rationale keys. Removing the loader fields reddens this.""" + import os + + for p in projects: + assert p.docs_dir and os.path.isdir(p.docs_dir) + assert set(p.verdict_input) == {"decision", "rationale"} + assert p.verdict_input["decision"] and p.verdict_input["rationale"] + + def test_cost_item_total_is_quantity_times_unit_cost() -> None: item = CostItem(code="X", description="d", quantity=4.0, unit="m2", unit_cost=215.0) assert item.total_cost == pytest.approx(860.0)