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

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