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
7.2 KiB
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
- Manifest layer — parse + fail-fast schema validation BEFORE any source call
(spec §4):
manifest_version == 1, required top-level fields, polymorphicsource.type, extraction list validation (id grammar[a-z0-9][a-z0-9-]*, unique ids, single-line titles, positivemax_rows), and the verdict reservation (spec §3):okf_typemust not beverdict(case-insensitive) and generated names must not fall inpromoted-verdict-*or beindex.md. Hand-rolled validation (no pydantic — zero-dep rule); typed error hierarchy rooted inIngestError. - Connectors (spec §4):
file: CSV underroot, boundary-checked fail-closed path resolution,utf-8-sigdecoding, streaming row cap, ragged-row rejection.sql: sqlite viaconnection_refenv-var resolution, single read-only SELECT, bool-value rejection, sqlite errors wrapped in typed errors, streaming row cap.http: GET againstbase_url+ path,credential_refenv-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).
- Materialization (spec §5): explicit required
ingested_at(ISO-8601 UTCZ, 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_manifeststamp{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. - Index generation (spec §6): create with
bundle_summarywhen 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. - 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. - Golden fixtures (spec §11):
examples/ingest-golden-file/andexamples/ingest-golden-sql/(conformance MUSTs), plusexamples/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. - 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:
- Manifest validation (schema, id grammar, verdict reservation, fail-fast order — validation error must occur with zero source calls).
- Body renderers (escaping, number forms, NULL, type rejection) as pure functions.
fileconnector (fixture catalogue; boundary check; BOM; caps).sqlconnector (fixture sqlite file; env-var resolution; read-only; caps).- Materialization + collision gate + replacement semantics.
- Index generation (fresh, preserve, idempotent, removal rules).
httpconnector + network gate (mock transport).- Golden fixtures authored, then the byte-for-byte conformance test.
- 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, thenportfolio-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:
pytestgreen, including the golden byte-for-byte comparison for all three fixture cases.mypy --strict src/clean;ruff check .andruff format --check .clean.- Determinism: run the materializer twice on the same golden inputs into two
fresh directories;
diff -rbetween them and againstexpected-bundle/is empty. - Idempotence: re-run over the just-materialized bundle;
diff -rstill empty. - Layer safety: fixture bundle containing a
promoted-verdict-*file and its index link → after re-ingest both survive byte-identical (named test). - Verdict reservation: manifest with
okf_type: Verdictrejected at validation, before any source call (named test asserts no source I/O happened). - Network gate:
httpmanifest without the opt-in flag fails fast with a typed error; no socket use (mock transport asserts zero calls). - No wall-clock:
grep -rn "datetime.now\|time.time\|utcnow" src/returns nothing. - Zero deps:
pyproject.tomlhas an empty runtime dependency list. - Offline: full test suite passes with network access disabled.