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 perv1-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 thecocoindex-write-model.mdreference doc. Grounded against (this session, direct read):scripts/cocoindex_pipeline/server.py(full),scripts/cocoindex_pipeline/flow.py(app_main2140–2370,walk_dir2307–2311, lifespan),scripts/cocoindex_pipeline/workspace_resolver.py(manifest validator), the ID-62 spec chain (TECH{62.3}/ PLAN{62.4}), the ID-62 subtaskdetails,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.
0. TL;DR
Section titled “0. TL;DR”| Subtask | Verdict | One-line blocker / what-must-happen-first |
|---|---|---|
| {62.6} corpus activation + boot seed | NEEDS-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-NOW | Host-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 doc | READY-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-9 | BLOCKED-BY ID-75/42.10 | Confirmed 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)”1.1 What the ID-62 spec assumed
Section titled “1.1 What the ID-62 spec assumed”{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_mainreadsCOCOINDEX_SOURCE_PATH(1536) then loads the manifest (1582) … The watch arms ONCE atapp_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) — BEFOREstart_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_main → localfs.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 callscoco.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’smain_fn, soapp_main(and thereforewalk_dir) NEVER runs at boot (server.py:9–16, 487–498). A container boot/restart can never auto-walk or burn Anthropic, regardless of whetherCOCOINDEX_SOURCE_PATHis 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 runningKH_PIPELINE_APP.update_blocking(live=False)(server.py:342) — one-shot, incremental, returns to idle.live=False, notlive=True. - Consequently
app_main’slocalfs.walk_dir(source_path, live=True, recursive=True)(flow.py:2307–2311) still haslive=Truein source, but it is reached only via a/walkupdate_blocking(live=False)pass — the engine runs one scan pass and returns; the continuous fs-watch thelive=Trueflag implies is effectively dormant (no boot arming, no long-lived watcher loop). The/stagedocstring atserver.py:201–203(“the co-locatedwalk_dir(live=True)watcher polls”) is now stale — there is no polling watcher; a/walkmust 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:
COCOINDEX_SOURCE_PATH=/corpusCompose env — already done (S312): the brief says set it on the B1 Compose env; the prompt confirms it is now SET to/cocoindex-state/corpuson 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.)- Seed
.kh-workspace-map.jsonat the corpus root — still 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, orapp_mainraisesManifestLoadErrorand every walk produces zero rows” (flow.py:2206–2230,raiseat 2230). The seed can live inserver.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–127—mappingsmay be an empty list, “degenerate but legal”). - Activation = a
/walk, not a container restart. The brief says “Activation = container (re)start (the watch arms once atapp_main)”. Under ID-83 a restart does NOTHING (boot never walks). Activation is nowPOST /walk. This sentence in thedetailsis wrong and must be corrected. - 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.mdL83 (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_handlerstages a file — “exactly the local-HTML-fixture shape that{42.10}depends on and that PullMD cannot read.” PullMD’s contract isGET {PULLMD_SERVICE_URL}/api?url=<http(s) URL>; it has no filesystem access and nofile://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, assertpullmd_*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
/stagedriver must stage a URL item (seed afeed_articles/feed_sourcesrow the remote source enumerates, OR yield a URL through the source), not a local HTML fixture.”
- §2.6 + §6:
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. KeepCOCOINDEX_SOURCE_PATHempty 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_items↔source_documents↔entity_mentions) are written on separate autocommit connections, possibly concurrently, NOT parent-before-child. So a{62.9}assertion polling for acontent_itemsrow must not assume thesource_documentsparent is already present (or vice-versa) —pollContentItemsForalready polls with a timeout, which tolerates this, but any cross-table assertion (e.g.{62.10}readingpullmd_share_idfromsource_documentsafter seeing thecontent_itemsrow) must poll the second table independently, not assume co-arrival. Carry to{62.10}’s driver HTML-mode probe. - R3 +
dropFixture:entity_mentionshas a natural-key UNIQUE + best-effort cleanup ({62.8}detailsalready flags the ID-49.5-deferred best-effort cleanup; the write-model R1 trade-off — noON DELETE CASCADE— meansdropFixturemust 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
/walkstep (§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 firstPOST /walk; activation is a/walk.” - Already done:
COCOINDEX_SOURCE_PATHis SET (S312) — fix the literal in the manifest-shape test from/corpusto/cocoindex-state/corpus(or assert path-agnostically). - Residual code: seed
{"schema_version": 1, "mappings": []}at${COCOINDEX_SOURCE_PATH}/.kh-workspace-map.jsonif absent (placement inserver.main()is still acceptable; pre-thread-spawn ordering no longer required). Unit-test thatload_workspace_manifestparses the seed without raising (pytest, no live host). - Tool-discipline:
main/app_mainare indexed Python symbols —gitnexus_impact({target: "main", direction: "upstream"})before edit,gitnexus_detect_changes()before commit.
{62.7} — READY-NOW
Section titled “{62.7} — READY-NOW”- Build
scripts/cocoindex_pipeline/verify_driver.pyexactly perdetails: read(fixturePath, destPath, titlePrefix)tuples, POST multipart tohttp://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/walkbetween 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/walkstep — 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 tocanonical-pipeline-sequencing.mdT13 (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).
{62.10} — BLOCKED-BY ID-75/42.10
Section titled “{62.10} — BLOCKED-BY ID-75/42.10”- Do NOT start until ID-75 lands the remote-URL source + the
{42.10}fixture is re-shaped. - When unblocked, the
/stagedriver stages a URL item (seedfeed_articles), not a local HTML file; the Inv-9GET http://localhost:3000/s/<share_id>round-trip then references a real remote fetch. Apply the R1 cross-table-poll caution (§3.2) to thesource_documentspullmd_share_idread. - Record the
ID-62 → ID-75(+ID-42 → ID-75) Task-level dependency edge.
5. Status summary table (for the ledger)
Section titled “5. Status summary table (for the ledger)”| Subtask | Spec status | Real readiness | Action before Executor dispatch |
|---|---|---|---|
| {62.6} | pending, deps [5] | NEEDS-RE-REVIEW → READY-NOW residual | Re-frame boot-watch→/walk model; fix /corpus literal; then host-agnostic seed + pytest |
| {62.7} | pending, deps [5] | READY-NOW | Build now; decide where /walk lives (driver vs trigger) |
| {62.9} | pending, deps [6,7,8] | READY-NOW build / operator live | Insert POST /walk into the stage→assert sequence; author trigger + Inv-29 doc |
| {62.10} | pending, deps [9] | BLOCKED-BY ID-75/42.10 | Wait 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.