docs(ingest): the chain from ingest to run does not close by itself

Walked Door A from a fresh clone: materializing the file-family golden
manifest writes index.md plus one concept file per extraction, and pointing
the run at the result is refused —

    run refused: IR projection not found in bundle: 'validator-input.json'

A clean fail-fast, but nothing adopter-facing said it was coming, while the
README actively invites it ("swap --bundle-dir for your own bundle"). The
run path needs the bundle's IR projection, which ingest does not and cannot
produce: ingest materializes source documents, the projection states the
candidate measure. Both docs now say so, with the shape reference named.

Also corrects a live-doc claim that was wrong in both halves: the MCP
timeout is `anyio.fail_after` nested inside both task groups, not
`asyncio.wait_for`, and `tests/test_ingest_golden_mcp.py` covers it
(verified — 2 passing timeout tests). And no bundled example ships a
`cost-baseline.json`, so the text no longer implies one does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GyAbxJoyypnLLUDcMvnKh8
This commit is contained in:
Kjell Tore Guttormsen 2026-08-05 11:39:58 +02:00
commit bd221e6349
2 changed files with 37 additions and 1 deletions

View file

@ -79,6 +79,10 @@ uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
Ends in `ValidatedProposal`. Swap `--bundle-dir`/`--docs-dir` for your own bundle to run it over
your own data — that is the point of this door, and the reason it is not the same thing as step 2.
Your bundle needs one file the ingest layer does not write for you: `validator-input.json`, the
candidate the deterministic validator judges (a bundle without it is refused, by design — see
[`docs/extending.md`](docs/extending.md)). Copy the shape from
`shared/examples/bygg-energi-mikro/`.
**4 — Watch it say no.** Raise `claimed_saving_nok` to `250000` in `replies.json` and run the same
command again. The outcome becomes `Rejection`: the deterministic validator refuses a saving the

View file

@ -59,6 +59,27 @@ seam from the retriever above: one JSON **manifest** per source coupling declare
and a list of extractions; `materialize(...)` runs the connector for that type and writes an OKF
bundle.
**An ingested bundle is not yet a runnable bundle — the chain does not close by itself.** Walked
from a fresh clone (2026-08-05): materializing `examples/ingest-golden-file/manifest.json` writes
exactly `index.md` plus one concept file per extraction, and pointing the run at it is refused:
```
run refused: IR projection not found in bundle: 'validator-input.json'
```
That is a clean fail-fast, not a crash — but nothing before this paragraph told you it was coming.
The run path additionally needs the bundle's **IR projection**, `validator-input.json` (the
candidate the deterministic validator judges — `shared/method-spec.md` §7.1), which the ingest
layer does **not** produce: ingest materializes *source documents*, while the IR projection states
the *candidate measure*, and no connector can infer one from the other. A bundle may also ship an
optional `cost-baseline.json`; without it the validator still runs, but unanchored to the
project's real cost lines. Both are hand-authored today. For the IR projection the shape reference
is `shared/examples/bygg-energi-mikro/validator-input.json`; **no bundled example ships a
`cost-baseline.json`** (checked), so its shape — `{code: {quantity, unit_cost}}` — comes from
`ir.CostBaseline` and the README. Writing them from ingested content is
unbuilt, and is not on the 90 %-principle side of the line: what candidate to propose is the
agents' job, not the connector's.
> **Where to change it (2026-07-20):** `ingest.py` is a thin adapter — Door A is implemented by
> the shared [`llm-ingestion-okf`](https://git.fromaitochitta.com/open/llm-ingestion-okf) library
> (git-pinned to `v0.3.1`), so connectors, materialization and index generation improve in ONE
@ -171,7 +192,18 @@ its `retrieve_cost_docs(query)` has a required parameter (verified — it return
The two are separate seams by design: `build_mcp_server` serves the agents' retrieval path.
**Still true:** MCP remains **unwired in the optimiser run path** — the in-process `FunctionTool`
seam stays the default there. The timeout path (`asyncio.wait_for`) is not covered by a test.
seam stays the default there.
**The timeout path moved, and it is covered.** This paragraph used to say the deadline was
`asyncio.wait_for` and that no test exercised it; both halves are now out of date. Measuring
against a genuinely hanging server showed `asyncio.wait_for` never produced a `TimeoutError` at
all — it cancels the call from *outside* the structure anyio owns (`stdio_client`, `ClientSession`),
and the two cancellation mechanisms do not compose; the observed outcome was a
`BrokenResourceError` inside an exception group. The deadline is therefore `anyio.fail_after`
nested *inside* both task groups, and only the scope that hit its own deadline
(`CancelScope.cancelled_caught`) earns the `mcp_timeout` code — any other `TimeoutError` is
re-raised untouched, since the builtin is also `socket.timeout` and `asyncio.TimeoutError`.
`tests/test_ingest_golden_mcp.py` pins both the deadline and that discriminator.
**Where the D7 sibling stands (målbilde §11 boundary).** The Claude Agent SDK sibling built the
**file/CSV and SQL** connectors — mirroring I3/I5 — with bit-identical golden extractions. **HTTP