feat(portfolio): K12 — CLI parity, doc sync, knowledge-base recipe (parity row 24) [skip-docs]

The last ungated build session: the operator now drives the whole build from the
command line, and the documents claim exactly what the code does (§1).

run.py becomes the collecting entrance. Exactly one of --bundle (one project) or
--portfolio (N projects from a schema-validated reference config, with
--verdict-dir as the portfolio-level expert inbox) is required; both and neither
are refused. --goals loads a goal contract and checks it against --ledger's
realized sum BEFORE the first model call: the §8 caps bound spend, the goal bounds
achievement, so a hard target the book already meets stops the run at exit 4
without constructing a client. A soft target reached is a flag and the run
continues; an absent ledger is an empty book, so the goal is still evaluated,
never skipped. The one declared goal also drives --value-report's goal progress —
one contract, never two figures that can disagree.

The portfolio path persists nothing (K3 returns typed results; the outbox names
pairs by run_id, which a portfolio pass has none of). Rather than accept
--out/--outbox/--run-id/--value-report/--inbox/--live-dry-run there and silently
ignore them, the entrance refuses them and says why. run_portfolio is imported
lazily — portfolio.py imports this module, so a module-level import is circular.

Three seams, each detach-proven RED:
- unwire the goal check → the run proceeds and spends → red
- unwire the portfolio branch → the configured projects never run → red
- document a flag no CLI offers → the README honesty grep goes red

That last one is the doc-sync made load-bearing: the test reads README.md,
collects every --flag it documents (excluding third-party dev-tooling lines) and
asserts each exists in the --help of a CLI the README names. The drift it exists
to close was real — README claimed 562 tests, CHANGELOG claimed 265, actual 597.

Docs synced to the code: README gains an operator-CLI section and honest goal/
portfolio descriptions, CHANGELOG is rewritten to what actually shipped, and
docs/oppskrift-kunnskapsbase.md delivers D-H point 1 — the documented team
process for building a knowledge base, with the honest 1–2 week expectation and
every factory-dependent step (verdict translation, demo path) marked NOT BUILT.

597 passed · ruff clean · mypy strict clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 06:42:52 +02:00
commit da93a68ce7
6 changed files with 956 additions and 28 deletions

View file

@ -11,6 +11,10 @@ runs offline: ingest makes **zero model calls**, touches no network, and
[`ingest.py`](../src/portfolio_optimiser_claude/ingest.py) imports nothing from the Agent SDK
— it is pure standard library.
Ingest is the **automated** path to a bundle: it covers the sources that are already tabular.
Everything a domain expert knows that no table holds is built by hand, and that process is a
separate deliverable — see [the knowledge-base recipe](oppskrift-kunnskapsbase.md).
## Where ingest sits
```

View file

@ -0,0 +1,163 @@
# Recipe: building a knowledge base
*(Norwegian title kept from the programme plan — «oppskrift» = recipe. Contents in English,
like the rest of this repo's documentation.)*
This is the documented **team process** for building the OKF bundles this framework reads.
It is a recipe, not a wizard: there is deliberately no onboarding interview and no guided
verdict command. Both were evaluated and rejected — a setup this consequential is done by
people talking to each other, and a wizard would hide exactly the decisions that need
deciding.
**Honest expectation: 12 weeks of dedicated work** for a knowledge base worth running
against. The quality of that investment decides the quality of everything downstream. A
thin bundle produces confident nonsense; the deterministic validator will catch arithmetic,
but it cannot catch a domain the base never described.
**Honesty rule (method spec §1):** this document separates what this repo *does today* from
what the process *will* look like once the bundle factory exists. Sections marked
**NOT BUILT** describe a target arrangement, not a capability you can use.
## 1. The team
Setup is always done by a small team, never by one person and never by a tool alone:
| Role | Brings | Owns |
|---|---|---|
| Technical | the file formats, the ingest layer, the run entrance, the caps | the bundle's *shape* |
| Domain expert(s) | what is actually true in this domain, and what the numbers hide | the bundle's *content* |
The split matters because the two failure modes are different. A technically perfect bundle
with no domain substance runs cleanly and tells you nothing. A domain-rich pile of documents
that never navigates is invisible to the loop.
## 2. What a knowledge base is here
One **OKF bundle** per project: a directory of plain markdown, plus the IR projection the
deterministic validator consumes. From the shared micro example
([`shared/examples/bygg-energi-mikro/`](../shared/examples/bygg-energi-mikro/)):
```
bygg-energi-mikro/
index.md # REQUIRED entry point; links to everything else
bygg-kontor-nord.md # type: project — the asset and its baseline
tiltak-led-retrofit.md # type: hypothesis — the candidate measure + parameters
metode-ipmvp-a.md # type: methodology — how savings are measured
kilder-realiseringsgap.md # type: reference — sourced literature
verdict-led-fro.md # type: verdict — an expert judgement (see §5)
validator-input.json # the IR projection the validator consumes
```
Four rules the code actually enforces — verify against these, not against intent:
1. **`index.md` is the entry point.** A bundle without one fails fast (`navigate_bundle`).
2. **The index's cross-links define the bundle.** Navigation follows the links in `index.md`;
a file nobody links to is a file the model never sees. Links containing a path separator
are skipped (fail-closed boundary rule), so keep concept files flat and beside the index.
3. **Every non-index file needs a `type` in its frontmatter.** `project`, `hypothesis`,
`methodology`, `reference`, `verdict` are the types in use.
4. **`type: verdict` files are excluded from the read-context.** A verdict reaches the next
hypothesis only through the experience fold, never by leaking through context. This is a
seam, not a preference — see the load-bearing tests.
`validator-input.json` is separate and **required by the run entrance**: it is the candidate
measure projected into the cost-IR (`project_id`, `measure`, `affected_items` with quantity
and unit cost, `claimed_saving_nok`). Writing it is technical work, and it is where the
domain expert's parameters become numbers the validator can refuse.
## 3. The process, week by week
**Day 12 — scope and one skeleton.** Pick *one* project and *one* candidate measure. Build
the bundle above end to end, however thin. Run it:
```bash
uv run python -m portfolio_optimiser_claude.preflight
uv run python -m portfolio_optimiser_claude.costsim --projects 1
uv run python -m portfolio_optimiser_claude.run --bundle <dir> --live-dry-run \
--outbox <dir> --run-id skeleton-1
```
The drill builds everything a real run builds and stops before the first model call, so the
rig is proven before anyone spends anything. Do not proceed to content until this is clean.
**Day 37 — content, expert-led.** The domain experts fill in what the skeleton only named:
the baseline the asset actually has, the method by which a saving is measured, and — the part
that matters most — the *sourced* reference material describing where modelled numbers and
realized numbers diverge. In the micro example this is `kilder-realiseringsgap.md`, and it is
the reason the learning loop has anything to learn.
**Day 810 — seed the judgement layer.** Add at least one `type: verdict` file encoding what
the experts know that the model does not: the correction they would apply and *why*. Without
this the first runs have no experience to fold, and the loop is a validator with extra steps.
**Ongoing — sufficiency review.** Ask the base directly: *is this comprehensive enough to be
useful, and what is missing?* The answer becomes the domain experts' work list. This review
is human-run today; an automated evaluator is part of the factory (**NOT BUILT**, §7).
## 4. The domain expert's interface
The expert is asked exactly three things: **what** to contribute, **how**, and **where**.
They deliver files in their own formats — notes, spreadsheets, reports, memos — and are never
asked to fill in a schema or hand-write JSON.
**Today:** a technical team member transcribes those contributions into bundle files. This is
the interim arrangement, and it is real work — budget for it in the 12 weeks. The AI
translator that would do it is **NOT BUILT** (§7).
**Tabular sources are the exception, and they are automated.** If the contribution is a CSV
catalogue or a local SQL database, the ingest layer materializes an OKF bundle from it
deterministically, with provenance frontmatter and a `generated: true` honesty marker. That
path makes zero model calls and touches no network. See
[`docs/extending.md`](extending.md).
## 5. Verdicts — the loop's fuel
After a run, an expert judges the proposal. The verdict is a plain-JSON file dropped into an
inbox folder; a later run merges it before the fold. The shape (§4.2) is deliberately small:
an `id`, a `decision`, a `rationale`, and the proposal features the verdict is about.
**Today:** the same transcription applies — the expert judges in their own words, a technical
team member writes the verdict file. The decision is the human's in both arrangements; only
the typing changes.
**NOT BUILT — the target arrangement.** The expert delivers a free-format file and the
factory AI-translates it into the strict verdict format, because at volume manual translation
is unrealistic. Approval in practice then becomes **spot checks** in the generated bundle,
which only works with one guard in place: *the structured verdict must always point back at
the expert's original file*, so a spot check can compare what the expert wrote against what
the system understood. **The verdict is the human's judgement; the AI is a translator.** Any
implementation of this that drops the provenance pointer breaks the arrangement.
## 6. Reading and editing bundles
Bundles are plain markdown with frontmatter, so **Obsidian or VS Code is the reading solution**
— Obsidian in particular follows the cross-links the way the framework navigates them, which
makes "what will the model actually see?" answerable by clicking. A dedicated reading view is
a later building block in the toolkit repo, not a prerequisite.
## 7. What is not built
Named here so the recipe cannot be mistaken for a description of shipped capability (§1):
- **The bundle factory** — the tooling that would translate free-format expert contributions
(and verdicts) into bundle files. It lives in a separate toolkit repo that **does not exist
yet**. Everything above marked NOT BUILT depends on it.
- **The demo path** — fresh clone → unzip a worked example into a bundle inbox → the factory
builds it → the whole loop runs. Factory-dependent, and honest only once the factory is
real: the point of that demo is that it shows the actual process with finished content.
- **A dedicated reading view** for bundles (see §6).
- **A dimension catalogue** — the formalized scoping of measure types and cost codes. Today
the routing table treats a measure as a config string; the catalogue is gated on a pending
amendment to the shared spec.
## 8. Checklist before the first real run
- [ ] `index.md` exists and links every concept file that should be visible.
- [ ] Every non-index file has a `type` in its frontmatter.
- [ ] `validator-input.json` matches the hypothesis file's parameters.
- [ ] At least one `type: verdict` file encodes real expert correction.
- [ ] Reference material is sourced — a claim without a source is a claim the experts cannot
defend when the model repeats it back.
- [ ] `preflight` is clean and `costsim` gives a cost you have accepted.
- [ ] `--live-dry-run` completes and its captured artifacts look right.
- [ ] Stop criteria and a budget cap are set — the framework refuses to start without them.