Skip to content

ID-62 readiness assessment (S312) — fixture-staging live-verification infra

ID-62 readiness assessment (S312) — fixture-staging live-verification infra

Section titled “ID-62 readiness assessment (S312) — fixture-staging live-verification infra”

Provenance: S312 (2026-06-04). READ-ONLY analysis to tee up the next Executor session. No code, ledger, or spec edits were made authoring this. Scope: ID-62 “Fixture-staging live-verification infra” (in_progress, dep [66] now satisfied — ID-66 fully DONE per v1-completion-sequence.md §0/S309). Determines whether {62.6}, {62.7}, {62.9}, {62.10} can be implemented NOW, and which subtasks need re-review given recent platform changes — especially ID-83 / bl-221 boot-never-walks and the cocoindex-write-model.md reference doc. Grounded against (this session, direct read): scripts/cocoindex_pipeline/server.py (full), scripts/cocoindex_pipeline/flow.py (app_main 2140–2370, walk_dir 2307–2311, lifespan), scripts/cocoindex_pipeline/workspace_resolver.py (manifest validator), the ID-62 spec chain (TECH {62.3} / PLAN {62.4}), the ID-62 subtask details, docs/specs/ID-75-pullmd-cocoindex/RESEARCH.md §6, cocoindex-write-model.md, v1-completion-sequence.md, docs/specs/bl-221-cocoindex-walk-trigger/TECH.md.


SubtaskVerdictOne-line blocker / what-must-happen-first
{62.6} corpus activation + boot seedNEEDS-RE-REVIEW (then mostly READY-NOW, scope shrinks)The spec’s framing — “arm the walk_dir(live=True) watch at boot; seed manifest in server.main() BEFORE start_cocoindex_thread()” — is superseded by ID-83/bl-221 boot-never-walks. Boot never reaches app_main, so there is no boot watch to arm. Re-scope to: (a) COCOINDEX_SOURCE_PATH=/corpus (already SET, S312), (b) seed a schema-valid .kh-workspace-map.json at the corpus root so a /walk produces rows (not a false green), (c) the seed home is no longer load-bearing-before-thread-spawn.
{62.7} verify driver (verify_driver.py)READY-NOWHost-agnostic stage-loop is buildable + unit-testable now (/stage route is DONE). Live on-host verification waits on operator; no code blocker. One small spec carry: the stage-only driver should optionally also POST /walk (or document that the operator/scheduled task does) since staging bytes no longer auto-triggers a walk (ID-83).
{62.9} B1 trigger + host Vitest go-live + Inv-29 docREADY-NOW (build) / BLOCKED-on-operator (live proof)The trigger artefact + env block + Inv-29 doc item are authorable now, but the trigger MUST now include the POST /walk step (stage → walk → assert), which the spec predates. Live Tier-1/2/3 proof needs a supervised on-host run (operator-gated, the same burn-supervision discipline).
{62.10} wire {42.10} HTML/Inv-7-8-9BLOCKED-BY ID-75/42.10Confirmed double-gated. ID-75 RESEARCH §6 + v1-completion-sequence.md L83 both state {62.10} must stage a URL item (seed feed_articles), NOT a local HTML fixture — PullMD cannot read a staged local file. {62.10} cannot be implemented until ID-75 lands the remote-URL source (Option A) and the {42.10} fixture is re-shaped local-HTML→remote-URL.

Single biggest re-review risk: {62.6} (and the trigger half of {62.9}) were specced against the OLD continuous-watcher model (walk_dir(live=True) armed once at boot). ID-83/ bl-221 replaced that with lifespan-only boot + on-demand POST /walk (update_blocking(live=False)). The whole “seed before the thread spawns so the watch arms” load-bearing sequencing in {62.6} details / PLAN §3.1–3.2 is now moot for the boot path — and, more importantly, staging a fixture via /stage no longer causes ingestion on its own. Every ID-62 live-verify path ({62.7}/{62.9}/{62.10}) now needs an explicit POST /walk between “stage” and “assert”. This is not in any ID-62 spec artefact (they predate ID-83). It MUST be threaded in before the live go-live, or every fixture produces zero rows and the Vitest poll times out.


1. The boot-never-walks impact on {62.6} (the load-bearing change)

Section titled “1. The boot-never-walks impact on {62.6} (the load-bearing change)”

{62.3} TECH §Slice B + {62.4} PLAN §3.1/§3.2 + the {62.6} details all rest on this model (quoting the live details):

app_main reads COCOINDEX_SOURCE_PATH (1536) then loads the manifest (1582) … The watch arms ONCE at app_main; today "" = idle return. So a schema-valid manifest MUST be present at the corpus root at process start, not merely before the first fixture … server.main() (234–255) — BEFORE start_cocoindex_thread() (252): os.makedirs(source, …) then seed a minimal manifest.”

That is the pre-ID-83 boot model: start_cocoindex_thread() ran KH_PIPELINE_APP.update_blocking(live=True), which ran app_mainlocalfs.walk_dir(…, live=True) at boot, arming a continuous fs-watch. Under that model, a missing/invalid manifest at boot aborted the whole flow, so seeding before the thread spawned was genuinely load-bearing.

1.2 What ID-83 / bl-221 actually did (current code, grounded)

Section titled “1.2 What ID-83 / bl-221 actually did (current code, grounded)”

server.py has already been rewritten by ID-83 (the boot path INVERTED):

  • Boot is lifespan-only. start_cocoindex_thread() now calls coco.start_blocking() (server.py:545), which enters the App’s @coco.lifespan (provisions the asyncpg pool + LMDB engine) and returns — it runs NO registered App’s main_fn, so app_main (and therefore walk_dir) NEVER runs at boot (server.py:9–16, 487–498). A container boot/restart can never auto-walk or burn Anthropic, regardless of whether COCOINDEX_SOURCE_PATH is set (bl-221 G1).
  • The walk fires ONLY on POST /walk. The bearer-gated _walk_handler (server.py:354–454, single-flight lock) spawns a worker running KH_PIPELINE_APP.update_blocking(live=False) (server.py:342) — one-shot, incremental, returns to idle. live=False, not live=True.
  • Consequently app_main’s localfs.walk_dir(source_path, live=True, recursive=True) (flow.py:2307–2311) still has live=True in source, but it is reached only via a /walk update_blocking(live=False) pass — the engine runs one scan pass and returns; the continuous fs-watch the live=True flag implies is effectively dormant (no boot arming, no long-lived watcher loop). The /stage docstring at server.py:201–203 (“the co-located walk_dir(live=True) watcher polls”) is now stale — there is no polling watcher; a /walk must be issued.

1.3 What {62.6} should become (re-scope, do not delete)

Section titled “1.3 What {62.6} should become (re-scope, do not delete)”

{62.6} is NEEDS-RE-REVIEW, not blocked — most of the substance survives but the framing shrinks:

  1. COCOINDEX_SOURCE_PATH=/corpus Compose envalready done (S312): the brief says set it on the B1 Compose env; the prompt confirms it is now SET to /cocoindex-state/corpus on both Coolify envs. So this half of {62.6} is satisfied. (Note the path is /cocoindex-state/corpus, not the spec’s literal /corpus{62.6}’s manifest-shape test asserting == "/corpus" must be updated to the real value or made path-agnostic.)
  2. Seed .kh-workspace-map.json at the corpus rootstill required, but the reason and placement change. It is no longer “before the thread spawns so the boot watch arms” (boot never walks). It is now “the manifest must exist at the corpus root before the first /walk, or app_main raises ManifestLoadError and every walk produces zero rows” (flow.py:2206–2230, raise at 2230). The seed can live in server.main() (still fine) OR be a one-time corpus-prep step — the pre-start_cocoindex_thread() ordering constraint is moot. The minimal schema-valid shape is confirmed: {"schema_version": 1, "mappings": []} (workspace_resolver.py:107–127mappings may be an empty list, “degenerate but legal”).
  3. Activation = a /walk, not a container restart. The brief says “Activation = container (re)start (the watch arms once at app_main)”. Under ID-83 a restart does NOTHING (boot never walks). Activation is now POST /walk. This sentence in the details is wrong and must be corrected.
  4. The burn-tension with cocoindex-write-model.md §6 is RESOLVED by ID-83 — see §3.

Verdict {62.6}: NEEDS-RE-REVIEW (correct the boot-watch framing + the activation model + the /corpus path literal), after which the residual work (manifest seed + manifest-shape test) is READY-NOW and host-agnostic (unit-testable via pytest).


2. The {62.10} ↔ ID-75/42.10 dependency chain (confirmed BLOCKED)

Section titled “2. The {62.10} ↔ ID-75/42.10 dependency chain (confirmed BLOCKED)”

{62.10} (“Wire the {42.10} HTML/Inv-7-8-9 invocation through the same verify driver”) is BLOCKED-BY ID-75/42.10. The chain is confirmed by two independent sources:

  • v1-completion-sequence.md L83 (LANE P): {62.10} is annotated “wire {42.10} (double-gated on ID-75)”. L84/L28/L158 reiterate “PullMD/ID-75 does NOT gate the re-ingest cutover — only {62.10} + new-URL/RSS ingest need it”.
  • ID-75/RESEARCH.md §6 (the doc the prompt flagged) is explicit and load-bearing:
    • §2.6 + §6: _stage_handler stages a file — “exactly the local-HTML-fixture shape that {42.10} depends on and that PullMD cannot read.” PullMD’s contract is GET {PULLMD_SERVICE_URL}/api?url=<http(s) URL>; it has no filesystem access and no file:// support, so handing it a staged /cocoindex-state/corpus/test/x.html “can never work.”
    • §6 ID-42.10: the current {42.10} (stage one local HTML source, assert pullmd_* extraction) is “unsatisfiable as written.” ID-75 “changes what 42.10 proves — from ‘local HTML file → PullMD’ to ‘remote URL → PullMD’.” So {42.10} must be re-shaped (owned by ID-75, coordinated with ID-42’s close-out).
    • §6 ID-62.10: “under Option A the /stage driver must stage a URL item (seed a feed_articles/feed_sources row the remote source enumerates, OR yield a URL through the source), not a local HTML fixture.”

Why it is hard-blocked, not just “re-review”: {62.10}’s entire job is to parameterise the verify driver with the (HTML fixture, Inv-7/8/9) set and run the Inv-9 GET http://localhost:3000/s/<share_id> round-trip. That round-trip only returns a real share-id if PullMD actually fetched a real remote URL. Until ID-75 lands the remote-URL source (Option A — verified VIABLE against cocoindex 1.0.3 in ID-75 RESEARCH §4) and the {42.10} fixture is re-shaped to a feed_articles-seeded URL, there is nothing for {62.10} to wire that can pass.

Dependency edge to record (next ledger pass): the ID-62 PLAN {62.4} §7 DAG currently has {62.10} depending ONLY on its sibling {62.9} (sibling-only constraint), with ID-42 unblocked at the Task level. ID-75 RESEARCH now adds a new cross-Task reality: {62.10} (and ID-42’s {42.10}) depend on ID-75’s remote-URL wiring. The cleanest encoding is a Task-level ID-62 → ID-75 (and ID-42 → ID-75) dependency, mirroring the existing ID-62 → ID-66 / ID-42 → ID-62 Task edges — NOT a Subtask cross-dep (preserves the §3.3 sibling-only rule). The Orchestrator records the edge; this assessment flags it.

Chain (read top-down):

ID-75 {75.x} (remote-URL source, Option A; feed_articles enumeration)
└─► re-shapes ID-42 {42.10} fixture (local-HTML → remote-URL)
└─► unblocks ID-62 {62.10} (/stage stages a URL item, not a file; Inv-9 round-trip real)

3. Subtasks needing re-review vs cocoindex-write-model.md

Section titled “3. Subtasks needing re-review vs cocoindex-write-model.md”

cocoindex-write-model.md (S297) is the canonical cocoindex-1.0.3 write-model + live-ops/burn runbook. Reviewed in full against the four pending ID-62 subtasks. Findings:

3.1 The SOURCE_PATH burn-tension — RESOLVED by ID-83 (the one material interaction)

Section titled “3.1 The SOURCE_PATH burn-tension — RESOLVED by ID-83 (the one material interaction)”

cocoindex-write-model.md §6 “Operational notes” (lines 183–185) states:

Burn footgun: the deploy/restart restarts the app on the pinned COCOINDEX_IMAGE_TAG. Keep COCOINDEX_SOURCE_PATH empty by default … so any auto-restart boots idle (no corpus walk, no Anthropic burn); set it only during a supervised smoke and empty it immediately after.”

This directly contradicts {62.6}’s “set COCOINDEX_SOURCE_PATH=/corpus in the Compose env” (a persistent setting). Under the S297 model, a persistent SOURCE_PATH was a live burn hazard (any Coolify auto-restart → boot walk → Anthropic spend). ID-83/bl-221 resolves this tension: boot is now lifespan-only and never walks “regardless of whether COCOINDEX_SOURCE_PATH is set” (server.py:16, 497). So a persistently-set SOURCE_PATH is now burn-safe — exactly why S312 was able to set it on both Coolify envs. cocoindex-write-model.md §6 lines 183–192 are now STALE (they describe the retired SOURCE_PATH-blanking burn-valve). This is a doc-refresh item, not an ID-62 blocker, but it is the reason {62.6}’s “set it persistently” is now correct where S297 said the opposite — the Executor must not “fix” {62.6} back to the empty-default discipline.

3.2 R1 (no cross-target FK) / R3 (UNIQUE dedup) — informs the {62.9}/{62.10} assertions, no change

Section titled “3.2 R1 (no cross-target FK) / R3 (UNIQUE dedup) — informs the {62.9}/{62.10} assertions, no change”

The write-model R1–R5 rules govern what rows land and how. They do not change the ID-62 staging infra, but they shape what the Vitest assertions in {62.9}/{62.10} can rely on:

  • R1: related rows (content_itemssource_documentsentity_mentions) are written on separate autocommit connections, possibly concurrently, NOT parent-before-child. So a {62.9} assertion polling for a content_items row must not assume the source_documents parent is already present (or vice-versa) — pollContentItemsFor already polls with a timeout, which tolerates this, but any cross-table assertion (e.g. {62.10} reading pullmd_share_id from source_documents after seeing the content_items row) must poll the second table independently, not assume co-arrival. Carry to {62.10}’s driver HTML-mode probe.
  • R3 + dropFixture: entity_mentions has a natural-key UNIQUE + best-effort cleanup ({62.8} details already flags the ID-49.5-deferred best-effort cleanup; the write-model R1 trade-off — no ON DELETE CASCADE — means dropFixture must delete children explicitly). This is already noted in {62.8}; no new action, but the first live run ({62.9}) should confirm cleanup does not leak rows across runs (R1 §70 “wipe must delete children explicitly”).

3.3 The seven-bug “smoke is the oracle” discipline — reinforces, does not block

Section titled “3.3 The seven-bug “smoke is the oracle” discipline — reinforces, does not block”

cocoindex-write-model.md §3 (“the mocked unit suite is structurally blind”) is the raison d’être of ID-62: a live verify tier that the mocked suite cannot replace. Nothing to re-review — it validates the whole task. The one operational reminder it adds: {62.9}/{62.10} live runs write to prod today (the on-prem app is wired to prod; §6 line 161, OQ-64-8) — so the live verify run uses test-prefixed fixtures wiped via dropFixture, and the operator runs it under the same supervised-burn discipline as the S297 smokes.

3.4 Subtasks with NO write-model re-review needed

Section titled “3.4 Subtasks with NO write-model re-review needed”
  • {62.7} verify driver — stage-only, no Supabase/SQL by design; the write-model is irrelevant to the staging half. The only carry is the ID-83 /walk step (§1.2), not a write-model concern.

4. Per-subtask detail: what an Executor does next

Section titled “4. Per-subtask detail: what an Executor does next”

{62.6} — NEEDS-RE-REVIEW → then READY-NOW (host-agnostic residual)

Section titled “{62.6} — NEEDS-RE-REVIEW → then READY-NOW (host-agnostic residual)”
  • Re-frame the brief (the load-bearing edit): remove “arm the boot watch” / “seed before start_cocoindex_thread()” / “activation = container restart” — all moot under ID-83. Replace with “manifest must exist at the corpus root before the first POST /walk; activation is a /walk.”
  • Already done: COCOINDEX_SOURCE_PATH is SET (S312) — fix the literal in the manifest-shape test from /corpus to /cocoindex-state/corpus (or assert path-agnostically).
  • Residual code: seed {"schema_version": 1, "mappings": []} at ${COCOINDEX_SOURCE_PATH}/.kh-workspace-map.json if absent (placement in server.main() is still acceptable; pre-thread-spawn ordering no longer required). Unit-test that load_workspace_manifest parses the seed without raising (pytest, no live host).
  • Tool-discipline: main/app_main are indexed Python symbols — gitnexus_impact({target: "main", direction: "upstream"}) before edit, gitnexus_detect_changes() before commit.
  • Build scripts/cocoindex_pipeline/verify_driver.py exactly per details: read (fixturePath, destPath, titlePrefix) tuples, POST multipart to http://localhost:<port>/stage, exit 0 iff all 2xx; stage-only (no Supabase/SQL import); idempotent re-stage.
  • One spec carry (ID-83): staging bytes no longer triggers ingestion. The driver should either (a) issue a POST /walk (bearer-gated) after staging, or (b) explicitly document that the operator/{62.9} trigger issues /walk between stage and assert. The cleanest is for the driver to remain stage-only (per Inv-8) and for {62.9}’s trigger to own the /walk step — but this must be a conscious decision, not silently dropped.
  • Unit-testable now with a mocked HTTP client (exit-code semantics; assert no Supabase symbol imported). gitnexus_detect_changes() before commit (new module).

{62.9} — READY-NOW (build) / operator-gated (live proof)

Section titled “{62.9} — READY-NOW (build) / operator-gated (live proof)”
  • Author the B1 trigger artefact (host runbook step / Coolify scheduled-task stub / thin SSH workflow) with the commented # FLIP ON AT LAUNCH (ID-62 Inv-29) marker; add the Inv-29 action item to canonical-pipeline-sequencing.md T13 (line 241).
  • The trigger sequence MUST now be: driver stages → POST /walk (bearer-gated, single-flight) → wait for ingest → run host Vitest tier. The spec’s “run the driver then the Vitest go-live step” predates ID-83 and omits the /walk. Without it, every fixture produces zero rows (the manifest-abort/idle-poll-timeout failure mode, but here caused by “no walk was triggered”).
  • Env block per details: COCOINDEX_STAGING_URL / COCOINDEX_FIXTURE_STAGING_URL (= http://localhost:<port>), COCOINDEX_SOURCE_PATH (= /cocoindex-state/corpus), host Supabase creds. Per-file Tier-3 live-vs-defer decision (TECH §Testing tier map). Live proof = a supervised on-host run (operator-gated, prod-write discipline per §3.3).
  • Do NOT start until ID-75 lands the remote-URL source + the {42.10} fixture is re-shaped.
  • When unblocked, the /stage driver stages a URL item (seed feed_articles), not a local HTML file; the Inv-9 GET http://localhost:3000/s/<share_id> round-trip then references a real remote fetch. Apply the R1 cross-table-poll caution (§3.2) to the source_documents pullmd_share_id read.
  • Record the ID-62 → ID-75 (+ ID-42 → ID-75) Task-level dependency edge.

SubtaskSpec statusReal readinessAction before Executor dispatch
{62.6}pending, deps [5]NEEDS-RE-REVIEW → READY-NOW residualRe-frame boot-watch→/walk model; fix /corpus literal; then host-agnostic seed + pytest
{62.7}pending, deps [5]READY-NOWBuild now; decide where /walk lives (driver vs trigger)
{62.9}pending, deps [6,7,8]READY-NOW build / operator liveInsert POST /walk into the stage→assert sequence; author trigger + Inv-29 doc
{62.10}pending, deps [9]BLOCKED-BY ID-75/42.10Wait for ID-75 remote-URL source + {42.10} reshape; record Task edge ID-62→ID-75

Done already (no action): {62.5} /stage route (DONE, server.py:197–291), {62.8} multipart stageFixture (DONE). {62.1}{62.4} spec chain DONE.