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: internalcocoindex 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.
Summary
Section titled “Summary”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):
- A co-resident
POST /stageroute on the cocoindex aiohttp server that accepts fixture bytes and drops them into the watched corpus directory on the host’s persistent local disk. - A co-located verify driver — a process on the same B1 host — that stages fixtures by
reaching
/stageoverhttp://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.
Problem
Section titled “Problem”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:
-
No
/stageserver exists. The client helperstageFixture(__tests__/integration/cocoindex/_helpers/fixture-staging.ts:100–139) POSTs to${COCOINDEX_FIXTURE_STAGING_URL}/stage(endpoint built at line 111), butscripts/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. -
Reachability.
flow.py:app_main(line 1524) readsCOCOINDEX_SOURCE_PATH(""today → idle return, lines 1536–1543) and armslocalfs.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 wereingress: internaland 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 — novpc-access/vpc-connectorannotation exists in any manifest, so a same-project caller to aningress: internalService 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 indexedknowledge-hubrepos (the main checkout, 77 commits behind, and a sibling worktreesubo-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.pylocalfs.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, andgitnexus_contextonstageFixture/pollContentItemsForreturnsSymbol not found(confirmed in the prior{62.2}artefact and by the index scope here). These are grounded by direct file read withfile:linecites against this worktree:__tests__/integration/cocoindex/_helpers/fixture-staging.ts—hasFixtureStagingUrl(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-titlePrefixguard at 383–385), the form-template polls matching onname 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 / Non-goals
Section titled “Goals / Non-goals”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 asInv-21under the pre-P4 numbering, but current #21 at:448is 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;
/stageis 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
/stageroute + 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.)
Behavior
Section titled “Behavior”Surfaces and their consumers:
- The
/stageroute (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
/stageroute is identical on Cloud Run or a co-located host. cocoindex still runs an aiohttp server with thewalk_dir(live=True)watcher in one process; a co-resident/stageroute writing to the watched corpus dir is topology-agnostic. (Old Inv-1..6, Inv-8 — carried verbatim in spirit.)
-
Co-resident with the watcher. The cocoindex container exposes a
POST /stageroute in the same process as the cocoindex worker daemon thread, so the handler writes to the exact local filesystem directory thatlocalfs.walk_dir(source_path, live=True, recursive=True)is watching. There is NO second process and NO network hop between the/stagewriter and the watcher. (Grounds:server.py:build_app(153–161) adds routes to the same aiohttp app the daemon thread runs inside,start_cocoindex_thread169–226;flow.py:1683–1687watches an in-container local path.) Testable: the route is registered onbuild_app()’s app and a posted fixture lands in${COCOINDEX_SOURCE_PATH}without crossing a process boundary. -
Bytes on the wire, not a path the writer cannot resolve.
POST /stageaccepts the fixture bytes in the request body ({62.3}fixes the exact encoding — multipart vs base64-in-JSON), NOT afixturePathstring 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:stageFixturetoday 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 + adestPathproduces a written file; a request carrying only a path string (no bytes) is rejected. -
Writes to the corpus-relative destination. On a well-formed request with
COCOINDEX_SOURCE_PATHset 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 benigndestPathlands the file under the corpus root; a path-escapedestPathreturns a 4xx and writes nothing outside the root. -
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:
stageFixturereads{ destPath, requestId? }from the response,fixture-staging.tsStageFixtureResult73–85 + 130–138 — the response contract is preserved even though the request contract changes.) Testable: a successful POST returns 2xx and a JSON body whosedestPaththe helper can read. -
Loud reject on a mis-wired / missing corpus path. When
COCOINDEX_SOURCE_PATHis unset/empty, OR is set but names a directory that does not exist at request time,POST /stagereturns 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/stagecall rather than silently producing zero rows downstream. (Mirrors the idle-mode contract:flow.py:1536–1543treats an unset path as idle and 1545–1553 treats a missing path as idle.) Testable: withCOCOINDEX_SOURCE_PATHunset → a 4xx naming the variable; with it set to a non-existent dir → a clear error naming the path; neither returns 2xx. -
GET /healthsurvives as an optional B1 liveness hook, and/stagedoes not break it. Per Liam’s decision, no readiness/liveness probe is required under B1, and the Cloud RunhttpGet /healthprobe is gone with the Cloud Run manifest. The cheapGET /healthendpoint (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). AddingPOST /stageMUST NOT alter the/healthbehaviour: 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-/healthregression note, NOT a probe requirement. B1 datapath monitoring (watchpipeline_runsrow 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 asOQ-62-P4-3inOQ-pending.md, recommended default “keep/health” adopted.) Testable:GET /healthreturns the same status semantics before and after the/stageroute is added; staging a fixture does not flip/healthto 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.
-
The driver runs on the B1 host and reaches
/stageover localhost. The verify driver is a process on the co-located B1 host. It stages fixtures by POSTing their bytes to the cocoindex/stageroute athttp://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/stageover loopback / the host’s private container network — there is noingress: internalclassification 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/stageon a localhost port, successfully stages a fixture; the same driver run off-host (no localhost/stage) fails fast. -
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. -
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.
-
Exit-code semantics. The driver exits 0 only when every requested fixture was staged successfully (each
/stagePOST returned 2xx). It exits non-zero when any/stagePOST fails (4xx/5xx, timeout, connection-refused), and the failing fixture + the/stageresponse 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/stagereturns 2xx exits 0; a run where one fixture’s/stagereturns 4xx exits non-zero and names the failing fixture in its output. -
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. -
The driver’s default staging path is the
/stageHTTP 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/stageHTTP route over localhost (preserving the bytes-on-the-wire contract, the path-escape guard, and thetitlePrefixinjection 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 asOQ-62-P4-2inOQ-pending.md, recommended default “keep/stageload-bearing” adopted.{62.3}confirms the mechanics.) Testable: the driver’s default codepath issues an HTTP POST to a localhost/stageendpoint (not a raw filesystem write); the disk-drop path, if implemented, is gated behind an explicit non-default flag/mode.
The B1 network posture
Section titled “The B1 network posture”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.
-
/stageis NOT bound to a public interface. On the B1 host, the cocoindex/stageroute binds to localhost / the host’s private container-network interface, NOT to a public ingress./stagewrites 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:/stageis reachable from on-host localhost / the private container network and is NOT reachable from a public/external network address. -
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 noingress: allflip, 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.jsonmust exist before the watch arms — SURVIVES unchanged.
-
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. -
Corpus activation is an explicit, observable precondition. The harness only produces rows when the cocoindex container has
COCOINDEX_SOURCE_PATHset to an existing directory and has been (re)started soapp_mainarmed 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/stageloud-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. -
The workspace manifest must exist before the watch arms.
app_mainloads${COCOINDEX_SOURCE_PATH}/.kh-workspace-map.jsononce at flow start (flow.py:1582); a missing/unparseable/schema-invalid manifest ABORTS the flow with a structuredmanifest_missing/manifest_invalidstage error (flow.py:1584–1606). So the harness’s corpus directory MUST contain a valid.kh-workspace-map.jsonat the corpus root before the watch arms, or no fixture will be processed. This is a cocoindexapp_mainbehaviour (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.jsonproduces amanifest_missingstage error and nocontent_itemsrows; 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/dropFixturequery live Supabase, reachable from anywhere on any topology. The reachability rationale changes (it runs wherever the test author runs it, againstingress: allSupabase), but the assertion half itself is topology-agnostic.
-
The authored Vitest tests remain the assertion surface, asserting against live Supabase. The authored
__tests__/integration/cocoindex/*.integration.test.tstests 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) queriescontent_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 (pollFormTemplatesForetc., matchingname 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 (notskipIf-skipped) when the env is wired; a row that lands satisfies the poll; a row that never lands fails with the awaited title-prefix. -
stageFixtureposts bytes. ThestageFixturehelper and itsStageFixtureArgsshape (fixture-staging.ts:51–139) change from sending afixturePathstring to reading the fixture bytes and POSTing them in the body (Inv-2). The helper continues to env-gate onhasFixtureStagingUrl()(43–45), continues to throw fast on a missingCOCOINDEX_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 publicStageFixtureArgskeepsfixturePath(helper reads bytes host-side before POSTing) or is restructured — Inv-26.) Testable:stageFixtureissues a request whose body carries fixture bytes (not a bare path), and still throws on unset URL / non-2xx. -
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 emptycontentIds(throw, 376–378) or emptytitlePrefix(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_mentionscleanup 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:dropFixturewith an emptytitlePrefixrefuses to run; a populated call removes the staged rows it owns.
The shared {42.10} invocation
Section titled “The shared {42.10} invocation”P4-RECONCILIATION §3.5 (REFRAME): the
{42.10}split-assertion rationale (“split-home because pullmd isingress: 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.
-
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 +/stageroute with different(fixture set, assertion set)parameters; no HTML-specific staging branch exists. -
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_textis non-empty markdown; (Inv-8)source_documents.extraction_method = pullmd_*matching the liveX-Sourceheader. 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 acontent_itemsrow with non-empty markdowncontent_textand asource_documentsrow whoseextraction_methodispullmd_*. -
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, assertingpullmd_share_id = X-Share-Id— runs from the co-located verify driver, reaching pullmd onhttp://localhost:3000(ID-66 §1-b). The old “from inside the in-VPC job context because pullmd isingress: 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 inOQ-pending.md.) Testable: the driver, run on the B1 host, issues aGET http://localhost:3000/s/<id>for the staged HTML’spullmd_share_idand 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.
-
OQ-62-1 (reframed) — corpus path value + activation on the B1 host. The concrete
COCOINDEX_SOURCE_PATHvalue (e.g./corpus, on the persistent-disk volume), the dir-exists-at-boot guarantee, the.kh-workspace-map.jsonseed (Inv-17), and the container-(re)start step that arms the watch (the watch arms once atapp_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. -
OQ-62-6 —
titlePrefixinjection per fixture format.pollContentItemsFormatchescontent_items.title ILIKE '${titlePrefix}%'(line 199).{62.3}decides, per fixture format, where/stageinjects 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 onname/storage_pathinstead oftitle(486–504), so the injection strategy may differ per tier. -
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 publicStageFixtureArgsretainsfixturePath(helper reads host-side) or is restructured. -
OQ-62-P4-2 (carried for
{62.3}mechanics) —/stageHTTP vs direct disk-drop. PRODUCT sets the default to the/stageHTTP 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 (thatfetcha localhost URL) run (most naturally: the co-located driver). Recorded asOQ-62-P4-2inOQ-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.
-
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.ymlintegration 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 NOgcloud 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-blockingci.ymlintegration job; it is invoked by an explicit operator/B1-host trigger. -
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.
External-API verification (OQ-3 / Q-EX2)
Section titled “External-API verification (OQ-3 / Q-EX2)”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 atcocoindex[postgres]==1.0.3(requirements.txt:44). Already empirically verified in the codebase:flow.py:1683–1687call site;server.py:191docstring “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/stageroute and/healthuse aiohttp, an already-present, exercised dependency (server.py:44;build_app153–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.
Verification
Section titled “Verification”| Date | Pinned version | Symbol path checked | Result |
|---|---|---|---|
| 29/05/2026 | cocoindex[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/2026 | aiohttp (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.