feat(fase3): config-driven docs_dir + verdict_input on Project loader

This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 11:55:21 +02:00
commit c7c42eeb75
6 changed files with 44 additions and 2 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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},

View file

@ -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"]
)

View file

@ -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)