docs(readme): the offline walkthrough now reaches portfolio mode and the report

Steps 6 and 7, each run verbatim from a fresh clone before being written.

Step 6 is the portfolio pass, which the walkthrough could not reach until the
scripted door was wired into portfolio mode. It is also the clearest single
demonstration the framework has: all four reference projects carry a cost line
01.1 at four different amounts, so one unchanged proposal yields one
ValidatedProposal and three Rejection -- the gate is anchored to each project's
own baseline, not to the proposal's internal arithmetic. The shared verdict id
is explained rather than hidden: a verdict is keyed on the candidate, not the
project, and that key is how a later run finds the earlier judgement.

Step 7 documents the value report and, more importantly, the gap a downloader
hits first: nothing in the shipped code writes a savings ledger. Measured, not
assumed -- no .save call on a ledger exists outside the library API. That is by
design and is now said out loud: the ledger records savings actually realized in
the world, which is not a conclusion the system may draw from its own proposals.
A validated proposal is a claim; a ledger entry is a result. The refusal a
downloader sees before creating one is quoted, and the library snippet that
creates one is shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118noV9rCfrdREH26XqZB5z
This commit is contained in:
Kjell Tore Guttormsen 2026-08-05 10:51:33 +02:00
commit 8d258e9a3b

View file

@ -37,7 +37,7 @@ above is the verification.
## Walk the whole chain offline
Five commands, no API key, no network, no cost. They exercise the real loop — context navigation
Seven commands, no API key, no network, no cost. They exercise the real loop — context navigation
over the knowledge base, the maker/checker debate, the deterministic validator, the verdict — with
**scripted stand-ins for the agents' answers**. Every scripted invocation prints a banner saying so,
because a scripted run that reads like a model run would be worse than having no offline mode at
@ -101,6 +101,74 @@ uv run python -m portfolio_optimiser.costsim --projects 4 --profile local
Modelled upper bounds per role and model, with the source of each price quoted. `--profile local`
prices the free local backend; the estimate is a ceiling, not a bill.
**6 — Run the whole portfolio, and watch the gate anchor to each project separately.** The same
flag works across every bundled reference project at once:
```bash
cat > replies.json <<'JSON'
{
"proposer": "{\"measure\":\"scope_reduction\",\"affected_items\":[{\"code\":\"01.1\",\"quantity\":1,\"unit_cost\":850000}],\"claimed_saving_nok\":40000}",
"checker": "Rigging and site operations can absorb this reduction. VERDICT: APPROVE"
}
JSON
uv run python -m portfolio_optimiser.run --portfolio --scripted-replies replies.json
```
One `ValidatedProposal`, three `Rejection`. All four reference projects carry a cost line `01.1`,
but at four different amounts — so a claim stated against one project's estimate is refused for the
other three. Nothing about the proposal changed between them; what changed is the project's own
numbers, which is the whole point of anchoring the gate to a cost baseline rather than to the
proposal's internal arithmetic.
The four lines quote the same `verdict id=`. That is not a bug: a verdict is keyed on the
*candidate* it judges, not on the project it was judged in, so an identical proposal mints an
identical id by design — that key is how a later run finds the earlier judgement.
A portfolio pass reports what happened to every project. Projects that raised are printed to
stderr with their error, and the command exits non-zero; the projects that completed still print
their outcome, because one dead project must not discard the rest of the pass. A pass stopped by
the global token cap says so, separately from a pass stopped because a savings goal was reached —
running out of budget and hitting your target are not the same event.
**7 — Report what has actually been realized:**
```bash
uv run python -m portfolio_optimiser.run --report --ledger savings-ledger.json
```
This reads a savings ledger and prints per-project and portfolio totals with each entry's
provenance. It makes no model calls and changes nothing.
**The ledger is an input, and the framework will not write it for you.** It records savings that
were *actually realized* — a contract was changed, an invoice came in lower — which is a fact about
the world, not a conclusion the system is entitled to draw from its own proposals. A validated
proposal is a claim; a ledger entry is a result. Keeping them apart is deliberate, and it is why no
command here produces a ledger as a side effect. Run `--report` before creating one and it says so
plainly (`run report refused: savings ledger not found`).
You write entries when the saving materializes:
```python
from portfolio_optimiser.ledger import LedgerEntry, SavingsLedger, to_ore
ledger = SavingsLedger()
ledger.add_realized(
LedgerEntry(
project_id="FV42-GSV-E1",
dimension="rigg",
candidate_identity="33fba649cade8529",
amount_ore=to_ore(40000),
verdict_id="33fba649cade8529",
provenance="expert Kari Nordmann, 2026-08-05, realized via contract amendment",
)
)
ledger.save("savings-ledger.json")
```
Amounts are held in øre as integers, and `to_ore` is the only conversion — money is quantized once,
per amount, before anything is summed.
> `--live-dry-run` is a different, narrower drill: it builds contracts, clients and budget against
> your own configuration and **stops before the first model call**. It verifies the setup; it does
> not run the loop. `--scripted-replies` runs the whole loop. The two are mutually exclusive and