diff --git a/docs/extending.md b/docs/extending.md new file mode 100644 index 0000000..c363638 --- /dev/null +++ b/docs/extending.md @@ -0,0 +1,53 @@ +# Extending the framework (extension points) + +portfolio-optimiser is a generic core with explicit **config seams** (D4/D5, 90 %-prinsippet): +you onboard a new project, a new data source, or a new model-map **without editing the core +`src/portfolio_optimiser/*.py`**. The three guides below name the exact seam for each. + +> **Honesty note (rent teknisk rammeverk).** The bundled reference domain +> (`data/reference_projects.json` + `data/docs//`) is a set of **SYNTHETIC, AI-authored +> fixtures** — fictional construction-cost projects, dummy estimates, and placeholder +> `verdict_input` decisions. They are flagged in each file's `_note`. A production deployer +> **replaces the data source** with their own and supplies **Layer-2 verdicts via real HITL** +> (fageksperter), not static config. The static `verdict_input` field is a test-fixture +> convenience that stands in for the durable HITL verdict in the offline synthetic framework. + +## Legg til eget prosjekt + +A new project is **config + docs only** — no code change (this is exercised by the SC1 test +`test_e_new_project_flows_through_via_config_only` + the `test_f_no_hardcoded_project_ids_in_src` +guard, which fails if any project id leaks into `src/`). + +1. Append an entry to `src/portfolio_optimiser/data/reference_projects.json` with the full key + set: `id`, `name`, `description`, `currency`, `cost_items`, **`docs_dir`**, and + **`verdict_input`** (`{"decision", "rationale"}`). + - `docs_dir` is a path **relative to the package `data/` root** (e.g. `"docs/MY-PROJECT"`); + the loader (`reference_domain.load_reference_projects`) resolves it to an absolute path. + - `verdict_input` carries the (synthetic) Layer-2 decision/rationale; flag it in the file's + `_note` as synthetic if it is not a real expert verdict. +2. Create the bundled docs folder `src/portfolio_optimiser/data/docs//` with at least one + text file whose content names the cost-saving measure/terms (so `retrieve_chunks` returns at + least one citable chunk). +3. Run the project: `run_portfolio(["MY-PROJECT"], "local", client_factory=...)` (or include it + in the default fan-out by passing no `project_ids`). + +## Legg til egen datakilde + +The retriever (`retrieval.py` / `datasource.py`) reads a **local docs folder** per project, +selected by the project's `docs_dir` in `reference_projects.json`. To point a project at your own +data, change its `docs_dir` to your folder and drop your cost documentation there — the citation +seam (`{file, locator, snippet, score}`) is identical on the in-process tool and MCP paths. The +folder is boundary-checked (fail-closed) against path traversal, so keep documents inside the +configured `docs_dir`. A real deployer swaps the bundled synthetic docs for their own source. + +## Legg til egen modell-map + +Model choice is **config, not code** (B12): `src/portfolio_optimiser/data/model_map.json` maps +`profile -> role -> model/deployment` (`resolve_model(profile, role)`). To use your own models: + +- Edit the `local` block to your local model ids (Ollama/LM Studio), and/or +- Edit the `azure` block to your Foundry deployment names (the placeholders + `REPLACE-WITH-FOUNDRY-DEPLOYMENT` are tenant-specific — replace them or supply via env). + +The role keys (`proposer`, `checker`, `default`) let you assign a distinct model per debate role; +`default` is the fallback when a role is unmapped. diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 7380162..544cc45 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -211,3 +211,16 @@ def test_f_no_hardcoded_project_ids_in_src() -> None: if pid in py.read_text(encoding="utf-8") ] assert offenders == [], f"hardcoded project ids in src: {offenders}" + + +def test_g_extension_doc_exists_and_references_seams() -> None: + """SC6: the extension guide exists and names the concrete config seams a deployer extends — + the per-project config (`reference_projects.json` with `docs_dir` + `verdict_input`) and the + `model_map.json` role->deployment map. Pattern: test_budget.py:88-99 (file-content guard).""" + from pathlib import Path + + doc = Path("docs/extending.md") + assert doc.exists(), "docs/extending.md is missing" + text = doc.read_text(encoding="utf-8") + for seam in ("reference_projects.json", "docs_dir", "verdict_input", "model_map.json"): + assert seam in text, f"extension doc does not reference seam: {seam}"