Skip to content

PRODUCT — Fixture-staging live-verification infra — co-located /stage route + localhost verify driver (B1 on-prem) (ID-62.2)

PRODUCT — Fixture-staging live-verification infra — co-located /stage route + localhost verify driver (B1 on-prem) (ID-62.2)

Section titled “PRODUCT — Fixture-staging live-verification infra — co-located /stage route + localhost verify driver (B1 on-prem) (ID-62.2)”

Task: ID-62 — Fixture-staging live-verification infra. Subtask: {62.2} PRODUCT. Predecessor: {62.1} RESEARCH (docs/specs/id-62-fixture-staging-infra/RESEARCH.md), including the appended “OQ-62-9 reachability synthesis (S281)”. Directive set implemented: docs/specs/id-62-fixture-staging-infra/P4-RECONCILIATION.md (the ratified S282 delta — §2 What SURVIVES / §3 What MUST CHANGE / §4 Net shape). Author date: 29/05/2026 (S283).

SUPERSEDES the S279 P2 “in-VPC Cloud Run Job” design. This is a wholesale re-spec, not an amendment. The prior PRODUCT.md (Liam-ratified-as-of-S279) specced an in-VPC Cloud Run Job reaching an ingress: internal cocoindex Service over WIF. The S281 premise inversion (cocoindex→pullmd internal-ingress does NOT work as deployed — RESEARCH §OQ-62-9) and Liam’s S282 ratification of Option B1 (co-locate cocoindex + pullmd on one on-prem host — docs/specs/ID-66-onprem-pivot/RESEARCH.md) dissolved that design’s organising spine. The whole in-VPC Cloud-Run-Job / WIF / ingress-posture / entry-gate block is deleted, not relabelled. Cross-reference: P4-RECONCILIATION.md §1 (WHOLESALE verdict). Git history preserves the old version.

This is the {62.2} artefact authored by a fresh Planner instance (Q-PLANNER-2 — NOT the {62.1} author). {62.3} TECH and {62.4} PLAN are separate fresh-Planner instances. This document specifies behaviour (numbered, testable invariants) only — no wire shapes, no module boundaries, no migration mechanics. Those live in {62.3} TECH.


Make the cocoindex integration tier (the authored-but-never-run-live *.integration.test.ts files under __tests__/integration/cocoindex/) executable against the live cocoindex stack by building two cooperating surfaces on the B1 co-located on-prem host (the ID-66 host where cocoindex + pullmd run as sibling containers and cocoindex reaches pullmd over http://localhost:3000):

  1. A co-resident POST /stage route on the cocoindex aiohttp server that accepts fixture bytes and drops them into the watched corpus directory on the host’s persistent local disk.
  2. A co-located verify driver — a process on the same B1 host — that stages fixtures by reaching /stage over http://localhost:<port> and exits non-zero on any staging failure, after which the existing Vitest assertion surface verifies the produced rows against live Supabase.

The same primitive, invoked with the ID-42 {42.10} HTML fixture and the Inv-7/8/9 assertion set (where the /s/<share_id> round-trip runs against pullmd on localhost), also clears the {42.10} blocker.

The cocoindex integration tier — *.integration.test.ts files under __tests__/integration/cocoindex/ (≈35 files reference the env-gate / staging helpers; the authored assertion count is the “39 tests” figure) — is describe.skipIf(!ENABLED)-gated on an env triplet (COCOINDEX_STAGING_URL + COCOINDEX_FIXTURE_STAGING_URL + COCOINDEX_SOURCE_PATH) plus live Supabase credentials. None of those vars is set in CI, so the tier skips clean and has never produced a single green live assertion (RESEARCH §1).

Two structural blockers sat under the original “wire the env” premise:

  1. No /stage server exists. The client helper stageFixture (__tests__/integration/cocoindex/_helpers/fixture-staging.ts:100–139) POSTs to ${COCOINDEX_FIXTURE_STAGING_URL}/stage (endpoint built at line 111), but scripts/cocoindex_pipeline/server.py:build_app (lines 153–161) registers exactly one route — app.router.add_get("/health", _health_handler) (line 160). No handler writes fixtures into the corpus.

  2. Reachability. flow.py:app_main (line 1524) reads COCOINDEX_SOURCE_PATH ("" today → idle return, lines 1536–1543) and arms localfs.walk_dir(source_path, live=True, recursive=True) (lines 1683–1687) on a local filesystem path the worker watches. The original spec solved CI → that watcher via an in-VPC Cloud Run Job because all staging Services were ingress: internal and a GitHub-hosted runner gets HTTP 403. The S281 premise inversion (RESEARCH §OQ-62-9) established that the in-VPC datapath itself does not work as deployed — no vpc-access/vpc-connector annotation exists in any manifest, so a same-project caller to an ingress: internal Service is NOT classified internal per Google’s rule, and there is no recorded evidence cocoindex→pullmd has ever succeeded live. The in-VPC framing was built on an untested assumption.

B1 dissolves the premise (P4-RECONCILIATION §1). Liam ratified Option B1 in S282: co-locate cocoindex + pullmd on one on-prem host (IONOS/Coolify) where /stage, the cocoindex walk_dir(live=True) watcher, and pullmd all live together. cocoindex reaches pullmd over http://localhost:3000 (ID-66 §1-b); the corpus + the cocoindex LMDB state store live on persistent local disk (ID-66 §1-c). There is no ingress: internal classification to defeat, no VPC, no NAT, no Cloud Run Job, no WIF. The reachability mechanism is localhost on a single host.

Real scope of the re-specced ID-62 = (i) build the co-resident POST /stage byte-drop route (topology-agnostic — same on Cloud Run or B1), (ii) activate the corpus watch on the B1 host’s persistent disk, (iii) build a co-located verify driver that stages fixtures over localhost and surfaces failures, and (iv) run the preserved Vitest assertion surface against live Supabase. Wiring env alone is still vacuous.

Code-intelligence orientation (per the planner block)

Section titled “Code-intelligence orientation (per the planner block)”
  • gitnexus_query({query: 'cocoindex server route health corpus watcher'}) — the index has two indexed knowledge-hub repos (the main checkout, 77 commits behind, and a sibling worktree subo-id-52); this worktree (subo-id-62) is not separately indexed, so the query was grounded by direct file read against this worktree’s working tree (current line numbers cited throughout). The Python server surface is the relevant indexed surface: server.py:build_app (153–161), server.py:_health_handler (132–145), server.py:start_cocoindex_thread (169–226), server.py:main / web.run_app (234–255); flow.py:app_main (1524), flow.py localfs.walk_dir(source_path, live=True, recursive=True) (1683–1687).
  • TS test helpers are OUTSIDE the GitNexus index by construction__tests__/integration/ is not in the indexed TS corpus, and gitnexus_context on stageFixture / pollContentItemsFor returns Symbol not found (confirmed in the prior {62.2} artefact and by the index scope here). These are grounded by direct file read with file:line cites against this worktree: __tests__/integration/cocoindex/_helpers/fixture-staging.tshasFixtureStagingUrl (43–45), StageFixtureArgs (51–71), StageFixtureResult (73–85), stageFixture (100–139, POST body { fixturePath, destPath, titlePrefix } at 116–120, throw-on-unset-URL at 104–108, throw-on-non-2xx at 123–128), pollContentItemsFor (179–222, ilike('title', '${titlePrefix}%') at 199, timeout error at 220–221), dropFixture (376+, empty-contentIds throw at 376–378, empty-titlePrefix guard at 383–385), the form-template polls matching on name ILIKE / storage_path ILIKE (486–504).
  • No symbol-impact analysis is claimed where the symbol is not indexed. This is a Python-server + out-of-index-TS-helper surface; the cocoindex external symbol (localfs.walk_dir) is the only third-party API and is already pinned (see External-API verification).

Goals. A repeatable B1 primitive — parameterised by (fixture set, assertion set) — that stages real fixtures into the live B1 cocoindex corpus over localhost and lets the existing Vitest assertion surface verify produced rows against live Supabase. One primitive serves BOTH the ID-62 integration tier AND the ID-42 {42.10} HTML/Inv-7-8-9 proof.

Non-goals.

  • (a) The harness is NOT a PR-blocking gate today (Inv-28 — :522 “On-demand, not PR-blocking (today)”; was cited as Inv-21 under the pre-P4 numbering, but current #21 at :448 is the {42.10} one-invocation invariant. Repointed by the S515 id-402 sweep).
  • (b) The verify driver does NOT duplicate the Vitest assertions (Inv-9).
  • (c) The B1 stack is NOT publicly exposed; /stage is NOT bound to a public interface (Inv-13) — corpus-injection remains a rejected attack surface, re-expressed for B1.
  • (d) This Task does NOT change the cocoindex extraction pipeline behaviour, the Stage-1→6 invariants themselves, or the entity-resolution stage — it only stages inputs and preserves the existing assertions.
  • (e) B1 host provisioning, the Compose file, the persistent volume, secrets-store migration, push-to-deploy, and datapath monitoring are ID-66 concerns, not ID-62 (see the ID-66 host-readiness precondition below). ID-62 builds the /stage route + verify driver that RUN ON that host; it does not build the host.
  • (f) The cocoindex→pullmd production datapath repair is the B1 co-location itself (ID-66); ID-62 consumes the co-located stack, it does not stand it up.

ID-66 host-readiness precondition (Task-level dependency)

Section titled “ID-66 host-readiness precondition (Task-level dependency)”

ID-62 implementation cannot begin until the B1 host exists: the co-located Docker host + Compose stack co-locating cocoindex + pullmd over localhost (ID-66 §1-a, §1-b) + the persistent local-disk volume hosting the corpus directory and the cocoindex LMDB (ID-66 §1-c). The verify driver runs on that host by construction, and /stage writes into the host’s persistent-disk corpus directory. This is a Task-level dependency (ID-62 depends on ID-66 via Task.dependencies[]), consistent with the sibling-only Subtask-dep constraint — it is NOT a Subtask cross-dependency. (P4-RECONCILIATION §5 OQ-62-P4-4. The Orchestrator owns recording the dependency edge in task-list.json; the Planner flags it.)

Surfaces and their consumers:

  • The /stage route (co-resident byte-drop) — consumer is the verify driver (and any on-host caller). Invariants Inv-1 … Inv-6.
  • The co-located verify driver — consumer is the operator / B1 trigger. Invariants Inv-7 … Inv-12.
  • The B1 network posture — invariants Inv-13 … Inv-14.
  • Corpus activation on persistent local disk — invariants Inv-15 … Inv-17.
  • The assertion surface (preserved Vitest tier) — consumer is the test author / the Checker reading green runs. Invariants Inv-18 … Inv-20.
  • The shared {42.10} invocation — invariants Inv-21 … Inv-23.
  • Carried TECH-level constraints ({62.3} resolves) — invariants Inv-24 … Inv-27.
  • Launch-time scheduled-flip / trigger policy — invariants Inv-28 … Inv-29.

The /stage route (co-resident byte-drop, topology-agnostic)

Section titled “The /stage route (co-resident byte-drop, topology-agnostic)”

P4-RECONCILIATION §2 (SURVIVES): the co-resident /stage route is identical on Cloud Run or a co-located host. cocoindex still runs an aiohttp server with the walk_dir(live=True) watcher in one process; a co-resident /stage route writing to the watched corpus dir is topology-agnostic. (Old Inv-1..6, Inv-8 — carried verbatim in spirit.)

  1. Co-resident with the watcher. The cocoindex container exposes a POST /stage route in the same process as the cocoindex worker daemon thread, so the handler writes to the exact local filesystem directory that localfs.walk_dir(source_path, live=True, recursive=True) is watching. There is NO second process and NO network hop between the /stage writer and the watcher. (Grounds: server.py:build_app (153–161) adds routes to the same aiohttp app the daemon thread runs inside, start_cocoindex_thread 169–226; flow.py:1683–1687 watches an in-container local path.) Testable: the route is registered on build_app()’s app and a posted fixture lands in ${COCOINDEX_SOURCE_PATH} without crossing a process boundary.

  2. Bytes on the wire, not a path the writer cannot resolve. POST /stage accepts the fixture bytes in the request body ({62.3} fixes the exact encoding — multipart vs base64-in-JSON), NOT a fixturePath string the cocoindex process cannot read. The handler reads the posted bytes; it does not read a path on a repo checkout the container does not have. (Grounds: stageFixture today sends { fixturePath, destPath, titlePrefix } as a JSON path, fixture-staging.ts:116–120 — this is the PRODUCT-mandated change, see Inv-19.) Testable: a request carrying raw bytes + a destPath produces a written file; a request carrying only a path string (no bytes) is rejected.

  3. Writes to the corpus-relative destination. On a well-formed request with COCOINDEX_SOURCE_PATH set to an existing directory, the handler writes the bytes to ${COCOINDEX_SOURCE_PATH}/<destPath>, where <destPath> is the caller-supplied path relative to the corpus root (including the file extension). The watcher picks the file up on its next watch cycle and runs it through the extraction pipeline. A <destPath> that attempts to escape the corpus root (e.g. ../, absolute paths) is rejected — the handler never writes outside ${COCOINDEX_SOURCE_PATH}. Testable: a benign destPath lands the file under the corpus root; a path-escape destPath returns a 4xx and writes nothing outside the root.

  4. 2xx with echoed destination on success. The handler responds 2xx on a successful write, echoing back at least the destination path it wrote to (which MAY carry a corpus-root prefix the caller did not supply) so the caller can sanity-check placement. A service-side request id MAY be included for audit; its absence is not an error. (Grounds: stageFixture reads { destPath, requestId? } from the response, fixture-staging.ts StageFixtureResult 73–85 + 130–138 — the response contract is preserved even though the request contract changes.) Testable: a successful POST returns 2xx and a JSON body whose destPath the helper can read.

  5. Loud reject on a mis-wired / missing corpus path. When COCOINDEX_SOURCE_PATH is unset/empty, OR is set but names a directory that does not exist at request time, POST /stage returns a clear, client-diagnosable error naming the missing/absent path (a 4xx when the mis-wire is a deployment-config problem; a 5xx only when it is unambiguously a server-side mount failure — {62.3} chooses the exact codes). It does NOT silently accept-and-drop bytes into a void, and it does NOT pretend a write landed in a watched location when it did not. A mis-wired host is loud at the first /stage call rather than silently producing zero rows downstream. (Mirrors the idle-mode contract: flow.py:1536–1543 treats an unset path as idle and 1545–1553 treats a missing path as idle.) Testable: with COCOINDEX_SOURCE_PATH unset → a 4xx naming the variable; with it set to a non-existent dir → a clear error naming the path; neither returns 2xx.

  6. GET /health survives as an optional B1 liveness hook, and /stage does not break it. Per Liam’s decision, no readiness/liveness probe is required under B1, and the Cloud Run httpGet /health probe is gone with the Cloud Run manifest. The cheap GET /health endpoint (server.py:_health_handler, 132–145) is retained as an optional container-liveness hook a B1 Compose/Coolify healthcheck MAY reuse (ID-66 §1-f). Adding POST /stage MUST NOT alter the /health behaviour: it still returns 200 {"status": "ok"} while the worker thread is alive and 503 once the worker has crashed. This invariant is a /stage-must-not-break-/health regression note, NOT a probe requirement. B1 datapath monitoring (watch pipeline_runs row arrival, ID-66 §1-f) is the separate ID-66 ops concern, out of ID-62 scope. (Decision: P4-RECONCILIATION §3.4 / OQ-62-P4-3 — recorded as OQ-62-P4-3 in OQ-pending.md, recommended default “keep /health” adopted.) Testable: GET /health returns the same status semantics before and after the /stage route is added; staging a fixture does not flip /health to 503.


The co-located verify driver (replaces the in-VPC Cloud Run Job)

Section titled “The co-located verify driver (replaces the in-VPC Cloud Run Job)”

P4-RECONCILIATION §3.1 (RE-ARCHITECT): the verify mechanism is no longer a Cloud Run Job authenticated via WIF reaching an internal-ingress Service. It is a process ON the B1 host that stages fixtures over localhost. NO Cloud Run Job, NO WIF gcloud run jobs execute, NO Secret-Manager --set-secrets. Old Inv-13/14/15/16 SURVIVE in spirit as properties of the host driver; old Inv-10/11 are DELETED; old Inv-12 reframes to the B1 secrets store.

  1. The driver runs on the B1 host and reaches /stage over localhost. The verify driver is a process on the co-located B1 host. It stages fixtures by POSTing their bytes to the cocoindex /stage route at http://localhost:<port>/stage (the recommended default, OQ-62-P4-2 — see Inv-12). Because the driver runs on the same host as the cocoindex container, its request reaches /stage over loopback / the host’s private container network — there is no ingress: internal classification to defeat and no 403. (Grounds: the in-VPC Job + WIF mechanism is deleted; localhost is the reachability — P4-RECONCILIATION §1, §3.1.) Testable: the driver, run on a host where the cocoindex container serves /stage on a localhost port, successfully stages a fixture; the same driver run off-host (no localhost /stage) fails fast.

  2. The driver stages only; it does not assert. The driver’s responsibility is to read each fixture’s bytes and POST them to /stage (Inv-2), then exit. The driver does NOT poll Supabase, does NOT run the invariant SQL probes, and does NOT embed a copy of the Vitest assertions. The staging half and the assertion half are separated by construction. (P4-RECONCILIATION §3.1 — old Inv-13 survives in spirit.) Testable: the driver writes fixtures and exits; no Supabase query originates from the driver process.

  3. No duplicated assertion logic. The authored Vitest tests remain the single assertion surface (Inv-18). The driver MUST NOT re-implement those assertions inside it; doing so would create two divergent sources of truth for “did the pipeline produce the right rows”. (P4-RECONCILIATION §3.1 — old Inv-14 survives.) Testable: the driver source contains no SQL probe equivalent to the Vitest invariant assertions.

  4. Exit-code semantics. The driver exits 0 only when every requested fixture was staged successfully (each /stage POST returned 2xx). It exits non-zero when any /stage POST fails (4xx/5xx, timeout, connection-refused), and the failing fixture + the /stage response status/body are surfaced in the driver output so the operator can diagnose without re-running. A non-zero exit fails the triggering step / surfaces a failed run to the operator. (P4-RECONCILIATION §3.1 — old Inv-15 survives, now a property of the host driver.) Testable: a run where every /stage returns 2xx exits 0; a run where one fixture’s /stage returns 4xx exits non-zero and names the failing fixture in its output.

  5. Idempotent re-stage. Re-running the driver re-stages the same fixtures into the corpus. Re-staging an already-present fixture name overwrites it (the watcher re-processes on change) rather than erroring; the driver does not require a clean corpus to start. Consequence: cleanup (dropFixture) is the test layer’s responsibility (Inv-20), not the driver’s. (P4-RECONCILIATION §3.1 — old Inv-16 survives.) Testable: running the driver twice in succession produces no error on the second run and the corpus reflects the most-recently-staged bytes.

  6. The driver’s default staging path is the /stage HTTP route; direct disk-drop is a permitted convenience, not the default. Because the driver and the watcher share the host filesystem, the driver COULD write fixture bytes directly into the local-disk corpus directory, bypassing /stage. The spec’d default is that the driver uses the /stage HTTP route over localhost (preserving the bytes-on-the-wire contract, the path-escape guard, and the titlePrefix injection centralised in the handler). Direct disk-drop is recorded as an available fallback for the co-located case but is NOT the default. (Decision: P4-RECONCILIATION §3.1, §3.5, §3.7 / OQ-62-P4-2 — recorded as OQ-62-P4-2 in OQ-pending.md, recommended default “keep /stage load-bearing” adopted. {62.3} confirms the mechanics.) Testable: the driver’s default codepath issues an HTTP POST to a localhost /stage endpoint (not a raw filesystem write); the disk-drop path, if implemented, is gated behind an explicit non-default flag/mode.


P4-RECONCILIATION §3.2 (REPLACE): old Inv-23/24/25 were about the Cloud Run ingress model and are MOOT under B1 (no ingress to flip, localhost is the reachability, no self-hosted runner question). The corpus-injection attack-surface concern that motivated old Inv-23 STILL MATTERS and is re-expressed for B1.

  1. /stage is NOT bound to a public interface. On the B1 host, the cocoindex /stage route binds to localhost / the host’s private container-network interface, NOT to a public ingress. /stage writes arbitrary bytes into the ingest corpus that then flow through the full extraction pipeline into the live DB, so exposing it to the public internet — even token-gated — is a rejected corpus-injection surface. The co-located stack is not publicly exposed by ID-62. (Re-expresses old Inv-23 for B1 — the corpus-injection concern is carried; only the network-layer it is expressed in changes from Cloud-Run-ingress to host-interface binding.) Testable: /stage is reachable from on-host localhost / the private container network and is NOT reachable from a public/external network address.

  2. Reachability is localhost on one host — no network-isolation toggle, no self-hosted runner. The driver reaches /stage (and, for {42.10}, pullmd) because both run on the same host, not because any network-layer isolation was relaxed or any new standing CI primitive was introduced. There is no ingress: all flip, no VPC connector, no self-hosted GitHub Actions runner inside a VPC — those were Cloud-Run-era options that do not arise under B1. (P4-RECONCILIATION §3.2 — old Inv-24/25 are moot, replaced by this.) Testable: no ingress-flip manifest change and no self-hosted-runner registration is introduced by ID-62; the only reachability mechanism is on-host localhost.


Corpus activation on persistent local disk

Section titled “Corpus activation on persistent local disk”

P4-RECONCILIATION §3.3 (REFRAME / MOOT): on B1 the corpus is a directory on the host’s PERSISTENT local disk (co-hosted with the cocoindex LMDB). The Filestore-vs-ephemeral tension and the “cold-start corpus loss mid-run” risk DISAPPEAR. The load-bearing carry — .kh-workspace-map.json must exist before the watch arms — SURVIVES unchanged.

  1. The corpus lives on the host’s persistent local disk. The watched corpus directory (COCOINDEX_SOURCE_PATH, e.g. /corpus) is a directory on the B1 host’s persistent local-disk volume — the same persistent disk that hosts the cocoindex LMDB state store (ID-66 §1-c). There is no Filestore (~$200+/mo), no GCS-FUSE, and no ephemeral-tmpfs caveat: the corpus survives container restarts. (Reframe of old Inv-7/Inv-30: the co-resident default is the only shape on B1; the cold-start corpus-loss risk is gone.) Testable: a fixture staged into the corpus is still present after the cocoindex container restarts (persistent-disk volume), and no Filestore/GCS-FUSE/Cloud-Run corpus volume is introduced.

  2. Corpus activation is an explicit, observable precondition. The harness only produces rows when the cocoindex container has COCOINDEX_SOURCE_PATH set to an existing directory and has been (re)started so app_main armed the watch — today the value is "" → idle, watching nothing (flow.py:1536–1543). If the watch is not armed, staged bytes land in a directory nothing is watching and no rows are produced. The harness MUST make this failure mode observable, never a false green: either the /stage loud-reject (Inv-5) fires because the path is unset/missing, or — if the path is set but the watch was never armed — the downstream Vitest poll times out with a diagnosable message (Inv-18), never silently passing. (Carries old Inv-17; the activation step is now a B1 Compose/host env + container (re)start, not a Cloud Run redeploy.) Testable: with the watch unarmed, a staged fixture produces no rows and the Vitest poll times out with the title-prefix it was waiting for (a real failure signal), not a pass.

  3. The workspace manifest must exist before the watch arms. app_main loads ${COCOINDEX_SOURCE_PATH}/.kh-workspace-map.json once at flow start (flow.py:1582); a missing/unparseable/schema-invalid manifest ABORTS the flow with a structured manifest_missing / manifest_invalid stage error (flow.py:1584–1606). So the harness’s corpus directory MUST contain a valid .kh-workspace-map.json at the corpus root before the watch arms, or no fixture will be processed. This is a cocoindex app_main behaviour (not a Cloud Run behaviour) and carries forward unchanged. (P4-RECONCILIATION §3.3 — the load-bearing carry SURVIVES; {62.3} specifies the seed-at-startup mechanics on the B1 host.) Testable: staging a fixture into a corpus directory lacking .kh-workspace-map.json produces a manifest_missing stage error and no content_items rows; staging into a corpus with a valid manifest produces rows.


The assertion surface (preserved Vitest tier)

Section titled “The assertion surface (preserved Vitest tier)”

P4-RECONCILIATION §2 (SURVIVES): the Vitest assertion surface is unchanged — pollContentItemsFor / dropFixture query live Supabase, reachable from anywhere on any topology. The reachability rationale changes (it runs wherever the test author runs it, against ingress: all Supabase), but the assertion half itself is topology-agnostic.

  1. The authored Vitest tests remain the assertion surface, asserting against live Supabase. The authored __tests__/integration/cocoindex/*.integration.test.ts tests remain the assertion surface. The Stage-1→6 invariant assertions, the per-invariant SQL probes, and the form-extraction assertions are unchanged in intent — they now execute live instead of skipping. pollContentItemsFor (fixture-staging.ts:179–222) queries content_items (ilike('title', '${titlePrefix}%'), line 199) via the live service-role client until a row lands or the 120s deadline elapses, then surfaces a diagnosable timeout error (lines 220–221). Because Supabase REST is reachable from anywhere, this assertion half runs unmodified wherever the tier is run and needs NO on-host context. A timeout is a real failure signal (no false green). The form-extraction polls (pollFormTemplatesFor etc., matching name ILIKE / storage_path ILIKE, lines 486–504) behave the same way. This Task does not rewrite what the tests assert; it makes them runnable. Testable: the tier runs live (not skipIf-skipped) when the env is wired; a row that lands satisfies the poll; a row that never lands fails with the awaited title-prefix.

  2. stageFixture posts bytes. The stageFixture helper and its StageFixtureArgs shape (fixture-staging.ts:51–139) change from sending a fixturePath string to reading the fixture bytes and POSTing them in the body (Inv-2). The helper continues to env-gate on hasFixtureStagingUrl() (43–45), continues to throw fast on a missing COCOINDEX_FIXTURE_STAGING_URL (104–108), and continues to throw on a non-2xx response (123–128). The change is internal to the helper’s request construction; the behaviour invariant is “bytes on the wire, not a path the writer can’t see”. (P4-RECONCILIATION §2 — the multipart-bytes wire contract SURVIVES; old Inv-2/20/33. {62.3} decides whether the public StageFixtureArgs keeps fixturePath (helper reads bytes host-side before POSTing) or is restructured — Inv-26.) Testable: stageFixture issues a request whose body carries fixture bytes (not a bare path), and still throws on unset URL / non-2xx.

  3. Cleanup is test-owned and scoped. Each test that stages a fixture cleans up via dropFixture (fixture-staging.ts:376+) / dropFormFixture, which run PK-scoped, titlePrefix-guarded deletes across the derivation tables then the parent rows, refusing to run with an empty contentIds (throw, 376–378) or empty titlePrefix (no-op + warning, 383–385). Cleanup is best-effort (individual delete errors are logged and swallowed) so a partial cleanup does not block teardown. (P4-RECONCILIATION §2 — the assertion/cleanup surface SURVIVES. Carried {62.3} caveat: entity_mentions cleanup remains best-effort while ID-49.5 is deferred — confirm the table/FK shape when the live tier first runs so cleanup does not leak rows across runs.) Testable: dropFixture with an empty titlePrefix refuses to run; a populated call removes the staged rows it owns.


P4-RECONCILIATION §3.5 (REFRAME): the {42.10} split-assertion rationale (“split-home because pullmd is ingress: internal”) DISSOLVES on B1 — pullmd is on localhost. Most natural: the co-located verify driver runs the Inv-9 round-trip over localhost. Old Inv-26/27 survive; old Inv-28 reframes.

  1. One primitive, {42.10} is one invocation. The same B1 harness, invoked with the ID-42 {42.10} HTML fixture and the Inv-7/8/9 assertion set, produces the {42.10} end-to-end proof. ID-62 builds ONE primitive parameterised by (fixture set, assertion set); {42.10} is one invocation of it, and the ID-62 integration tier is another. The harness does not special-case HTML. (P4-RECONCILIATION §3.5 — old Inv-26 survives.) Testable: the {42.10} HTML proof and the ID-62 tier are driven by the same verify driver + /stage route with different (fixture set, assertion set) parameters; no HTML-specific staging branch exists.

  2. The {42.10} content/extraction assertions run against live Supabase. For the {42.10} invocation, the driver stages one real HTML source into the B1 cocoindex corpus, the co-located worker ingests it, and the assertions verify: (Inv-7) content_items.content_text is non-empty markdown; (Inv-8) source_documents.extraction_method = pullmd_* matching the live X-Source header. These are live-Supabase assertions, reachable from anywhere (Inv-18). (P4-RECONCILIATION §3.5 — old Inv-27 survives; the assertions are Supabase-reachable on any topology.) Testable: staging the HTML fixture yields a content_items row with non-empty markdown content_text and a source_documents row whose extraction_method is pullmd_*.

  3. The Inv-9 /s/<share_id> round-trip runs on the co-located driver over localhost. The {42.10} Inv-9 round-trip — GET <pullmd>/s/<share_id> -> 200 non-empty, asserting pullmd_share_id = X-Share-Id — runs from the co-located verify driver, reaching pullmd on http://localhost:3000 (ID-66 §1-b). The old “from inside the in-VPC job context because pullmd is ingress: internal” rationale dissolves: pullmd is a sibling container on the same host, reachable over localhost. The round-trip is one HTTP call within the driver invocation, not a separate external curl. (P4-RECONCILIATION §3.5 — old Inv-28 reframed; decision per OQ-62-P4-2 default, recorded in OQ-pending.md.) Testable: the driver, run on the B1 host, issues a GET http://localhost:3000/s/<id> for the staged HTML’s pullmd_share_id and asserts a 200 non-empty response; the same assertion is not attempted from an off-host context.


Carried TECH-level constraints (do not resolve here — {62.3} owns these)

Section titled “Carried TECH-level constraints (do not resolve here — {62.3} owns these)”

P4-RECONCILIATION §3 (CARRIED): the surviving TECH OQs, reframed for B1. These are surfaced so the Checker can confirm they are carried forward, not silently dropped. The moot ones (old OQ-62-2 volume kind, the in-VPC parts of old OQ-62-1) are NOT carried.

  1. OQ-62-1 (reframed) — corpus path value + activation on the B1 host. The concrete COCOINDEX_SOURCE_PATH value (e.g. /corpus, on the persistent-disk volume), the dir-exists-at-boot guarantee, the .kh-workspace-map.json seed (Inv-17), and the container-(re)start step that arms the watch (the watch arms once at app_main; today "" = idle) are TECH decisions for the B1 Compose/host env. The Filestore-vs-ephemeral fork (old OQ-62-2) is MOOT — the corpus is persistent local disk by construction.

  2. OQ-62-6 — titlePrefix injection per fixture format. pollContentItemsFor matches content_items.title ILIKE '${titlePrefix}%' (line 199). {62.3} decides, per fixture format, where /stage injects the prefix into title metadata and the fallback when a format carries no title (e.g. derive the title from the filename / destPath). The form-extraction tier polls on name/storage_path instead of title (486–504), so the injection strategy may differ per tier.

  3. OQ-62-7 — wire contract: exact byte encoding. PRODUCT mandates bytes on the wire (Inv-2, Inv-19). {62.3} fixes the exact encoding (multipart form-data vs base64-in-JSON), the metadata fields carried alongside the bytes (destPath, titlePrefix), and whether the public StageFixtureArgs retains fixturePath (helper reads host-side) or is restructured.

  4. OQ-62-P4-2 (carried for {62.3} mechanics) — /stage HTTP vs direct disk-drop. PRODUCT sets the default to the /stage HTTP route over localhost (Inv-12). {62.3} confirms the driver’s HTTP-POST mechanics and whether the direct-disk-drop fallback is implemented at all, and decides where any Tier-3 topology assertions (that fetch a localhost URL) run (most naturally: the co-located driver). Recorded as OQ-62-P4-2 in OQ-pending.md.


Trigger policy + launch-time scheduled-flip

Section titled “Trigger policy + launch-time scheduled-flip”

P4-RECONCILIATION §3.6 (REPLACE mechanism, keep policy): the on-demand gating policy is topology-agnostic and SURVIVES; the trigger MECHANISM (WIF + gcloud run jobs execute) is replaced by the B1 trigger. Old Inv-19/35 survive as policy.

  1. On-demand, not PR-blocking (today). The live-tier verification is triggered on-demand (an operator runs the verify driver on the B1 host, or a B1 push-to-deploy/operator hook triggers it — ID-66 §1-d), NOT inlined into the PR-blocking ci.yml integration job, and NOT scheduled today. The rationale: users are not editing the corpus yet (no nightly drift to catch), and the live tier carries higher latency + external-dependency variance (Anthropic tail latency, container warmup) than belongs in a per-PR gate. The existing PR-blocking integration job is unchanged. There is NO gcloud run jobs execute, NO WIF, NO Cloud Run Job in the trigger path. (P4-RECONCILIATION §3.6 — old Inv-19 policy survives; the mechanism is the B1 trigger.) Testable: the live tier is not present in the PR-blocking ci.yml integration job; it is invoked by an explicit operator/B1-host trigger.

  2. Launch-time scheduled-flip is tracked, not silently omitted. The decision to run the live tier on-demand (Inv-28) is explicitly a pre-launch posture. A tracked action item records: flip to a scheduled/automatic cadence at launch. When the platform launches and users begin editing the corpus, the live-tier verification gains a scheduled cadence (e.g. a nightly trigger on the B1 host, or promotion to a gate) so corpus drift is caught automatically. This invariant is satisfied by the presence of that recorded action item — not by wiring the schedule now. The omission of a schedule today is deliberate and documented. (P4-RECONCILIATION §2 — old Inv-35 launch-flip survives; {62.3}/{62.4} name the canonical home for the action item under B1.) Testable: a tracked action item for the launch-time scheduled-flip exists in the sequencing/ops record; no schedule is wired today.

This PRODUCT cites no NEW third-party library symbols requiring an import-and-call check. The topology change (Cloud Run in-VPC → B1 co-located localhost) is agnostic to the route/watcher code: the /stage route, the walk_dir(live=True) watcher, and the aiohttp server run identically regardless of where the host sits.

  • localfs.walk_dir(source_path, live=True, recursive=True) (cocoindex) — the only external symbol referenced. Pinned at cocoindex[postgres]==1.0.3 (requirements.txt:44). Already empirically verified in the codebase: flow.py:1683–1687 call site; server.py:191 docstring “Verified empirically against installed cocoindex 1.0.3”. Result: PRESENT (carried verification, 29/05/2026). No re-verification required — the symbol is unchanged by the B1 topology.
  • aiohttp — the /stage route and /health use aiohttp, an already-present, exercised dependency (server.py:44; build_app 153–161; multipart handling verified in the prior {62.3} TECH §Verification). No new import.

The one infra assumption the OLD spec flagged for empirical verification — internal→internal Cloud-Run-Job-to-Service reachability (old Inv-34 / the OQ-62-9 entry gate) — is DELETED, not carried (P4-RECONCILIATION §4 item 7): under B1 there is no internal-ingress Service to probe and no Cloud Run Job, so the entry gate is moot. The premise it would have tested was itself the thing the S281 inversion found false; B1 removes the surface entirely.

DatePinned versionSymbol path checkedResult
29/05/2026cocoindex[postgres]==1.0.3 (requirements.txt:44)cocoindex.connectors.localfs.walk_dir(..., live=True, recursive=True)PRESENT (carried — call site flow.py:1683–1687; empirical-verification docstring server.py:191)
29/05/2026aiohttp (present dependency, server.py:44)aiohttp.web app/router (build_app 153–161)PRESENT (carried — exercised by /health; multipart verified in prior {62.3} TECH §Verification)

No ABSENT / SIGNATURE_DRIFT / BEHAVIOUR_DRIFT results: this re-spec introduces no new external-symbol citations beyond the two already-pinned, topology-agnostic dependencies.