/trekplan for the http source family (MAF-only). Validator green (4 steps, plan_version 1.7). 5 exploration agents; scope-guardian ALIGNED; plan-critic REVISE -> all resolved (1 blocker: gate false-green, now asserts both branches so it goes RED on either detach; 5 major inc. materialize-only flag scoping verified vs run.py, fence-collision -> fail-fast, create_sdk_mcp_server verified vs official Agent SDK docs, live-grep scoped to I6 artifacts, golden oracle hand-derived in-plan). High-effort gemini-bridge pass: re-confirmed BROKEN (retrieval bug + deprecated model) -> substituted plan-critic + primary-source checks (CommonMark 4.5 fence, CPython str.splitlines -> \n-only max_rows count). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015U5UK6gVFJNvTEdDcyUb8W
41 KiB
I6 — HTTP source connector as a demonstrated extension point (MAF-only)
Plan quality: A (92/100) — APPROVE_WITH_NOTES
Generated by trekplan v5.9.1 on 2026-07-04 —
plan_version: 1.7
Context
The ingest layer implements the file/CSV (I2/I3) and sql (I4/I5) source families on
both stacks, built from the frozen shared/ingest-spec.md. That spec (§1, §4) declares
type: "http" an OPTIONAL extension point: the schema already validates HttpSource
(src/portfolio_optimiser/ingest.py:84-100), but materialize refuses it fail-fast
(ingest.py:394-398, "gated extension point (I6) with no connector"). I6 activates that stub
to demonstrate — honestly and end-to-end — HOW a third, network-transport source family plugs
into the same connector / materialization / gate contracts, WITHOUT consuming the platform's
"first live source" milestone. It realizes two locked framework principles at once: the
90%-principle (generic core + clear extension points, do not chase the last 10%) and the
security frame's "local-only default, no silent egress" (§8) — a network source added strictly
behind an explicit run-flag the manifest itself cannot grant. MAF-only; okf.py, shared/,
and the frozen spec are untouched.
Architecture Diagram
graph TD
subgraph "I6 changes in ingest.py"
M["materialize(...,<br/>allow_network=False,<br/>http_get=None)"]:::mod
GATE{"isinstance(HttpSource)<br/>AND not allow_network?"}:::new
DISP["dispatch loop<br/>(file / sql / http branch)"]:::mod
RH["read_http(base_url, query,<br/>*, max_rows, credential_ref, get)"]:::new
UG["_urllib_get<br/>(stdlib GET — ONLY socket path)"]:::new
FENCE["_render_fenced_block<br/>(verbatim, ``` fence)"]:::new
end
subgraph "reused UNCHANGED"
RC["_render_concept_file<br/>(7-key frontmatter)"]:::keep
WB["_write_bytes / ownership /<br/>collision / index"]:::keep
OKF["okf.render_frontmatter /<br/>link_in_index"]:::keep
end
TEST["golden + load-bearing tests<br/>inject a canned http_get<br/>(no socket, no creds)"]:::test
M --> GATE
GATE -->|"no flag → IngestError"| REFUSE["fail-fast (spec §8)"]:::new
GATE -->|"allow_network=True"| DISP
DISP --> RH --> UG
RH -->|"body text"| FENCE --> RC --> WB --> OKF
TEST -.injects.-> RH
classDef new fill:#2d6,stroke:#164,color:#000
classDef mod fill:#fd6,stroke:#a70,color:#000
classDef keep fill:#ddd,stroke:#888,color:#000
classDef test fill:#8cf,stroke:#048,color:#000
Codebase Analysis
- Tech stack: Python ≥3.10, Pydantic (validation/IR), stdlib
urllib(transport),pytest(+ byte-for-byte golden harness),ruff/mypy,uv. Zero new runtime deps. - Key patterns: one connector fn per source type returning renderable content; a
per-source-type dispatch inside
materialize; credential/connection resolution by env-var name (never from the manifest); a committed golden case per type; load-bearing tests that go RED when a seam is detached; spec-silent decisions pinned in the module docstring + frozen in golden bytes. - Relevant files (verified):
src/portfolio_optimiser/ingest.py—HttpSourceschema (:84-100), the gate to replace (:394-398), the dispatch loop (:408-426),read_sqltemplate (:233-278),_render_concept_file7-key frontmatter (:304-318,source_system = manifest.source.id),_write_bytes(:321-327), ownership/collision/index machinery (:428-470), module docstring pin-block (:1-31).tests/test_ingest_golden_sql.py— byte-for-byte golden harness (_bundle_bytes, file-set equality, §10 second-run idempotence).tests/test_ingest_sql_loadbearing.py/tests/test_ingest_sql.py— detach-RED and connector-unit templates.tests/test_ingest_materialize.py:245-253—test_http_source_has_no_connector(stale after I6: still green under default-no-flag, but its "no connector" comment becomes false).examples/ingest-golden-sql/— golden layout to mirror (manifest.json,fixture/,ingested-at.txt,expected-bundle/{index.md, ingest-*.md})..claude/projects/2026-07-04-i4-ingest-sql-maf/build_fixture.py— throwaway fixture-builder pattern.docs/extending.md— already exists (bilingual dev doc: Norwegian headers, English prose); I6 appends an http source-family section + the D7 hook pointer.
- Reusable code (leverage unchanged):
_render_concept_file,_write_bytes, the ownership scan / collision gate / index generation (ingest.py:428-470),load_manifest,okf.render_frontmatter/okf.link_in_index. Explicitly NOT reused for http:render_table/_escape_cell/_sql_value_to_text(http body is verbatim inside a fenced code block, not a table). - External tech (researched): none — 0 research topics (frozen spec fully defines http
semantics; I4 SQL is the direct in-repo precedent; stdlib mock). The single external-adjacent
item (
create_sdk_mcp_servercurrency for the docs pointer) is a doc-time verification, not a planning input. - Recent git activity:
bc52c12(I6 brief),1b7612b/d7e5f2f(I4 SQL, the direct precedent),e4ee8bd→994f5be(I2 CSV). Branchmain, clean.
Implementation Plan
Each step is test-first (Iron Law). Steps are ordered by dependency: connector → gate+dispatch → golden → docs.
Step 1: Implement read_http connector + injectable transport seam (docstring-pinned)
- Files:
src/portfolio_optimiser/ingest.py,tests/test_ingest_http.py(new) - Changes: Add, near
read_sql(ingest.py:233):- A transport-seam type alias
HttpGet = Callable[[str, str | None], str]((url, credential) -> response body text); addfrom collections.abc import Callableandfrom urllib.request import Request, urlopen+from urllib.error import URLErrorto imports. def _urllib_get(url: str, credential: str | None) -> str— the stdlib GET and the only socket path: build aRequest(url, headers=..., method="GET"), attachAuthorization: Bearer {credential}iffcredential is not None,urlopen, read, decode UTF-8. WrapURLError/OSError/UnicodeDecodeErrorasIngestError. Do NOT pre-add a# noqa: S310—pyproject.tomlselects noflake8-bandit(S) rules, so S310 never fires and a dead noqa would tripRUF100if it is ever enabled; add the noqa ONLY ifuv run ruff check .actually flags theurlopenat Verify.def read_http(base_url, query, *, max_rows, credential_ref=None, get: HttpGet = _urllib_get) -> strmirroringread_sql: resolvecredential_refviaos.environ.get(present-but-unset/empty →IngestErrorfail-fast, mirroringingest.py:249;None→ no auth); buildurl = base_url.rstrip("/") + "/" + query.lstrip("/"); callget(url, credential); normalize CRLF→LF; fence-collision fail-fast — if any normalized line, after stripping up to its leading spaces (line.lstrip(" ")), begins with```(a fence marker — note CommonMark permits a closing fence indented ≤3 spaces, so a leading-space check is required), raiseIngestError(a body that cannot be safely embedded in a fenced code block is an ERROR, never silently-corrupted markdown — the module's no-silent-coercion discipline + §8 fail-fast); enforce the cap on the\n-only line count of the CRLF-normalized text —n = normalized.count("\n"); line_count = n if (normalized == "" or normalized.endswith("\n")) else n + 1;line_count > max_rows → IngestError(never truncation, §8). Do NOT usestr.splitlines()for the cap: CPythonsplitlines()splits on the extended Unicode set (\v,\f,\x1c-\x1e,\x85,\u2028,\u2029), so a body with a vertical-tab or line-separator would be over-counted vs the LF-only rendered file (verified 2026-07-04). The\n-only count matches the rendered body and still treats a single trailing newline as no phantom line. Return normalized text. Pure: no writes, no logging (materialize owns §8 log).def _render_fenced_block(body: str) -> str→f"\``\n{body.rstrip(chr(10))}\n```\n"` (verbatim inside a fenced code block; LF-only; exactly one trailing newline).- Docstring pin (module docstring,
ingest.py:23-30block): append the http (I6) pinned decisions, mirroring the I4 sql sentence: transport is an injectablegetcallable (default_urllib_get, the only socket);credential_refnames an env secret resolved at run time (Bearer), never from the manifest, never logged, never stamped in frontmatter; URL =base_url(one trailing slash stripped) +/+query(leading slash stripped); body = response decoded UTF-8, CRLF→LF, trailing newlines stripped, rendered verbatim in afenced block (NOT table-escaped); `max_rows` caps response **line count** (`len(normalized.splitlines())`), fail-fast; a response line beginning with `→IngestError` fail-fast (cannot be safely fenced — never silent corruption, consistent with the no-silent-coercion frame; the fixed 3-backtick fence is deliberate, NOT a silent fallback).
- A transport-seam type alias
- Reuses:
read_sqlstructure (ingest.py:233-278) — env-resolve → fetch → cap →IngestErrorwrap; the CSV/SQL streaming-cap idiom (ingest.py:193-197,:270-274). - Test first:
- File:
tests/test_ingest_http.py(new) - Verifies (each injects a canned
get, no socket, no creds unless monkeypatched):read_httpreturns the injected body verbatim and callsgetwith the joined URL; a body of exactlymax_rowslines → returns normally (boundary pass);max_rows + 1lines →IngestError(never truncated) [SC max_rows]; a body with a line beginning```→IngestError(fence-collision fail-fast);credential_refpresent but env unset →IngestError;credential_refset → the injectedgetreceives the resolved secret (secret comes from env, never the manifest);read_http'sgetdefault is_urllib_get(inspect.signature— proves the stdlib default without a socket). - Pattern:
tests/test_ingest_sql.py(connector-unit style; localget-builder mirroring_make_dblocality — do NOT add toconftest.py).
- File:
- Verify:
uv run pytest tests/test_ingest_http.py -q→ expected: all pass (offline). - On failure: revert —
git checkout -- src/portfolio_optimiser/ingest.py tests/test_ingest_http.py - Checkpoint:
git commit -m "feat(ingest): http read_http connector + injectable transport seam (I6)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_http.py min_file_count: 2 commit_message_pattern: "^feat\\(ingest\\): http read_http connector \\+ injectable transport seam \\(I6\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/ingest-spec.md must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "def read_http" - path: src/portfolio_optimiser/ingest.py pattern: "_render_fenced_block" - path: tests/test_ingest_http.py pattern: "read_http"
Step 2: Gate http behind the allow_network run-flag + wire the dispatch branch
- Files:
src/portfolio_optimiser/ingest.py,tests/test_ingest_http_loadbearing.py(new),tests/test_ingest_materialize.py - Changes:
materializesignature (ingest.py:373-375): add keyword-onlyallow_network: bool = Falseandhttp_get: HttpGet | None = None(both defaulted → all existing file/sql callers stay backward-compatible;run_project/run_portfolioare NOT touched — verified they never callmaterialize). Extend the docstring to note the §8 network opt-in flag is a run argument.- Replace the unconditional refusal (
ingest.py:393-398) with the gate:if isinstance(source, HttpSource) and not allow_network: raise IngestError(...)— refuse fail-fast before any source call, message citing §8 ("the manifest cannot grant itself network access"). This exactand not allow_networkclause is the load-bearing seam. - Restructure the dispatch loop (
ingest.py:408-426) so each branch setsbodyand arow_count:FileSource→read_csv+render_table,row_count = len(rows);elif isinstance(source, HttpSource)→get = http_get if http_get is not None else _urllib_get;text = read_http(source.base_url, extraction.query, max_rows=extraction.max_rows, credential_ref=source.credential_ref, get=get);body = _render_fenced_block(text);row_count = len(text.splitlines());else(Sql) →read_sql+render_table,row_count = len(rows). Keep the single_LOGGER.info§8 log (ingest.py:416-421) loggingrows=row_count(line count for http). The stale "http already refused above" comment (ingest.py:412) is removed. tests/test_ingest_materialize.py:245-253: updatetest_http_source_has_no_connector's comment/intent — the default-no-flag path still raisesIngestError, but the wording "no connector" is now false; reword to "http is refused without the per-run network flag" (assertion stays green; comment corrected). Do not delete — it is the default-refuse smoke.
- Reuses:
_render_concept_file,_write_bytes, ownership/collision/index machinery (ingest.py:428-470) — all unchanged; the http bundle flows through the identical §3/§6 disk phase. - Test first:
- File:
tests/test_ingest_http_loadbearing.py(new) - Verifies (detach-RED seams, each maps to a Success Criterion):
- Network gate [SC1] — asserts BOTH branches on the SAME manifest + SAME recording
get(this is what makes it genuinely detach-RED):- Refuse branch:
materialize(http_manifest, out, ingested_at=…, allow_network=False, http_get=recording_get)→ assertIngestErrorandrecording_getwas never called (refuse before any source access, §8). - Allow branch:
materialize(http_manifest, out, ingested_at=…, allow_network=True, http_get=recording_get)→ assert it succeeds (writesingest-*.md) andrecording_getwas called. Why both: the seam isif isinstance(source, HttpSource) and not allow_network: raise. Removing the whole clause → the refuse assertion fails (get called, no error) → RED; removing justand not allow_network(reverting to the unconditional refusal that exists today) → the allow assertion fails (raises when it should fetch) → RED. A refuse-only test would false-green on the second detach. (Inject a recordinggetthat returns a valid payload — never the default_urllib_get, whose dead socket would false-green the refuse branch.)
- Refuse branch:
- Fenced-not-table [§5 http seam]: payload with a raw
|and\; assert the body is ``` -fenced and the pipe survives un-escaped ("\\|" not in body, raw|present). RED if the http path degrades torender_table. - Navigability [§2/§11]: drive the http bundle through unchanged
okf.navigate_bundle/bundle_context; reachable via index link, classified byokf_type, provenance rides through. RED when index linking detaches. - Over-cap writes nothing [SC max_rows]: over-cap payload →
IngestErrorand noingest-*.mdwritten (memory-staging). RED if silent truncation replaces the raise.
- Network gate [SC1] — asserts BOTH branches on the SAME manifest + SAME recording
- Pattern:
tests/test_ingest_sql_loadbearing.py(detach discipline) + the ordering proof intests/test_ingest_manifest.py:178.
- File:
- Verify:
uv run pytest tests/test_ingest_http_loadbearing.py tests/test_ingest_materialize.py -q→ expected: all pass. Detach-proof (manual, at review): the gate test goes RED on BOTH detaches — (a) delete the wholeif isinstance(source, HttpSource) and not allow_network:block → refuse branch RED; (b) reduce it to the unconditionalif isinstance(source, HttpSource):→ allow branch RED. - On failure: revert —
git checkout -- src/portfolio_optimiser/ingest.py tests/test_ingest_http_loadbearing.py tests/test_ingest_materialize.py - Checkpoint:
git commit -m "feat(ingest): gate http behind allow_network run-flag + dispatch (I6)" - Manifest:
manifest: expected_paths: - src/portfolio_optimiser/ingest.py - tests/test_ingest_http_loadbearing.py - tests/test_ingest_materialize.py min_file_count: 3 commit_message_pattern: "^feat\\(ingest\\): gate http behind allow_network run-flag \\+ dispatch \\(I6\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - src/portfolio_optimiser/run.py - shared/ingest-spec.md must_contain: - path: src/portfolio_optimiser/ingest.py pattern: "allow_network" - path: src/portfolio_optimiser/ingest.py pattern: "and not allow_network" - path: tests/test_ingest_http_loadbearing.py pattern: "allow_network=False"
Step 3: Author the byte-deterministic golden case examples/ingest-golden-http/
- Files:
examples/ingest-golden-http/manifest.json(new),examples/ingest-golden-http/fixture/status(new),examples/ingest-golden-http/fixture/report(new),examples/ingest-golden-http/ingested-at.txt(new),examples/ingest-golden-http/expected-bundle/(new, generated + hand-reviewed),tests/test_ingest_golden_http.py(new),.claude/projects/2026-07-04-i6-ingest-http-maf/build_fixture.py(new, throwaway) - Changes:
-
manifest.json:source={"type": "http", "id": "status-api", "base_url": "https://api.example.test/v1", "credential_ref": null}(.testreserved TLD +credential_ref: null→ runs with no network, no credentials, proving SC offline);bundle_summary; two extractions —{"id": "status", "title": "Service status", "query": "status", "okf_type": "dataset", "max_rows": 10},{"id": "report", "title": "Ops report", "query": "report", "okf_type": "reference", "max_rows": 10}. -
fixture/status(1 line, discriminates fenced-verbatim vs table-escape — contains raw|and\):{"service": "billing", "state": "degraded | partial", "path": "c:\temp\cache"} -
fixture/report(3 lines, ≤ max_rows):baseline ok/pipe | and backslash \ kept verbatim/end of report. -
ingested-at.txt:2026-07-04T12:00:00Z+ trailing newline (byte-write, §5 format). -
build_fixture.py: writes the two payload files +ingested-at.txt(mirrors the I4 builder; payloads are LF-only so the golden is platform-stable). Documents regenerability. Author the payload bytes as raw bytes / raw-string literals — in a normal Python string literal\tis a TAB and\ca literal backslash, so the intended verbatim backslashes inc:\temp\cache(andbackslash \) would be silently corrupted; write withrb"..."/.write_bytes(b"...")orr"..."so the byte the fenced-not-escaped test depends on survives exactly. -
expected-bundle/: generate by runningmaterializeagainst the fixture with a cannedget(mapurl.rsplit("/",1)[-1]→fixture/<name>),allow_network=True, then verify the generated bytes against the spec-derived expectation below BEFORE committing — this is an agent check against an independent oracle, not a human gate, so a headlessclaude -prun performs it too. The non-circular correctness oracle is carried here in the plan (and, independently, by the Step-2 load-bearing tests, which hand-assert the fenced-not-escaped body, the 7-key frontmatter, and navigability — so correctness is proven by hand-written assertions, not by comparing code output to code output). If the generatedingest-status.mddiverges from the expectation below in any way OTHER than theingest_manifesthash, escalate (do not commit a wrong oracle).Hand-derived
expected-bundle/ingest-status.md(from §5/§7 +fixture/status; every field except the manifest-hash is independently derived):--- type: dataset title: Service status source_system: status-api source_query: status ingested_at: 2026-07-04T12:00:00Z ingest_manifest: manifest@<sha256(manifest.json)[:16]> generated: true --- ```‹fence› {"service": "billing", "state": "degraded | partial", "path": "c:\temp\cache"} ```‹fence›Note the pipe and backslashes are un-escaped (verbatim) inside the fence — the exact discriminator vs
render_table.index.md=bundle_summary+- [Service status](ingest-status.md)- [Ops report](ingest-report.md). Theingest_manifesthash is the only computed field (SHA-256 of the committedmanifest.jsonbytes) — frozen once committed.
-
- Reuses: golden layout + harness from
examples/ingest-golden-sql/andtests/test_ingest_golden_sql.py; builder shape from the I4build_fixture.py. - Test first:
- File:
tests/test_ingest_golden_http.py(new) - Verifies [SC golden determinism]: build the canned
getfromfixture/;materialize(GOLDEN/manifest.json, out, ingested_at=<ingested-at.txt>, allow_network=True, http_get=fake); file-set equality, per-file byte compare againstexpected-bundle/, second-run idempotence (§10). No credential env set (credential_ref: null). - Pattern:
tests/test_ingest_golden_sql.py:30-47(swap themonkeypatch.setenvDSN for the injectedget).
- File:
- Verify:
uv run pytest tests/test_ingest_golden_http.py -q→ expected: pass, byte-identical. - On failure: revert —
git checkout -- examples/ingest-golden-http tests/test_ingest_golden_http.py .claude/projects/2026-07-04-i6-ingest-http-maf/build_fixture.py && rm -rf examples/ingest-golden-http - Checkpoint:
git commit -m "test(ingest): http golden extraction, byte-deterministic (I6)" - Manifest:
manifest: expected_paths: - examples/ingest-golden-http/manifest.json - examples/ingest-golden-http/ingested-at.txt - examples/ingest-golden-http/fixture/status - examples/ingest-golden-http/fixture/report - examples/ingest-golden-http/expected-bundle/index.md - examples/ingest-golden-http/expected-bundle/ingest-status.md - examples/ingest-golden-http/expected-bundle/ingest-report.md - tests/test_ingest_golden_http.py - .claude/projects/2026-07-04-i6-ingest-http-maf/build_fixture.py min_file_count: 9 commit_message_pattern: "^test\\(ingest\\): http golden extraction, byte-deterministic \\(I6\\)$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/ingest-spec.md must_contain: - path: examples/ingest-golden-http/expected-bundle/ingest-status.md pattern: "```" - path: examples/ingest-golden-http/expected-bundle/ingest-status.md pattern: "generated: true" - path: examples/ingest-golden-http/manifest.json pattern: "\"type\": \"http\""
Step 4: Document the http extension-point pattern + D7 SDK hook pointer in docs/extending.md
- Files:
docs/extending.md - Changes: Append a new section (bilingual house style: Norwegian header, English prose)
documenting the http source family as the worked extension-point example: the manifest
type: "http"contract (base_urlno embedded credentials,credential_refby env-name), the run-argument network opt-in (materialize(..., allow_network=True); the manifest cannot grant network — §8), the injectable transport seam (http_get, default stdliburllib), and the "verbatim in a fenced code block" body. Add the D7 sibling-hook pointer: an MCP-based connector is "an extension of this family" (§4), and on the Claude Agent SDK side the in-process server hook iscreate_sdk_mcp_server(name, version="1.0.0", tools=...) -> McpSdkServerConfig(packageclaude-agent-sdk) — VERIFIED 2026-07-04 against the official Claude Agent SDK Python docs (code.claude.com/docs/en/agent-sdk/python + docs.claude.com agent-sdk/mcp; the name is confirmed, not assumed — pin it). Note MCP is documented as an extension of this family, NOT a new client wired into the run path (Non-Goal). Frame everything as "local mock / no live source"; do not claim a live integration. - Reuses: the existing "Bevisst ikke bygget (90 %-kuttlista)" seam-naming style in
docs/extending.md:55-74; the honesty framing indocs/extending.md:7-13. - Test first: (docs — no unit test; command-checkable at Verify)
- Verifies:
grep -q create_sdk_mcp_server docs/extending.mdexits 0, and a new http source-family section is present; the new I6 artifacts carry no affirmative live-source claim.
- Verifies:
- Verify:
grep -q create_sdk_mcp_server docs/extending.md && echo OK→ expected:OK;grep -rin "live" docs/extending.md examples/ingest-golden-http/ src/portfolio_optimiser/ingest.py→ expected: any hit is only "local mock" / "no live source" negated framing (scoped to the I6 artifacts — the repo-wide grep is not a clean gate:docs/,src/persona.py,simulation.pyetc. carry dozens of unrelated pre-existing "live"/"delivery" substrings). - On failure: revert —
git checkout -- docs/extending.md - Checkpoint:
git commit -m "docs(i6): http source extension-point + D7 create_sdk_mcp_server pointer" - Manifest:
manifest: expected_paths: - docs/extending.md min_file_count: 1 commit_message_pattern: "^docs\\(i6\\): http source extension-point \\+ D7 create_sdk_mcp_server pointer$" bash_syntax_check: [] forbidden_paths: - src/portfolio_optimiser/okf.py - shared/ingest-spec.md must_contain: - path: docs/extending.md pattern: "create_sdk_mcp_server" - path: docs/extending.md pattern: "allow_network"
Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
Thread allow_network through run_project/run_portfolio (as the brief assumed) |
Matches the brief's literal assumption | run.py never imports ingest/calls materialize (verified by 2 agents + grep) — the param would be dead code |
Scope creep + speculative feature; violates surgical-change. Flag scoped to materialize only. |
Reuse render_table for the http body |
Maximal "mirror I4" symmetry | §5 requires the http body verbatim in a fenced code block, not a table; render_table escapes |/\\ |
Spec-nonconforming; the fenced-not-table load-bearing test would fail. New _render_fenced_block. |
localhost http.server thread for tests |
"Real" HTTP round-trip | Opens a socket → violates §11 "without network access"; flaky/slow; hermeticity risk | Injectable get callable (canned payloads, no socket) is the hermetic §11 reading. Documented fallback only. |
| Variable-length fence (count backticks in body) | Silently "handles" a body containing ``` | Extra determinism surface; papers over an ambiguous body the model would later read | Rejected in favour of fail-fast: a body line beginning ``` → IngestError (no silent corruption; consistent with the module's no-silent-coercion frame). Fixed 3-backtick fence is deliberate, not a fallback. |
urllib.parse.urljoin for the URL |
Stdlib canonical | Surprising path semantics (absolute paths, ..) → non-deterministic joins |
Explicit rstrip("/") + "/" + lstrip("/") — predictable, pinned. |
Test Strategy
- Framework:
pytest(uv run pytest); byte-for-byte golden harness; detach-RED load-bearing regime (spec §11). - Existing patterns: three-file-per-source-type convention
(
test_ingest_http.pyconnector-unit,_loadbearing.pydetach-RED,_golden_http.pybyte-for-byte) — mirrors the sql trio. Canned-getbuilders local to the test files (neverconftest.py, which is MAF/LLM-only). - New tests in this plan: ~11 across 3 new files + 1 comment correction.
Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | tests/test_ingest_http.py |
verbatim fetch; URL join; max_rows; credential env-resolve (set/unset); urllib default |
tests/test_ingest_sql.py |
| Load-bearing | tests/test_ingest_http_loadbearing.py |
network gate (never-called + IngestError); fenced-not-table; navigability; over-cap-writes-nothing | tests/test_ingest_sql_loadbearing.py |
| Golden | tests/test_ingest_golden_http.py |
byte-identical materialization; §10 idempotence; no creds | tests/test_ingest_golden_sql.py |
| Comment fix | tests/test_ingest_materialize.py:245 |
default-no-flag still refuses (intent reworded) | — |
Offline property (SC): satisfied by construction — every http test injects http_get (no
socket) and needs no credential. Acceptance: uv run pytest exits 0 with the network cable
pulled. Convention to enforce in review: no ingest test calls materialize/read_http for an
http source without passing http_get (the default _urllib_get is the only socket path and
is never exercised).
Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| Critical | Silent egress if the gate sits after connector construction, or _urllib_get is reachable without the flag |
ingest.py:393 gate / dispatch |
Network hit with no operator opt-in — defeats the task | Gate in materialize BEFORE the staging loop; _urllib_get only reached inside the http branch, which the gate guards. Load-bearing test asserts get never called when flag absent. |
| High | Credential echo in the verbatim body | http body render | A reflecting endpoint could echo Authorization into a committed bundle file |
Inherent to "verbatim body" — do NOT auto-redact (breaks determinism). Golden mock never echoes a secret; document as a residual deployer-owned risk (§1 boundary). |
| High | Credential leak via error/log text | read_http IngestError, _LOGGER.info |
Secret in an exception or log line | Mirror read_sql: wrap transport errors WITHOUT the secret; keep §8 log to id/ingested_at/row_count only. (Optional sentinel-secret grep test — nice-to-have, not gating.) |
| High | Determinism: verbatim-vs-LF-only tension, trailing newline, fence collision | _render_fenced_block / read_http |
Golden passes locally, diverges on other input/platform; or silently-broken markdown in the bundle | Pin (docstring + golden): UTF-8 decode fail-fast; CRLF→LF; strip trailing newlines outside the fence; max_rows = len(splitlines()); a body line beginning ``` → IngestError fail-fast (never a silently-corrupted fenced block — consistent with the no-silent-coercion frame). |
| Medium | max_rows line-count semantics (extended-Unicode / trailing / blank lines) |
read_http |
Spurious pass/fail vs golden | Count \n-only on the CRLF-normalized text (NOT str.splitlines(), which also splits on \v \f \x1c-\x1e \x85 \u2028 \u2029 — verified): n = normalized.count("\n"); line_count = n if normalized.endswith("\n") else n+1. Matches the LF-only body; a trailing newline adds no phantom line; internal blank lines count. Test exactly max_rows (pass) and max_rows+1 (raise). |
| Medium | base_url+query join edge cases |
read_http |
Non-deterministic joins; scheme/host escape | Pinned rstrip("/")+"/"+lstrip("/"); base_url userinfo already rejected (ingest.py:92-100); the joined URL is never stamped in frontmatter. |
| Low | Stale test_http_source_has_no_connector intent |
tests/test_ingest_materialize.py:245 |
Comment says "no connector" (now false) | Reword in Step 2; keep the default-refuse assertion. |
| Low | Temptation to touch okf.py/shared/ |
okf.py, shared/ |
Breaks D7-portability invariant / commons pull-only | forbidden_paths on every code step; SC git diff --stat shows no change. |
| Low | ruff S310 (urlopen) / mypy Callable typing |
ingest.py |
Lint/type red | # noqa: S310 on the gated urlopen; Callable from collections.abc; confirm at Verify. |
Assumptions
| # | Assumption | Why unverifiable at plan time | Impact if wrong |
|---|---|---|---|
| 1 | The default auth scheme for a resolved credential_ref is Authorization: Bearer {secret} |
The spec names credential-by-ref but not the wire scheme; endpoint-specific | A real deployer's endpoint may need a different scheme — acceptable for a demo extension point; documented, and the get seam is swappable. Golden uses credential_ref: null so this is not frozen in golden bytes. |
(RESOLVED, not open — the brief's [ASSUMPTION] flag threads through materialize and run_project was verified FALSE: run.py never calls materialize. The flag is scoped to
materialize only. source_system = manifest.source.id confirmed from
_render_concept_file:312; max_rows = response line count pinned per §8. The D7 pointer
create_sdk_mcp_server was verified 2026-07-04 against the official Claude Agent SDK Python docs
— it is the confirmed in-process MCP-server primitive, not an assumption.)
Verification
Per-step manifests are checked by trekexecute. These are the brief's Success Criteria as end-to-end checks (run from repo root):
uv run pytest -q→ expected: exits 0 (full suite green, no network, no credentials set).- Network gate detach-proof: comment out
and not allow_networkiningest.py, runuv run pytest tests/test_ingest_http_loadbearing.py -k gate→ expected: RED; restore → green. uv run pytest tests/test_ingest_golden_http.py -q→ expected: pass; output byte-matchesexamples/ingest-golden-http/expected-bundle/.uv run pytest tests/test_ingest_http.py -k max_rows→ expected: pass (over-cap → IngestError).grep -rin "live" docs/extending.md examples/ingest-golden-http/ src/portfolio_optimiser/ingest.py→ expected: any hit is only "local mock" / "no live source" negated framing (scoped to the NEW I6 artifacts — a repo-widegrep "live"is not a pass/fail gate: dozens of unrelated pre-existing hits indocs/,persona.py,simulation.py,test_*_live.py).git diff --stat(vs pre-I6) → expected: NO change tosrc/portfolio_optimiser/okf.pyorshared/.uv run ruff check .→ 0;uv run ruff format --check .→ 0;uv run mypy src→ 0.grep -q create_sdk_mcp_server docs/extending.md→ exits 0; new http section present.- Null API-spend: no model calls, no real network (golden/gate/unit all use injected
get).
Estimated Scope
- Files to modify: 3 (
ingest.py,tests/test_ingest_materialize.py,docs/extending.md). - Files to create: 11 (3 test files + 8 golden/fixture/builder:
manifest.json,ingested-at.txt,fixture/status,fixture/report, 3 ×expected-bundle/*.md,build_fixture.py). - Complexity: medium (one genuinely new render path + a security gate; the rest mirrors I4).
Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 95 | 4 steps, strict dependency order (connector→gate→golden→docs) |
| Step quality | 0.20 | 92 | test-first, exact file:line, explicit reuse; Step 2 touches 3 files (cohesive) |
| Coverage completeness | 0.20 | 95 | every Success Criterion → a step + a Verification check |
| Specification quality | 0.15 | 90 | pinned decisions concrete; 2 honest doc-time assumptions remain |
| Risk & pre-mortem | 0.15 | 92 | silent-egress, credential-leak, determinism all mitigated |
| Headless readiness | 0.10 | 90 | On-failure + Checkpoint + Manifest per step |
| Manifest quality | 0.05 | 88 | must_contain + forbidden_paths guard okf.py/shared/ |
| Weighted total | 1.00 | 92 | Grade: A |
Adversarial review:
- Plan critic: REVISE → addressed. 1 blocker (gate false-green), 5 major (live-grep unsatisfiable, first-live-source circular, create_sdk_mcp_server contradiction, fence-collision silent, golden circular/hand-review), 6 minor — all resolved in Revisions below.
- Scope guardian: ALIGNED. 0 creep, 0 Non-Goal violations, 0 dependency issues; independently
confirmed the
materialize-only flag scoping is a correct scope fix (not a gap). Its one minor (over-broad live-grep) coincides with plan-critic MAJOR-1 and is fixed.
Revisions
Added by adversarial review (Phase 9). All blocker + major findings addressed; the plan was revised once for the merged set.
| # | Finding | Severity | Resolution |
|---|---|---|---|
| 1 | Gate load-bearing test would false-green: reverting and not allow_network to the unconditional refusal still passes a refuse-only test |
blocker | Step 2 gate test now asserts BOTH branches on the same manifest + same recording get (refuse: flag off → IngestError + get never called; allow: flag on → succeeds + get called). Removing the whole gate → refuse branch RED; removing just the clause → allow branch RED. Verify wording fixed. |
| 2 | grep -rin "live" docs/ src/ examples/ unsatisfiable (dozens of pre-existing hits) |
major | Scoped the "no live claim" check to the NEW I6 artifacts (docs/extending.md, examples/ingest-golden-http/, ingest.py) in both Step 4 and Verification. |
| 3 | git grep "first live source" circular (matches only plan/brief) |
major | Removed the circular sub-check. |
| 4 | create_sdk_mcp_server pinned in 3 gates while flagged "may be stale" |
major | Verified 2026-07-04 against official Claude Agent SDK Python docs — confirmed correct; pinned confidently, Assumption downgraded to a verified fact. |
| 5 | Fence-collision (body containing a ``` line) silently corrupts the bundle |
major | Changed to fail-fast: a body line beginning ``` → IngestError (Step 1 connector + docstring + risk + alternatives + a new unit test). |
| 6 | Golden expected-bundle circular + "hand-review" is a non-headless human step | major | Step 3 now carries the hand-derived ingest-status.md expectation IN the plan (non-circular, headless-usable), names the Step-2 load-bearing tests as the independent correctness oracle, and adds an escalate-on-divergence clause. |
| 7 | max_rows counts lines pre-strip; blank-line nuance |
minor | Pinned: count on CRLF-normalized text before the trailing-strip; blank lines count as content lines (deterministic). Risk row + Step 1 updated. |
| 8 | c:\temp\cache backslash/tab trap in build_fixture.py |
minor | Pinned raw-bytes / raw-string authoring so the verbatim backslash survives. |
| 9 | # noqa: S310 likely dead (no bandit config) |
minor | Do not pre-add; add only if ruff check flags the urlopen at Verify. |
| 10 | File-count off by one (10 vs 11 created) | minor | Corrected to 11 created / 3 modified. |
| 11 | Line-ref drift (:393 vs :394) |
minor | Standardized the gate reference to :394-398. |
| 12 | Pass-at-max_rows boundary test only in risk table |
minor | Added the boundary-pass + max_rows+1-raise pair to the Step 1 test enumeration. |
Adversarial Pass 2 (gemini-bridge, v5.1.1 high-effort)
The high-effort plan phase runs an additional independent gemini-bridge review. Outcome:
gemini-bridge is non-functional (verified 2026-07-04, not assumed). The deep-research query ran
to status: completed, but every retrieval path failed: gemini_get_research_result →
'Interaction' object has no attribute 'outputs' (server-side bug, 3× retry); the followup
re-emit → 404 model gemini-3-pro-preview no longer available (deprecated). This confirms the
prior recorded outage — the operator's expectation that it was fixed did not hold. No findings
were retrievable; the agent correctly refused to fabricate a second opinion.
Substitute triangulation (per the standing gemini-outage handling):
- An independent high-effort plan-critic confirmation pass traced the blocker fix against both
detach cases and verified revisions #2–#6 — plan sound, no new blocker/major; folded in one
minor (CommonMark ≤3-space indented closing fence →
line.lstrip(" ").startswith("```")). - The two decisions gemini flagged as spec-checkable were resolved by primary-source verification
(no gemini needed): CommonMark §4.5 fenced-code semantics (closing fence ≥ opening length,
≤3-space indent) → the fence-collision check is adequate; CPython
str.splitlines()splits on the extended Unicode set → themax_rowscap was switched to a\n-only count so it matches the LF-only rendered body (Step 1 + risk row updated). - Decision 4 ("config cannot grant its own capability; capability is an explicit runtime grant")
is the capability-security / POLA (Principle of Least Authority) pattern — the manifest is
data that cannot escalate its own authority; only the
allow_networkruntime grant can.
Net: no external second opinion available, but every decision gemini would have triangulated is covered by an independent internal pass or an authoritative primary-source check.