llm-ingestion-okf/docs/plan/phase-1-door-a.md
Kjell Tore Guttormsen b0ad2aedfb docs(plan): add detailed phase 1-4 implementation plans
Phase 1 (Door A): ingest-spec v1 implementation + §11 golden fixtures,
pinned to the current spec revision, with TDD order and byte-exact
verification criteria. Phase 2 (Doors B/C): guard-gated inbox and
external bundle import. Phase 3: configurable bundle contract via a
profile object, default profile locked to the golden suite. Phase 4:
zero-dep Node half, coordination-first with owner sign-offs.

Each plan carries explicit key assumptions with tests and a
verification section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeqhJpYQyghASjiJo5EhGg
2026-07-16 10:58:08 +02:00

124 lines
7.2 KiB
Markdown

# Phase 1 plan — Door A: ingest-spec implementation + §11 golden fixtures
Status: approved roadmap phase (see `CLAUDE.md`); this plan details it before any code.
Normative basis: `ingest-spec.md` v1 in `portfolio-optimiser-commons` (RFC-2119).
This plan was written against spec revision `sha256:959484e1952d28fa…` — if the spec has
changed when implementation starts, re-read it first and update this plan.
## Goal
A stdlib-only Python implementation of the full ingest-spec contract:
manifest → connector (`file`/`sql`/`http`) → deterministic materialization of
`ingest-{id}.md` files → index generation — plus the §11 golden fixture suite
(byte-exact) that no repo ships today. Zero model calls; zero runtime dependencies.
## Deliverables
1. **Manifest layer** — parse + fail-fast schema validation BEFORE any source call
(spec §4): `manifest_version == 1`, required top-level fields, polymorphic
`source.type`, extraction list validation (id grammar `[a-z0-9][a-z0-9-]*`,
unique ids, single-line titles, positive `max_rows`), and the verdict
reservation (spec §3): `okf_type` must not be `verdict` (case-insensitive) and
generated names must not fall in `promoted-verdict-*` or be `index.md`.
Hand-rolled validation (no pydantic — zero-dep rule); typed error hierarchy
rooted in `IngestError`.
2. **Connectors** (spec §4):
- `file`: CSV under `root`, boundary-checked fail-closed path resolution,
`utf-8-sig` decoding, streaming row cap, ragged-row rejection.
- `sql`: sqlite via `connection_ref` env-var resolution, single read-only
SELECT, bool-value rejection, sqlite errors wrapped in typed errors,
streaming row cap.
- `http`: GET against `base_url` + path, `credential_ref` env-var resolution,
hard network opt-in gate (spec §8): refuse fail-fast unless the per-run flag
is set; the flag is a run argument, never a manifest field. Transport behind
a small seam so tests inject a mock (spec §11: no live sources in tests).
3. **Materialization** (spec §5): explicit required `ingested_at`
(ISO-8601 UTC `Z`, regex-validated), frontmatter with exactly the seven keys in
spec order, body rendering per source type (cell escaping `\``\\`, `|`
`\|`, newline → space; integers plain decimal; floats shortest round-trip;
SQL NULL → empty string; any other type is an error), `ingest_manifest` stamp
`{stem}@{sha256(manifest bytes)[:16]}`, LF-only output with exactly one
trailing newline. Replacement semantics: stage everything in memory, run the
collision gate (a generated name colliding with an unstamped existing file is
an error) BEFORE any mutation, then remove stamped files, write the new set,
update the index.
4. **Index generation** (spec §6): create with `bundle_summary` when missing;
otherwise preserve every unmanaged line byte for byte; managed links
`- [{title}](ingest-{id}.md)` appended in extraction order, idempotent by
target; on re-materialization remove only links whose target was an
ingest-owned file removed this run.
5. **Public API** — one entry point, roughly
`materialize_bundle(manifest_path, bundle_dir, ingested_at, *, allow_network=False) -> IngestResult`,
plus the typed error hierarchy. Exact naming settled during TDD; the shape
(three explicit inputs + opt-in flag) is fixed by spec §5/§8.
6. **Golden fixtures** (spec §11): `examples/ingest-golden-file/` and
`examples/ingest-golden-sql/` (conformance MUSTs), plus
`examples/ingest-golden-http/` against mock payloads (documented as the
optional extension exercised). Each case: `manifest.json`, `fixture/`,
`ingested-at.txt`, `expected-bundle/`. Runnable offline without credentials.
7. **Load-bearing test table** (spec §11): one named test per seam — provenance
stamping, navigability, verdict reservation, re-ingest layer safety over a
promoted verdict, golden byte regression, network gate. (The spec-integrity
seam belongs to commons, which owns the spec.)
## Implementation baseline
The stricter behaviors named in `CLAUDE.md` are the floor, not options: streaming
row caps, `utf-8-sig`, in-memory staging with pre-mutation collision gate,
validated `ingested_at`, typed `IngestError`.
## TDD order
Each step starts with a failing test:
1. Manifest validation (schema, id grammar, verdict reservation, fail-fast order —
validation error must occur with zero source calls).
2. Body renderers (escaping, number forms, NULL, type rejection) as pure functions.
3. `file` connector (fixture catalogue; boundary check; BOM; caps).
4. `sql` connector (fixture sqlite file; env-var resolution; read-only; caps).
5. Materialization + collision gate + replacement semantics.
6. Index generation (fresh, preserve, idempotent, removal rules).
7. `http` connector + network gate (mock transport).
8. Golden fixtures authored, then the byte-for-byte conformance test.
9. Load-bearing table completed; each seam test verified red when its seam is
detached (mutation check by hand, noted in the test docstring).
## Key assumptions (each with its test)
| # | Assumption | Test |
|---|---|---|
| A1 | Spec v1 unchanged since this plan | Compare `sha256(ingest-spec.md)` against the hash above at implementation start |
| A2 | stdlib suffices (`csv`, `sqlite3`, `json`, `hashlib`, `urllib`, `re`) | Import smoke test + CI assert that `pyproject.toml` declares no runtime dependencies |
| A3 | Byte determinism holds across runs/platforms | Run materialization twice → `diff -r` empty; golden suite in CI |
| A4 | Float shortest round-trip (`repr`) is stable in Python ≥ 3.10 | Guaranteed since Python 3.1; covered by golden bytes anyway |
## Non-goals
- Consumer adoption (`portfolio-optimiser-claude`, then `portfolio-optimiser`)
is separate coordinated work in those repos — this phase only makes the library
adoption-ready. Spec changes, if any prove necessary, go via commons.
- Doors B/C, configurable contracts, Node — later phases.
- Per-row file splitting, multi-manifest bundles, incremental re-ingest,
approval registries — spec-declared extension points, not v1.
## Verification
All must pass before the phase is declared done:
1. `pytest` green, including the golden byte-for-byte comparison for all three
fixture cases.
2. `mypy --strict src/` clean; `ruff check .` and `ruff format --check .` clean.
3. Determinism: run the materializer twice on the same golden inputs into two
fresh directories; `diff -r` between them and against `expected-bundle/` is
empty.
4. Idempotence: re-run over the just-materialized bundle; `diff -r` still empty.
5. Layer safety: fixture bundle containing a `promoted-verdict-*` file and its
index link → after re-ingest both survive byte-identical (named test).
6. Verdict reservation: manifest with `okf_type: Verdict` rejected at validation,
before any source call (named test asserts no source I/O happened).
7. Network gate: `http` manifest without the opt-in flag fails fast with a typed
error; no socket use (mock transport asserts zero calls).
8. No wall-clock: `grep -rn "datetime.now\|time.time\|utcnow" src/` returns
nothing.
9. Zero deps: `pyproject.toml` has an empty runtime dependency list.
10. Offline: full test suite passes with network access disabled.