54 KiB
I2 — MAF reference implementation: ingest file catalogue/CSV (offline)
Plan quality: B+ (88/100) — APPROVE_WITH_NOTES
Generated by trekplan v5.9 on 2026-07-03 —
plan_version: 1.7
Context
The 8-step optimiser loop consumes hand-curated OKF bundles today; the frozen ingest
målbilde closes the gap to real data sources while preserving the architecture-defining
rule that data reaches the model ONLY via OKF bundles (no RAG, no query-time retrieval —
method spec §3). I2 is the first implementation session of that program: it proves the
frozen contract (shared/ingest-spec.md, authored and guarded in I1) is implementable
from the spec alone, on the reference (MAF) side, using the offline file source type so
the session stays at zero model calls and zero network (cost discipline D6). It closes the
session plan's key assumption 2 (fail-fast manifest validation without network, currently
RISK-marked) and turns the planning session's ephemeral ad-hoc navigability proof into a
durable load-bearing test. Source: brief Intent, verbatim intent trace.
Scope note (brief, Non-Goals): I2 is deliberately a partial-conformance milestone —
spec §1 full conformance (file + sql) completes at I4. The manifest contract validates
all three source variants; only the file connector executes in I2.
Architecture Diagram
graph TD
subgraph "New in this plan"
M[manifest.json] -->|"load_manifest() — pydantic fail-fast (§4), verdict reservation (§3)"| C[ingest.py contract models]
C -->|"file connector: safe_resolve boundary + csv.reader + max_rows cap"| X[extraction rows in memory]
X -->|"render: escaped markdown table + §5/§7 provenance frontmatter"| S[staged files in memory]
S -->|"replacement semantics: delete stamped only, collision fail (§3)"| B[OKF bundle dir]
S -->|"index create/update: bundle_summary body, managed-line removal, link_in_index append (§6)"| I[index.md]
end
subgraph "Existing, UNCHANGED"
B --> N["okf.navigate_bundle / bundle_context"]
I --> N
V["verdicts.promote_verdict"] -->|"promoted-verdict-*.md + index link — must survive re-ingest"| B
R["retrieval.safe_resolve / PathSecurityError"] -.reused by.-> C
L["okf.link_in_index / render_frontmatter / parse_frontmatter"] -.reused by.-> S
end
Codebase Analysis
- Tech stack: Python ≥3.10, pydantic ≥2.11,
uv, pytest ≥8 (asyncio_mode=auto; ingest tests are plain sync), ruff (line-length 100), mypy (py3.10 target). No new runtime dependencies needed: stdlibcsv,hashlib,logging,pathlib+ pydantic suffice. - Key patterns: fail-fast pydantic startup contracts (
contracts.py:30-96); frozen dataclasses for value objects,BaseModelfor validated external input; PEP 604 unions, keyword-only options via bare*; condition-noun exceptions (PromotionRefusedverdicts.py:447,PathSecurityErrorretrieval.py:24, dominant baseRuntimeError); narrative module docstrings citing målbilde/spec sections; load-bearing tests with detach-RED docstrings (tests/test_step8_promotion_loadbearing.py). - Relevant files:
src/portfolio_optimiser/okf.py(UNCHANGED — reuseparse_frontmatter:31,render_frontmatter:146,link_in_index:168,navigate_bundle:113,bundle_context:133),src/portfolio_optimiser/retrieval.py(safe_resolve:63,is_within_dir:48,PathSecurityError:24),src/portfolio_optimiser/verdicts.py(promote_verdict:465— structural analogue and the SC5 test's real promotion; SHA-256[:16]idiom atverdicts.py:95,461),tests/test_method_spec_loadbearing.py:156-174(recorded I2 obligation: field-level cross-check),tests/conftest.py(no ingest-relevant fixtures — none needed),shared/examples/bygg-energi-mikro/(copytree fixture for the re-ingest safety test). - Reusable code:
safe_resolvefor the §4 root boundary (fail-closed);render_frontmatterfor §5 key order + single-lining (collapses whitespace in ALL values — acceptable: §5 requires single-line values everywhere; golden pins it);link_in_indexfor §6 appends (idempotent by](target)— exactly the primitive §6 references);parse_frontmatterfor stamp detection (generated == "true"string compare — it returns strings, never booleans);promote_verdictas the shape template (explicit requiredtimestamp, no wall-clock default). - Deliberately NOT reused:
okf.write_concept_filefor the final write — it delegates toPath.write_text(platform newline translation risk) and does not guarantee exactly one trailing newline (okf.py:164). The ingest module builds each file's full content as a string and writes bytes (encode("utf-8")) for the §5 LF-only guarantee on CONCEPT files. Honest limit: index APPENDS reuse the frozenlink_in_index(Path.write_text,okf.py:180), soindex.mdLF-ness is platform-scoped — nil on the LF-only development/target platform; recorded as Assumption 6.contracts.pyis NOT imported: it importsbackends→agent_framework(contracts.py:25), which would break the ingest module's MAF-free guard. Manifest models live iningest.pyitself. - External tech (researched): none — offline task, frozen spec, established repo patterns (brief Research Plan: 0 topics).
- Recent git activity: I1 landed the spec + guards 1h before planning (
4df2140,7ba0fae);tests/test_method_spec_loadbearing.pyis freshly touched — extend, don't conflict. All work is sequential onmain, clean tree. Commons subtree is PULL-ONLY (violation observed + cleaned 2026-07-03); nothing in this plan writes undershared/. - Codebase-analysis correction (honesty): the brief's Research Plan rationale mentions
JSON-Schema validation; exploration verified
datasource.pycontains no schema validation — the fail-fast validation pattern lives incontracts.pyas pydantic. The plan follows the pydantic pattern (which the brief's Preferences already mandate).
Implementation Plan
Every step is TDD (Iron Law): the step's tests are written and observed RED before the
production code that turns them green. okf.py and everything under shared/ are
untouchable in every step (see per-step forbidden_paths). All commands run offline.
Step 1: Fail-fast manifest contract (pydantic, polymorphic, verdict reservation)
- Files:
src/portfolio_optimiser/ingest.py(new),tests/test_ingest_manifest.py(new) - Changes: Create the ingest module with its narrative docstring (purpose, spec §
references, MAF-free invariant, D7-portability) and the manifest contract per spec §4:
FileSource(type: Literal["file"],root: str),SqlSource(type: Literal["sql"],connection_ref: str),HttpSource(type: Literal["http"],base_url: str— reject embedded credentials (userinfo@) via validator, optionalcredential_ref: str), all with commonid: strconstrained to^[a-z0-9][a-z0-9-]*$;sourceas a discriminated union ontype(Field(discriminator="type")).Extraction:id(same grammar),title(non-empty, single-line — reject\n/\r),query: str,okf_type(non-empty, single-line,@field_validatorrejectingverdictcase-insensitively — spec §3, before any source call),max_rows: int = Field(gt=0).ManifestV1:manifest_version: Literal[1],source,bundle_summary: str(non-empty),extractions(min_length=1,@model_validatorrejecting duplicate extraction ids). Public loaderload_manifest(path: str | Path) -> tuple[ManifestV1, str]that reads raw bytes ONCE, computes the{stem}@{sha256(raw)[:16]}stamp (spec §5),json.loads, validates — raisingpydantic.ValidationError/json.JSONDecodeErrorbefore ANY source access. DefineIngestError(RuntimeError)(condition-noun family; used by later steps for materialization-time refusals).titleis whitespace-normalized at validation (" ".join(title.split())after the single-line check) so the frontmatter rendering (which collapses runs,okf.py:154) and the index label are guaranteed identical. TheHttpSourcecredential validator is defined precisely: reject whenurllib.parse.urlsplit(base_url)yields a non-Noneusernameorpassword(userinfo is THE credential-embedding URL mechanism §4 targets; query-param token heuristics are out of scope — documented in the validator docstring). Baseline capture (SC7): BEFORE writing anything in this step, runuv run pytest -qon the clean tree and record the summary line in the session log — this is the pre-existing-suite baseline SC7's "unchanged-green" is checked against. (new file) - Reuses: pydantic contract idiom from
contracts.py:30-96(Fieldconstraints,Literalenums,@model_validator(mode="after")returningself); SHA-256[:16]idiom fromverdicts.py:95; docstring style fromokf.py:1-12. - Test first:
- File:
tests/test_ingest_manifest.py(new) - Verifies: valid
filemanifest loads with correct stamp (stem + 16-hex of raw bytes); each missing top-level field raisesValidationError;manifest_version != 1rejected; emptyextractionsrejected; badidgrammar (uppercase, leading-, empty) rejected for both source and extraction ids; duplicate extraction ids rejected;max_rows <= 0rejected; unknownsource.typerejected;filesource missingrootrejected;okf_typeofverdict/Verdict/VERDICTrejected; multi-linetitlerejected;base_urlwith embedded credentials rejected; fail-fast ordering proof: a manifest that is malformed AND whosesource.rootdoes not exist raisesValidationErrorwithout touching the missing root (the ingest analogue oftest_no_chat_client_call_on_malformed_contractintests/test_contracts.py);sql/httpvariants VALIDATE (schema breadth per brief assumption) though no connector executes them in I2. - Pattern:
tests/test_contracts.py(inline dict constants, onepytest.raisesper malformation)
- File:
- Verify:
uv run pytest tests/test_ingest_manifest.py -q→ expected: all pass (after observed RED on first run without implementation) - On failure: revert —
rm -f src/portfolio_optimiser/ingest.py tests/test_ingest_manifest.py(both files are NEW and untracked until this step's checkpoint —git checkout --cannot remove untracked files) - Checkpoint:
git commit -m "feat(ingest): fail-fast manifest contract with verdict reservation (I2)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_manifest.py min_file_count: 2 commit_message_pattern: "^feat\\(ingest\\): fail-fast manifest contract with verdict reservation \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/ingest-spec.md must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "load_manifest" - path: src/portfolio_optimiser/ingest.py pattern: "discriminator"
Step 2: CSV connector — boundary check, streaming row cap, cell escaping, table body
- Files:
src/portfolio_optimiser/ingest.py,tests/test_ingest_materialize.py(new) - Changes: Add the
fileconnector + renderer. Connector: resolve the manifest'srootagainst the manifest file's parent directory when relative (pinned decision — extraction must not depend on cwd), thensafe_resolve(root, extraction.query)for the fail-closed §4 boundary check; open withencoding="utf-8-sig"(BOM never leaks into the first header cell) andnewline=""; parse withcsv.reader. First row = header; fail (IngestError) on empty file (no header). Stream data rows counting againstmax_rows— raiseIngestErrorthe moment the cap is exceeded (§8: error, never silent truncation; no fetch-all-then-count). Fail on ragged rows (data row width ≠ header width) — silent padding/truncation is coercion. Renderer: cell text verbatim with escaping in this exact order:\→\\FIRST, then|→\|, then any newline (CR, LF, CRLF) → single space (pinned decision: on thefile/CSV path every cell is a string, so §5's integer/float/NULL clauses bite typedsqlvalues in I4 — a spec-text reading recorded as Assumption 1). Newline replacement treats CRLF as ONE unit: replace\r\nfirst, then lone\r, then lone\n— each with a single space (a per-character replace would turn CRLF into two spaces). A well-formed manifest whoserootdirectory or resolvedqueryCSV does not exist raisesIngestErrorwith a clear message (never a bareFileNotFoundErrorleaking fromopen()). Body = markdown table:| h1 | h2 |header row,| --- | --- |separator, data rows in source order, LF line endings, ending in exactly one\n. Header cells are escaped identically to data cells (spec §5 separates "column names" from "cell values" without giving headers their own rule — pinned decision: same escaping/newline-collapse, or a header containing|or an embedded newline breaks the table). The connector itself is PURE (returns header + rows); the §8 source-call logging lives inmaterialize(Step 3), which ownsingested_at. - Reuses:
retrieval.safe_resolve/PathSecurityError(retrieval.py:63,24) — identical fail-closed semanticsokf.pyitself relies on. - Test first:
- File:
tests/test_ingest_materialize.py(new) - Verifies: table rendering (header/separator/rows, source order); escaping of
\,|, embedded quoted newline (one assertion each, plus one cell containing BOTH\and|proving escape order, plus an explicit embedded-CRLF cell → exactly ONE space); a header cell containing|and one containing an embedded newline → escaped/collapsed identically to data cells; header-only CSV → header + separator, zero data rows; empty CSV file →IngestError; ragged row →IngestError;max_rowsfixture with cap+1 rows →IngestError;../escapequery →PathSecurityError; missingrootdir and missingqueryfile →IngestError(not bareFileNotFoundError); BOM fixture → first header cell clean. - Pattern: local
tmp_pathCSV-builder helper (conftest's LLM fixtures are irrelevant to ingest — no conftest changes)
- File:
- Verify:
uv run pytest tests/test_ingest_materialize.py -q→ expected: all pass - On failure: revert —
git checkout -- src/portfolio_optimiser/ingest.py && rm -f tests/test_ingest_materialize.py(ingest.pyis tracked after Step 1's checkpoint; the test file is new in this step) - Checkpoint:
git commit -m "feat(ingest): CSV connector with boundary check, row cap and escaped table body (I2)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_materialize.py min_file_count: 2 commit_message_pattern: "^feat\\(ingest\\): CSV connector with boundary check, row cap and escaped table body \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - tests/conftest.py must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "safe_resolve" - path: src/portfolio_optimiser/ingest.py pattern: "utf-8-sig"
Step 3: Materialization — provenance frontmatter, LF byte-writer, in-memory staging
- Files:
src/portfolio_optimiser/ingest.py,tests/test_ingest_materialize.py - Changes: Add the per-extraction concept-file builder and the public entry point
materialize(manifest_path: str | Path, bundle_dir: str | Path, *, ingested_at: str) -> list[Path](explicit requiredingested_atkeyword, NO wall-clock default — mirrorspromote_verdict; validate against the regex^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$— NOTdatetime.fromisoformat, which rejects theZsuffix on Python 3.10, the repo's version floor — then stamp the string verbatim). This programmatic function IS the I2 invocation surface (recorded decision, resolving the brief's open question: apython -mCLI is not demanded by the I2 verification set and is deferred).materializeperforms the §8 source-call log per extraction on loggerportfolio_optimiser.ingest: source id, theingested_atargument (deterministic "when" — never wall-clock), row count; never log cell contents or (in later source types) resolved secrets. It creates the bundle directory first:Path(bundle_dir).mkdir(parents=True, exist_ok=True)—safe_resolvenever creates directories, and unlikeokf.write_concept_filethe byte-writer has no implicit mkdir. Frontmatter: ordered dict with EXACTLY the §5 keys in §5 order —type(=okf_type),title,source_system(=source.id),source_query(the query, whitespace-collapsed),ingested_at,ingest_manifest(the Step-1 stamp),generated(true) — rendered viaokf.render_frontmatter(insertion-ordered, single-lining). Filenames:ingest-{extraction.id}.md(§5; grammar keeps the namespace disjoint fromindex.md/promoted-verdict-*by construction). File content assembled as ONE string---\n{fm}\n---\n\n{body}(body already ends in exactly one\n) and written as bytes (content.encode("utf-8")) to asafe_resolved path — LF-only and exactly one trailing newline guaranteed on every platform (§5). All extractions execute and render in memory BEFORE the first disk mutation (crash-window mitigation for §5's non-atomic replace sequence; recovery = idempotent re-run, §10). - Reuses:
okf.render_frontmatter(okf.py:146),retrieval.safe_resolve;promote_verdict's explicit-timestamp shape (verdicts.py:465-472). - Test first:
- File:
tests/test_ingest_materialize.py(extend) - Verifies: generated file's RAW TEXT has the 7 frontmatter keys in exact §5 order (read
the file text, not just the parsed dict);
okf.parse_frontmatterround-trips the values (generated == "true"as string;ingest_manifest == {stem}@{hash16}computed independently in the test from the manifest's raw bytes;ingested_atverbatim;source_querywhitespace-collapsed); file bytes are LF-only with exactly one trailing\n(read_bytes()assertions: no\r, endswith exactly oneb"\n"); missing/invalidingested_at(no argument has no default — non-Z/non-ISO string raisesValueError; the golden timestamp2026-07-03T12:00:00Zpasses the regex); materializing into a NON-EXISTENT nestedbundle_dirsucceeds (mkdir path); §8 log record (viacaplog) carries source id, the explicitingested_atstring, and row count; twomaterializeruns with identical inputs produce byte-identical files (§10 idempotence at file level). - Pattern:
tests/test_okf.pyfrontmatter round-trip assertions
- File:
- Verify:
uv run pytest tests/test_ingest_materialize.py -q→ expected: all pass - On failure: revert —
git checkout -- src/portfolio_optimiser/ingest.py tests/test_ingest_materialize.py - Checkpoint:
git commit -m "feat(ingest): deterministic materialization with §5/§7 provenance stamp (I2)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_materialize.py min_file_count: 2 commit_message_pattern: "^feat\\(ingest\\): deterministic materialization with .5/.7 provenance stamp \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "ingested_at" - path: src/portfolio_optimiser/ingest.py pattern: "ingest_manifest"
Step 4: Index generation + stamped-replacement semantics
- Files:
src/portfolio_optimiser/ingest.py,tests/test_ingest_materialize.py - Changes: Complete
materializewith §3/§5/§6 bundle semantics. (a) Ownership scan: classify a bundle file as ingest-owned iffokf.parse_frontmatteryieldsgenerated == "true"AND aningest_manifestkey (string comparisons; sorted directory scan for determinism). (b) Collision gate: if a target filenameingest-{id}.mdexists WITHOUT the stamp →IngestError, nothing written (§3: never overwrite curated content). (c) Replace: delete every stamped file, then write the new staged set. (d) Index: ifindex.mdmissing → create it withbundle_summaryas the body (no frontmatter — spec-minimal, pinned decision; content ={bundle_summary}\nwritten as bytes). If present → preserve every unmanaged line byte-for-byte; remove ONLY whole lines matching the managed-link form- [<label>](<target>)whose<target>is a previously-stamped file deleted in this run and NOT re-created (exact full-line target match, never bare-substring — a promoted verdict's or curated link must be unreachable by this filter). (e) Link:okf.link_in_index(bundle_dir, "ingest-{id}.md", title)per extraction in manifest order (idempotent by target: links for re-created targets keep their existing position — byte-stability across re-ingest). (f) Label refresh: for a re-created target whose existing managed line's label no longer equals the (normalized) extractiontitle, rewrite THAT line in place —- [{new title}](ingest-{id}.md)— preserving its position; §6 defines the label AS the title, so a title change must never leave a stale label behind. The refresh uses the SAME anchoring as the removal filter: only an exact full-line managed form- [<label>](ingest-{id}.md)targeting THIS run's ingest files is rewritten; non-conforming lines (curated prose mentioning the target inline, links to non-ingest files) are untouched. Index update is the LAST disk mutation. - Reuses:
okf.link_in_index(okf.py:168— the exact primitive spec §6 cites),okf.parse_frontmatter(okf.py:31). - Test first:
- File:
tests/test_ingest_materialize.py(extend) - Verifies: fresh dir →
index.mdcreated withbundle_summarybody + one link per extraction in manifest order; existingindex.mdwith a curated line → line preserved byte-for-byte after materialization; re-materialization with an extraction REMOVED from the manifest → its file gone AND its index link gone, other links intact; re-materialization with a CHANGED extractiontitle(same id) → the managed link's label updated in place, position preserved (no stale label); unstampedingest-foo.mdpresent →IngestErrorand bundle unmodified (assert directory bytes unchanged); re-materialization twice with identical inputs → byte-identical bundle includingindex.md(§10). - Pattern:
tests/test_okf.py::test_link_in_index_makes_concept_file_navigable
- File:
- Verify:
uv run pytest tests/test_ingest_materialize.py -q→ expected: all pass - On failure: revert —
git checkout -- src/portfolio_optimiser/ingest.py tests/test_ingest_materialize.py - Checkpoint:
git commit -m "feat(ingest): index generation and stamped-replacement semantics (I2)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_materialize.py min_file_count: 2 commit_message_pattern: "^feat\\(ingest\\): index generation and stamped-replacement semantics \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - src/portfolio_optimiser/verdicts.py must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "link_in_index" - path: src/portfolio_optimiser/ingest.py pattern: "bundle_summary"
Step 5: Load-bearing seam quartet + MAF-free guard (detach-RED proofs)
- Files:
tests/test_ingest_loadbearing.py(new) - Changes: The durable seam tests per spec §11's table, in the repo's load-bearing
idiom (module docstring naming each seam + its detach-RED condition; scenarios built so
the seam is the ONLY thing preventing the observable artifact). (1) Verdict
reservation: an otherwise fully-valid manifest with
okf_type: "Verdict"→ValidationErrorat load, zero files/links written (RED if the case-insensitive reservation validator is removed). Plus the defensive namespace invariant: the id grammar +ingest-prefix makepromoted-verdict-*/index.mdcollisions structurally unreachable — asserted as invariant, with the honest note that its load-bearing bite is theokf_typecheck. (2) Provenance stamping: materialize, then assert the §7 layer on every generated file via UNCHANGEDokf.parse_frontmatter(RED if the stamp layer is dropped). (3) Navigability via UNCHANGED okf.py (the durable version of the planning session's ad-hoc proof):okf.navigate_bundlereaches everyingest-{id}.mdthrough index cross-links; each is classified by itsokf_type; provenance fields ride throughfrontmatter(unknown-field preservation);okf.bundle_contextrenders the generated content (RED if index linking detaches — file unreachable). (4) Re-ingest layer safety — with the removal filter ACTIVE: on ashutil.copytreecopy ofshared/examples/bygg-energi-mikro/, runmaterialize(manifest with ≥2 extractions), then the REALverdicts.promote_verdict(approved verdict, explicit timestamp), thenmaterializeAGAIN with a REDUCED manifest (one extraction dropped) — the §6 managed-line removal filter must actually FIRE while the promoted link and the bundle's curated managed-form links are present (an identical-manifest second run never removes anything, so it cannot catch a loose filter — the green-but-dead trap): the dropped extraction's file AND index link are gone; the promoted verdict file and its index link survive verbatim; every curated link survives verbatim; only stamped files were replaced (RED if the filter over-matches —promote_verdictwrites exactly the managed-line shape a bare-substring filter would wrongly delete — or if replacement stops honouring the stamp ownership rule). (5) MAF-free guard: AST-parsesrc/portfolio_optimiser/ingest.py(pattern:tests/test_okf.py::test_okf_is_maf_free) asserting no import roots in{agent_framework, mcp}AND noportfolio_optimiser.*imports outside{okf, retrieval}(transitively stdlib-pure —contracts/backendswould smuggle MAF in). Execution rule: each of (1)–(4) is proven RED by temporarily detaching its seam (comment out the validator / stamp keys /link_in_indexcall / the stamp filter), observing the failure, reattaching; the detach-proof log (seam → detached how → observed failure) goes into the session verification log for STATE/commit message. - Reuses:
_copy_bundlecopytree idiom (tests/test_step8_promotion_loadbearing.py:51), AST guard (tests/test_okf.py:165-181), realpromote_verdict(verdicts.py:465). - Test first:
- File:
tests/test_ingest_loadbearing.py(new — this whole step IS tests; the seams it guards were built in Steps 1–4, so these must pass immediately and the detach-proofs demonstrate they can fail) - Verifies: the four §11 seams + module purity, as above
- Pattern:
tests/test_step8_promotion_loadbearing.py(docstring naming detach-RED per test)
- File:
- Verify:
uv run pytest tests/test_ingest_loadbearing.py -q→ expected: all pass; then 4 documented detach→RED→reattach cycles each showing ≥1 failure - On failure: escalate — a quartet test that cannot go RED at detach means the seam is
not actually load-bearing; stop and redesign the test before proceeding (do not weaken
the assertion). Cleanup on abandon:
rm -f tests/test_ingest_loadbearing.py(new file — untracked until this step's checkpoint) - Checkpoint:
git commit -m "test(ingest): load-bearing seam quartet + MAF-free guard, detach-proven (I2)" - Manifest:
manifest: expected_paths: - tests/test_ingest_loadbearing.py min_file_count: 1 commit_message_pattern: "^test\\(ingest\\): load-bearing seam quartet \\+ MAF-free guard, detach-proven \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/examples/bygg-energi-mikro/index.md must_contain: - path: tests/test_ingest_loadbearing.py pattern: "promote_verdict" - path: tests/test_ingest_loadbearing.py pattern: "navigate_bundle"
Step 6: Spec §12 field cross-check (discharge the I1-recorded obligation)
- Files:
tests/test_method_spec_loadbearing.py - Changes: Add
test_ingest_spec_documents_every_contract_field, mirroring the existingtest_spec_documents_every_contract_field(line 106): iterate the REAL pydantic models'model_fields(ManifestV1, recursively into the three source variants and Extraction) plus the seven §5 frontmatter keys plus the four golden-case entries (manifest.json,fixture/,ingested-at.txt,expected-bundle/), asserting each token appears inshared/ingest-spec.md(the §12 cross-check table's completeness, test-enforced at field level). This discharges the deferred note recorded attests/test_method_spec_loadbearing.py:156-162("arrives with the I2 contracts"). RED when contract code and spec drift apart. Surgical: only ADD the test — the I1 structure/neutrality guards in the file are untouched. - Reuses: the file's own test-3 pattern (
model_fields-driven spec assertion). - Test first:
- File:
tests/test_method_spec_loadbearing.py(existing) - Verifies: every machine-readable contract field is documented in the frozen spec; a hypothetical new model field without a spec entry turns it RED (detach-proof: add a dummy field to a throwaway copy check — verified by temporarily adding a fake field name to the iteration list, observing RED, removing)
- Pattern:
tests/test_method_spec_loadbearing.py::test_spec_documents_every_contract_field
- File:
- Verify:
uv run pytest tests/test_method_spec_loadbearing.py -q→ expected: all pass - On failure: escalate — a genuine field↔spec mismatch means either the contract deviates from the frozen spec (fix the contract) or the spec is defective (STOP: spec changes are a separate GATED commons round, never folded into I2)
- Checkpoint:
git commit -m "test(spec-guard): ingest contract field cross-check vs spec §12 (I2)" - Manifest:
manifest: expected_paths: - tests/test_method_spec_loadbearing.py min_file_count: 1 commit_message_pattern: "^test\\(spec-guard\\): ingest contract field cross-check vs spec .12 \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - shared/ingest-spec.md - shared/method-spec.md must_contain: - path: tests/test_method_spec_loadbearing.py pattern: "test_ingest_spec_documents_every_contract_field"
Step 7: Golden extraction case + byte-for-byte regression test
- Files:
examples/ingest-golden-file/(new directory),tests/test_ingest_golden.py(new) - Changes: Author the golden case per spec §11 layout, LOCALLY in this repo (brief
Non-Goal: commons sharing is a later gated round;
shared/is untouchable). New top-levelexamples/directory (does not exist yet; outside ruff'ssrcconfig — fixture data, not lint targets):manifest.json(file source,root: "fixture"resolved against the manifest's directory, 2 extractions: a plain cost table AND an edge-case table whose quoted CSV cells exercise\,|, an embedded newline, a cell with both\and|, AND number-shaped cells that DISCRIMINATE the text-verbatim reading —007and1.50, strings whose numeric normalization differs byte-wise (7,1.5); Assumption 1 is only pinned if a number-normalizing implementation CANNOT reproduce the golden — a cell like30000.0discriminates nothing sincerepr(30000.0)round-trips identically),fixture/*.csv,ingested-at.txt(2026-07-03T12:00:00Z, one line).expected-bundle/is generated ONCE by the (now fully behaviour-tested) implementation, its content REVIEWED against the spec §5/§6/§7 rules by hand, then frozen in git (brief Assumption 4: golden = regression pin; correctness is carried by Steps 1–6's tests; the freeze happens LAST). Golden test: readingested-at.txt, runmaterializeintotmp_path, then assert (a) the produced file SET equals theexpected-bundle/file set (catches extra AND missing files) and (b) every file'sread_bytes()matches exactly. A secondmaterializerun over the same output must leave every byte unchanged (golden-level idempotence, §10). - Reuses:
BUNDLE_DIR-style module-level fixture-path idiom (tests/test_okf.py,tests/test_step8_promotion_loadbearing.py). - Test first:
- File:
tests/test_ingest_golden.py(new — written BEFOREexpected-bundle/exists; observed RED against the empty expectation, then the reviewed freeze turns it green) - Verifies: byte-for-byte golden regression + file-set equality + repeat-run idempotence
- Pattern:
tests/test_okf.pyfixture-path idiom
- File:
- Verify:
uv run pytest tests/test_ingest_golden.py -q→ expected: all pass; then the full gate battery:uv run pytest(pre-existing suite unchanged-green vs the Step-1 baseline + all new tests),uv run ruff check .→ 0 issues,uv run ruff format --check .→ clean,uv run mypy src→ 0 errors; honesty grep (brief SC8 — scoped to PROSE per the brief: docstrings/comments of the new module and new test files, plus any touched docs):grep -rinE "sql|http|live|commons" src/portfolio_optimiser/ingest.py tests/test_ingest_*.py→ every PROSE hit is a defer-mention (I4/I6/extension point/gated commons round) or a spec citation; hits that are polymorphic-contract SCHEMA IDENTIFIERS (SqlSource,HttpSource,Literal["sql"],Literal["http"],connection_ref,base_url,credential_ref) are expected and allowed — they validate variants per §4 and claim no delivered connector support - On failure: escalate — a golden mismatch after Steps 1–6 are green means a
determinism defect (platform newline, ordering, escaping); diagnose root cause before
re-freezing — NEVER re-freeze to make a mismatch disappear. Cleanup on abandon:
rm -rf examples/ tests/test_ingest_golden.py(new, untracked until checkpoint) - Checkpoint:
git commit -m "test(ingest): golden extraction case examples/ingest-golden-file, byte-frozen (I2)" - Manifest:
manifest: expected_paths: - examples/ingest-golden-file/manifest.json - examples/ingest-golden-file/ingested-at.txt - examples/ingest-golden-file/fixture/costs.csv - examples/ingest-golden-file/expected-bundle/index.md - tests/test_ingest_golden.py min_file_count: 5 commit_message_pattern: "^test\\(ingest\\): golden extraction case examples/ingest-golden-file, byte-frozen \\(I2\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/ingest-spec.md must_contain: - path: tests/test_ingest_golden.py pattern: "read_bytes" - path: examples/ingest-golden-file/ingested-at.txt pattern: "2026-07-03T12:00:00Z"
Execution Strategy
Seven steps, strictly sequential — every step builds on the previous step's code and the session's operating model is single-session TDD (one session = one closed delivery). No parallel waves.
Session 1: I2 complete (this session)
- Steps: 1, 2, 3, 4, 5, 6, 7
- Wave: 1
- Depends on: none
- Scope fence:
- Touch:
src/portfolio_optimiser/ingest.py,tests/test_ingest_*.py,tests/test_ingest_golden.py,tests/test_method_spec_loadbearing.py(additive only),examples/ingest-golden-file/** - Never touch:
src/portfolio_optimiser/okf.py,src/portfolio_optimiser/run.py,src/portfolio_optimiser/verdicts.py,src/portfolio_optimiser/contracts.py,shared/**,tests/conftest.py,pyproject.toml(no new deps needed)
- Touch:
Execution Order
- Wave 1: Session 1 (sequential steps 1→7)
Grouping rules applied
- All steps share
src/portfolio_optimiser/ingest.py→ one session by necessity. - TDD dependency chain (contract → connector → materializer → semantics → seams → guard → golden freeze) forbids reordering.
Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
Add a link-removal primitive to okf.py |
Symmetric API next to link_in_index |
Violates the brief's hard constraint (navigability criterion is "consumable via UNCHANGED okf.py"); widens D7-shared surface mid-program | Brief Constraints; removal lives in ingest.py |
Define manifest models in contracts.py |
One contracts home | contracts.py:25 imports backends → agent_framework; ingest module would fail its MAF-free guard; also violates surgical-scope |
Brief Constraints (MAF-free ingest seam) |
Reuse okf.write_concept_file for file writes |
Maximum reuse, path-safety built in | Path.write_text translates newlines per platform and doesn't guarantee exactly one trailing \n — §5 LF-only byte-determinism at risk |
Risk assessment (High); ingest writes bytes itself, still via safe_resolve + render_frontmatter |
Validate only the file source variant in I2 |
Smaller Step 1 | Spec §4's schema is polymorphic; I4 would retrofit the union; brief assumption already commits to schema breadth with connector-narrowness | Brief Assumption 1 (schema polymorphic, connector file-only) |
Hand-author expected-bundle/ before implementing |
"Purer" golden TDD | Byte-level hand-typing of hash stamps and escaping is error-prone; correctness is already carried by Steps 1–6 behaviour tests; golden is a regression PIN (spec §11) | Brief Assumption 4 (generate once, review, freeze LAST) |
| Remove + re-append index links for re-created targets | Simpler §6 filter | Moves ingest links to index end whenever anything sits between; unnecessary byte-churn breaks repeat-run stability | Idempotence (§10): keep links in place for re-created targets — WITH the Step-4(f) in-place label refresh, so a changed title never leaves a stale label (plan-critic major finding) |
Test Strategy
- Framework: pytest ≥8, plain sync tests (ingest is deterministic — no asyncio), no new
markers, no conftest changes, no mocking library (repo convention: hand-built doubles;
ingest needs only
tmp_path+ small CSV builders). - Existing patterns: fail-fast contract tests (
tests/test_contracts.py), detach-RED load-bearing modules (tests/test_step8_promotion_loadbearing.py), navigability round-trips (tests/test_okf.py), spec cross-check (tests/test_method_spec_loadbearing.py). - New tests in this plan: ~30 tests across 4 new files + 1 extended file.
Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | tests/test_ingest_manifest.py |
§4 schema fail-fast, verdict reservation, grammar, fail-before-source-access | tests/test_contracts.py |
| Unit | tests/test_ingest_materialize.py |
connector boundary/cap/escaping, §5 frontmatter order, LF bytes, §6 index semantics, collision gate, idempotence | tests/test_okf.py |
| Load-bearing | tests/test_ingest_loadbearing.py |
the four §11 seams + MAF-free module guard, each detach-RED-proven | tests/test_step8_promotion_loadbearing.py |
| Spec guard | tests/test_method_spec_loadbearing.py |
§12 field-level contract↔spec cross-check | its own test 3 |
| Golden | tests/test_ingest_golden.py |
byte-for-byte regression + file-set equality + repeat-run idempotence | tests/test_okf.py fixture idiom |
Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| Critical | §6 link removal implemented as substring match deletes a promoted verdict's or curated link | new removal filter in ingest.py; okf.py:168-181 is append-only |
Corrupted index.md; §11 re-ingest seam broken |
Exact whole-line managed-link match, only for targets deleted this run and not re-created (Step 4); re-ingest safety test uses the REAL promote_verdict (Step 5) |
| Critical | Crash mid-replace leaves half-materialized bundle (§5 sequence is non-atomic) | materialize orchestration |
Loop navigates a broken bundle until re-run | Full in-memory staging before first disk mutation; index update last; recovery = idempotent re-run (§10) — documented, not hidden |
| High | Reformatting numeric-looking CSV text (e.g. "30000.0", "007") breaks golden bytes |
renderer, spec §5 reading | Golden divergence; sibling (I3) mismatch | Pinned decision: CSV cells text-verbatim; §5 number rules are typed-sql territory (I4). Recorded as Assumption 1; golden fixture pins it |
| High | Escape-order bug (` | before`) double-escapes |
renderer | Silent cell corruption |
| High | Platform newline translation / trailing-newline drift breaks byte-determinism | file writes | Golden mismatch cross-platform | Bytes writer with explicit \n for concept files (Step 3); read_bytes() assertions; okf.write_concept_file not used for writes. Index appends stay on the frozen link_in_index → LF-ness there is platform-scoped (Assumption 6), nil on macOS/Linux |
| Medium | BOM/CRLF/embedded-newline CSV mishandling | connector | First header cell polluted; golden flaky | utf-8-sig + newline="" + csv.reader (pinned); BOM + CRLF + embedded-newline unit fixtures |
| Medium | Stamp misclassification (string "true" vs bool) deletes curated file or strands stale ingest files |
ownership scan; okf.py:46 returns strings |
§3 violation | Classify on generated == "true" AND ingest_manifest present; collision gate test (unstamped ingest-foo.md → error, bundle unmodified) |
| Medium | link_in_index raises when index.md missing |
okf.py:176 |
Mid-run crash on fresh bundle | Create index.md (bundle_summary body) before any linking (Step 4 order) |
| Medium | Title containing ](x.md) injects a parseable index link |
index label = raw title; okf.py:28 |
Spurious navigation targets (skipped if missing — fail-closed) | Spec-exact validation only (single-line); limitation documented in module docstring — spec change would be a gated commons round |
| Low | Golden re-frozen to hide a real determinism defect | Step 7 | Regression pin becomes a lie | Step 7 On-failure rule: diagnose root cause; NEVER re-freeze to green a mismatch |
Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|---|---|---|
| 1 | §5's integer/float/NULL rendering rules apply to typed (sql) values; file/CSV cells are text-verbatim (escape-only) |
Spec prose lists both under one bullet; the frozen spec cannot be amended inside I2 | Golden bytes + sibling (I3) diverge; if I3 reads it differently, a GATED commons clarification round is triggered — the golden fixture makes the chosen reading explicit and shareable |
| 2 | Fresh index.md is created WITHOUT frontmatter (body = bundle_summary only) — spec §6 is silent on index frontmatter |
Spec silence; curated example (bygg-energi-mikro/index.md) has type: index but is hand-authored, not generated |
Cosmetic divergence pinned by the golden; navigate_bundle handles both (frontmatter-less parse returns {}) — verified okf.py:38-39 |
| 3 | Relative root resolves against the manifest file's directory |
Spec §4 does not name the resolution base | Golden becomes cwd-dependent if wrong; pinned decision documented in docstring + spec cross-check exercise for I3 |
| 4 | utf-8-sig reading (BOM tolerated and stripped) is the correct connector-side choice |
Spec silent on encoding | A BOM-carrying golden fixture in the sibling could differ; our golden contains no BOM in fixture/, so the shared surface is unaffected |
| 5 | Empty CSV (no header), ragged rows, and missing root/query paths are IngestError fail-fast (never silent coercion/leaked FileNotFoundError) |
Spec §5's MUST-fail clause addresses typed values; these input-shape failures are unaddressed spec silence | I3 sibling could choose differently; the decisions are documented in the module docstring and exercised by unit tests so the I3 cross-check surfaces any divergence — if the sibling disagrees, a GATED commons clarification round resolves it |
| 6 | LF-only bytes are platform-scoped for index.md: concept files go through the ingest byte-writer (LF everywhere), but index appends REUSE the frozen okf.link_in_index, whose Path.write_text translates newlines per platform (and read_text universal-newline-decodes a CRLF curated index on round-trip) |
The brief mandates reusing link_in_index and forbids changing okf.py — the platform behaviour is frozen with it |
On the LF-only development/target platform (macOS/Linux) impact is nil and the golden holds; a Windows deployment would need its own golden verification — documented limitation, not a universal cross-platform claim |
6 items — above the template's 3-item investigation threshold, accepted deliberately: all are spec-SILENCE or platform-scope points (not unknowns about this codebase) pinned by the golden fixture and/or documented decisions — exactly the mechanism the program uses to surface such choices to I3; none blocks execution.
Verification
Per-step manifests verify automatically during execution. These are the end-to-end checks crossing step boundaries — they restate the brief's Success Criteria (SC1–SC8).
- SC1:
uv run pytest tests/test_ingest_golden.py -q→ all pass (byte-for-byte + repeat-run idempotence) - SC2–SC5:
uv run pytest tests/test_ingest_loadbearing.py -q→ all pass, AND the session log records four detach→RED→reattach proofs (provenance, navigability, verdict reservation, re-ingest safety) - SC3 (unchanged okf.py):
git diff main@{session-start} -- src/portfolio_optimiser/okf.py→ empty output - SC6:
uv run pytest tests/test_ingest_manifest.py -q→ all pass with no network available (key assumption 2 closed) - SC7:
uv run pytest→ exit 0, pre-existing suite unchanged-green vs the baseline recorded in Step 1 (pre-changeuv run pytest -qsummary in the session log) + new tests;uv run ruff check .→ exit 0;uv run ruff format --check .→ clean;uv run mypy src→ exit 0 - SC8:
grep -rinE "sql|http|live|commons" src/portfolio_optimiser/ingest.py tests/test_ingest*.py→ every PROSE hit (docstrings/comments — the brief's scope) is a defer-mention (I4/I6/extension point/gated) or spec citation; polymorphic-contract schema identifiers (SqlSource/HttpSource/Literal["sql"|"http"]/connection_ref/base_url/credential_ref) are allowed hits claiming no delivered support - Layer hygiene:
uv run pytest tests/test_okf.py tests/test_method_spec_loadbearing.py -q→ all pass (MAF-free + spec guards intact)
Estimated Scope
- Files to modify: 1 (
tests/test_method_spec_loadbearing.py, additive) - Files to create: 5 code/test files (
src/portfolio_optimiser/ingest.py,tests/test_ingest_manifest.py,tests/test_ingest_materialize.py,tests/test_ingest_loadbearing.py,tests/test_ingest_golden.py) + theexamples/ingest-golden-file/fixture tree (~6 small files) - Complexity: medium
Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 90 | strict TDD dependency chain; Step-2/3 logging ownership clarified post-review |
| Step quality | 0.20 | 88 | 1 module + 1 test file per step; all 3 critic majors fixed in revision |
| Coverage completeness | 0.20 | 88 | all 8 SCs + §11 seams mapped; title-refresh, fresh-dir, missing-source cases added post-review |
| Specification quality | 0.15 | 88 | 5 spec-silence points pinned as explicit assumptions |
| Risk & pre-mortem | 0.15 | 88 | risk table incl. 2 critical collisions; review added revert/mkdir/staleness fixes |
| Headless readiness | 0.10 | 85 | On-failure clauses corrected for untracked files; baseline capture now explicit in Step 1 |
| Manifest quality | 0.05 | 88 | Step-7 manifest now pins the byte-frozen golden artifacts |
| Weighted total | 1.00 | 88 | Grade: B+ |
Adversarial review:
- Plan critic: APPROVE_WITH_NOTES (81/100, grade B) — 0 blockers, 3 major, 10 minor; all 3 majors and 9 of 10 minors resolved in the revision below (the remaining minor — SC4's filename-namespace clause being structurally covered — is accepted as honest by-construction coverage, matching scope-guardian's reading).
- Scope guardian: ALIGNED — 0 creep, 0 dependency issues, 3 gaps (1 major: SC8 grep scope; 2 minor: baseline step, SC4 structural note); the major and the baseline gap are resolved below.
Revisions
Added by adversarial review (plan-critic + scope-guardian, deduped 16 findings).
| # | Finding | Severity | Resolution |
|---|---|---|---|
| 1 | Step 1 On-failure git checkout -- cannot remove untracked new files |
major | Step 1 revert → rm -f; Step 2 revert split (tracked checkout + rm new test file); Steps 5/7 got explicit cleanup-on-abandon commands |
| 2 | Changed extraction title on re-ingest left a stale index label (kept-in-place links) |
major | Step 4 gained (f) in-place label refresh preserving line position + a changed-title test; Alternatives row updated |
| 3 | materialize wrote bytes without creating bundle_dir (byte-writer has no mkdir) |
major | Step 3 now mkdirs bundle_dir first + fresh-nested-dir test added |
| 4 | SC8 whole-file grep unachievable — schema identifiers (SqlSource etc.) would false-positive |
major | SC8 (Step 7 + Verification) rescoped to PROSE hits per the brief, with schema identifiers as an explicitly allowed category |
| 5 | ingested_at validation via fromisoformat breaks on py3.10 (Z unsupported) |
minor | Pinned to an explicit regex; py3.10 trap documented in Step 3 |
| 6 | Missing root/query leaked bare FileNotFoundError |
minor | Wrapped in IngestError + tests added (Step 2) |
| 7 | render_frontmatter collapses title whitespace → frontmatter/label divergence |
minor | title whitespace-normalized at validation so both representations agree (Step 1) |
| 8 | CRLF could become two spaces | minor | CRLF replaced as one unit (\r\n first) + explicit CRLF test (Step 2) |
| 9 | Step 2 logged ingested_at it does not own |
minor | Connector is pure; §8 logging moved to materialize (Step 3) with the caplog test |
| 10 | HttpSource credential validator under-defined |
minor | Defined as urlsplit userinfo check; query-param heuristics explicitly out of scope (Step 1) |
| 11 | Ragged/empty-file decisions unpinned vs I3 sibling | minor | Added as Assumption 5 (documented + unit-tested divergence surface) |
| 12 | No baseline-capture step for SC7 | minor | Step 1 now opens with a pre-change uv run pytest -q baseline recording; SC7 references it |
| 13 | Step 7 manifest omitted the byte-frozen golden artifacts | minor | expected_paths now includes fixture/costs.csv + expected-bundle/index.md, min_file_count: 5 |
| 14 | Invocation surface (API vs CLI) unstated | minor | Recorded in Step 3: programmatic materialize IS the I2 surface; CLI deferred |
| 15 | SC4 filename-namespace clause covered by construction, not detach-testable | minor | Accepted as-is: honestly documented invariant (both reviewers concur); load-bearing bite is the okf_type check |
Adversarial Pass 2 (independent second pass, v5.1.1 high-effort)
Mandated by the brief's effort: high plan signal. The prescribed gemini-bridge pass
was SUBSTITUTED with an independent fresh-context contrarian reviewer: Gemini deep-research
result retrieval is verified broken as of 2026-07-03 (research completes, result
unretrievable — operator memory), so a bridge run would burn time with no retrievable
output. The substitute preserves the intent — an independent perspective that does not
re-tread Pass 1's dedup space (it was given Pass 1's findings as excluded ground).
Verdict: PASS2_FINDINGS(5) — 0 blocker, 3 major, 2 minor. All five incorporated:
| # | Finding | Severity | Resolution |
|---|---|---|---|
| P2-1 | Re-ingest safety test ran materialize twice with an IDENTICAL manifest — the §6 removal filter never fired, so the plan's own Critical risk (loose filter deleting a promoted/curated link) was untested (green-but-dead) |
major | Step 5(4) rewritten: second run uses a REDUCED manifest so removal fires with the promoted verdict + curated managed-form links present; over-match → RED |
| P2-2 | Golden fixture had no cell discriminating text-verbatim vs number-normalization — Assumption 1 was not actually pinned (30000.0 round-trips identically) |
major | Step 7 fixture now mandates 007 and 1.50 cells whose normalized forms differ byte-wise |
| P2-3 | Index writes route through the frozen link_in_index (Path.write_text) — the "LF-only on every platform" claim contradicted the plan's own reason for avoiding write_concept_file |
major | Determinism claim rescoped: concept files LF-guaranteed via byte-writer; index.md LF-ness platform-scoped (nil on macOS/Linux) — Assumption 6 + risk-row update |
| P2-4 | Header-cell escaping unspecified (spec §5 separates "column names" from "cell values") — a ` | `/newline header would break the table | minor |
| P2-5 | Step 4(f) label refresh did not restate the removal filter's full-line/ingest-target anchoring | minor | Step 4(f) now requires the same exact managed-line anchoring; non-conforming lines untouched |
Probed and clean (per the reviewer): §12 cross-check test CAN go red; file-deletion
ownership seam holds against real promote_verdict; no invariant violations (okf.py
unchanged, shared/ untouched, MAF-free, no new deps); escape order correct; golden
tmp_path second run does exercise re-ingest-over-existing.