Skip to content

Intended-Architecture Evidence — Vercel-side HTML-extraction cutover (new standalone Task scoping)

Intended-Architecture Evidence — Vercel-side HTML-extraction cutover (new standalone Task scoping)

Section titled “Intended-Architecture Evidence — Vercel-side HTML-extraction cutover (new standalone Task scoping)”

Date: 2026-06-15. Mode: Research-only (no code, no edits, no ledger). Question driving this: ID-42’s retire-by-re-point premise broke — pullmd is host-local to the Coolify/B1 compose network, UNREACHABLE from Vercel, no TS client. What did the intended architecture ALWAYS intend for the two live extraction surfaces, and is there a planned reachability mitigation?

Doc root: /Users/liamj/Documents/development/knowledge-hub-docs-site/src/content/docs/themes/canonical-pipeline/


The intended architecture is unambiguous and consistent across every doc: extraction does NOT run on Vercel. Both Surface A and Surface B retire INTO the cocoindex/B1 Python pipeline, where pullmd lives as a per-MIME @coco.fn adapter behind cocoindex. The intended architecture never has the Vercel app call pullmd at all — so “pullmd unreachable from Vercel” is not a defect against intent; it is the intended topology. The mitigation the docs ratify is option (c): move the extraction logic server-side into the cocoindex worker (S299/S300, RATIFIED). There is no ratified plan to stand up a pullmd public ingress or a Vercel→B1 proxy — those appear only as a rejected/secondary alternative in a 2026-06-15 survey-agent note, never in the canonical docs.

The one genuinely OPEN decision is Surface B’s synchronous UX (manual single-URL ingest returns the created item; an async B1 pipeline cannot). The intended-architecture docs do not reconcile this — that is the gap Liam must decide.


Part 2 — Q1–Q5 with exact quotes + cites

Section titled “Part 2 — Q1–Q5 with exact quotes + cites”

Q1 — Surface A end-state (sector-intel Firecrawl/Jina cascade)

Section titled “Q1 — Surface A end-state (sector-intel Firecrawl/Jina cascade)”

Verdict: COLLAPSED / RETIRED into pullmd-behind-cocoindex. Web/RSS ingestion moves INTO the cocoindex/B1 pipeline. Surface A becomes dead.

The collapse-list retires the entire Surface-A cascade file-by-file and the Firecrawl dependency:

  • 07-collapse-list.md §5.2 (lines 152-154):

    | lib/extraction/ (~1,000 LOC) | [RATIFIED-RETIRE] | pullmd + cocoindex extractors. | CX.6 RESOLVED + cocoindex-eval §5.3 | | lib/extraction/html.ts (Readability + JSDOM) | [RATIFIED-RETIRE] | pullmd Tier 2-3 cascade. | CX.6 RESOLVED | | lib/intelligence/content-extractor.ts (Tier 2 / 2.5 / 3) | [RATIFIED-RETIRE] | pullmd. | CX.6 RESOLVED |

  • 07-collapse-list.md §7 (line 210), the Firecrawl dependency:

    | @mendable/firecrawl-js | [RATIFIED-RETIRE] | pullmd retention for HTML per COCO.5 makes Firecrawl redundant; drop after wire-up. feed_articles.extraction_method='firecrawl' retires lockstep per §11.2 below. |

  • 07-collapse-list.md §11.2 (line 280) — the CV-driven lockstep retire of the Firecrawl extraction-method:

    | feed_articles.extraction_method='firecrawl' (CHECK constraint value + 27 prod rows) | [LOCKSTEP-WITH-MIGRATION] | firecrawl pruned from CV 16 baseline_values when the CHECK migration drops it… The §7.5 “drop Firecrawl” decision landed S236…

Does web/RSS ingestion move INTO the cocoindex/B1 pipeline (making Surface A dead)? Yes — this is the explicit intent. The pipeline retires line P1 “Python URL ingest” with [RATIFIED-RETIRE] “Cocoindex flow with pullmd @coco.fn for URL shape” (07-collapse-list.md §5.1 line 140), and reference/decision-graph.md CX.13 (line 231):

| CX.13 | URL + RSS ingest — pullmd vs skill-seekers vs cocoindex? | RESOLVED | pullmd for URL (per CX.6). … cocoindex orchestrates. RSS stays Turndown for tier 1. |

And CX.6 itself (reference/decision-graph.md line 228) is the load-bearing ratification:

| CX.6 | pullmd adoption — replace lib/extraction/html.ts + tier 2/2.5/3 cascade? | RESOLVED | Yes… adopt for HTML/CF/GN/Reddit; … drop Firecrawl + @mendable/firecrawl-js after wire-up. |

02-data-flow.md §3.1 (line 63) places HTML extraction inside the cocoindex flow’s Stage 2, not on Vercel:

| Binary conversion | files_transform + per-MIME @coco.fn wrapper | Produce content_text: str from PDF / DOCX / XLSX via Docling; from HTML via pullmd | …

Where extraction runs (decisive for the reachability question): 03-tech-stack.md §4 (line 70) + §7.3 (line 176):

Cloud Run hosts the Python ingest pipeline: cocoindex orchestration, Docling extraction…, pullmd extraction (HTML)… Both pullmd and Docling live behind cocoindex as per-MIME @coco.fn wrappers…

(Note the Cloud-Run framing is itself stale — torn down S298, now IONOS/Coolify — but the placement of pullmd behind cocoindex, server-side is unchanged and is what matters.)

Quote that most settles Q1: 07-collapse-list.md §5.2 — lib/intelligence/content-extractor.ts (Tier 2 / 2.5 / 3) | [RATIFIED-RETIRE] | pullmd. | CX.6 RESOLVED. Surface A is ratified-retire; its replacement is pullmd-behind-cocoindex, which runs in the B1 worker, never on Vercel cron.


Q2 — Surface B end-state (manual single-URL ingest → reference_items)

Section titled “Q2 — Surface B end-state (manual single-URL ingest → reference_items)”

Verdict: Intended-arch retires the synchronous Vercel extraction path (P4 / lib/extraction/) into cocoindex+pullmd, same as Surface A. BUT the intended-architecture docs do NOT reconcile the synchronous-UX problem — that reconciliation is silent/open.

The same collapse-list rows above cover Surface B’s extraction code (lib/extraction/url.ts, lib/extraction/html.ts, unpdf, @mozilla/readability, JSDOM):

  • 07-collapse-list.md §5.1 line 142:

    | P4 TS URL ingest (lib/extraction/) | [RATIFIED-RETIRE] | pullmd Tier 2-3 cascade + cocoindex flow. | CX.6 RESOLVED |

  • 07-collapse-list.md §7 lines 207-209: @mozilla/readability [RATIFIED-RETIRE]; JSDOM [RATIFIED-RETIRE]verify (may be retained by the ontology path — confirm before dropping, ID-42 {42.11-13}); unpdf [RATIFIED-RETIRE].

So the extractor for Surface B is intended to retire into cocoindex+pullmd (PDF→Docling/pdfplumber, HTML→pullmd).

The async/sync reconciliation — SILENT. The intended-architecture docs describe ingest as a file-staged, cocoindex-walked, eventually-consistent flow (02-data-flow.md §2.1 line 34: “The source of truth for every content item is the client’s own file system”; §2.2: cocoindex walk_dir(live=True) translates file-change events into pipeline runs). They never address a synchronous API route that must return the created row to the caller. There is no quote in 01/02/03/07/08-arch reconciling “route returns the created item” with an async B1 pipeline.

The async/sync tension is acknowledged ONLY in the live planning/research layer, not the intended-arch docs:

  • ID-75 shipped Surface B’s destination change (URL ingest → reference_items + source_documents) while leaving the extractor in place; the manual route is app/api/ingest/url/route.ts and lands SYNCHRONOUSLY today (confirmed by the S369 survey: “Manual URL ingest → reference_items (ID-75 destination changed, not extraction code)”).
  • The “manual URL → reference_items” disposition has its own live Task ID-110 (= bl-298) id-110-url-import-reference-items/RESEARCH.md — it explicitly frames open questions for Liam (“Option A/B/C/D… body producer”) and is not an intended-arch ratification. This is where the sync-vs-async producer question actually lives, unresolved.

Quote that most settles Q2: 07-collapse-list.md §5.1 — “P4 TS URL ingest (lib/extraction/) | [RATIFIED-RETIRE] | pullmd Tier 2-3 cascade + cocoindex flow.” The extractor retires into cocoindex+pullmd; the synchronous-return UX is NOT reconciled anywhere in intended-arch (open at ID-110/bl-298).


Q3 — The reachability question: IS there a planned mitigation?

Section titled “Q3 — The reachability question: IS there a planned mitigation?”

Verdict: YES — option (c), “extraction runs server-side in the cocoindex worker where pullmd is reachable,” is the CANONICAL, RATIFIED intent. Options (a) public pullmd ingress and (b) Vercel→B1 proxy are NOT in any canonical doc; they appear only as a secondary/rejected alternative in a 2026-06-15 survey-agent note.

The intended architecture places pullmd behind cocoindex in the worker (Q1 quotes: 02-data-flow.md §3.1, 03-tech-stack.md §4/§7.3). The Vercel app is intended to “read results from Postgres and expose them through the MCP server” — it does NOT call the extraction stack:

  • 03-tech-stack.md §3.2 (line 50):

    The Cloud Run sidecar (§4) absorbs Docling + cocoindex + pullmd; the Vercel application reads results from Postgres and exposes them through the MCP server.

So (c) is the architecture, not a mitigation bolted on — Vercel was never intended to reach pullmd.

The S299/S300 ratified wiring (this is the canonical “how a URL reaches pullmd” answer):

reference/pullmd-wiring-design-s300.md is RATIFIED by Liam at S300 (top banner, lines 19-28):

RATIFIED (S300, Liam): Option A (cocoindex remote/custom source) confirmed as primary, with B2 as the fallback if cocoindex 1.0.3’s custom-source surface proves non-functional… Land as a new TaskOQ-3 decided: the original-URL provenance is a new source_documents.source_url COLUMN — NOT JSONB

The three wiring options it weighs (§3) — and which were ratified — map directly onto your (a)-(d):

  • Option A (RATIFIED primary) = a cocoindex remote/custom source connector that yields URLs into the pipeline; _pullmd_to_markdown(item.url) runs in the worker. (pullmd-wiring-design-s300.md §3 Option A, lines 174-223; §3.5 recommendation lines 325-359.) → This is your (a) collapse Surface A/B into the cocoindex/B1 pipeline, done correctly (URL source, not local file).
  • Option B2 (RATIFIED fallback) = a fetch worker that calls pullmd and writes the canonical tables behind the same declare_row schema — still server-side, still in the worker. (§3 Option B, lines 225-266.)
  • Option C (DEMOTED) = .url pointer-file on localfs — “Acceptable only as a bounded batch fallback… Not recommended for the live-feed wiring” (§3 Option C lines 268-301).

reference/pullmd-cocoindex-intent-s299.md is the design-input predecessor (NOT ratified — line 12 “nothing here is ratified”) and carries Liam’s S299 design steer that URL/RSS feeds are inherently remote and belong server-side, not on localfs (§4.0 lines 251-273):

Liam’s steer at S299… URL/RSS feeds are inherently remote and belong to the intelligence workspace… they would never naturally live on localfs. … the next-session spec must evaluate a proper remote URL/feed source

Is there ANY canonical mention of a pullmd public ingress or a Vercel→B1 proxy (your (a)/(b))? NO. Neither pullmd-cocoindex-intent-s299.md, pullmd-wiring-design-s300.md, reference/decision-graph.md, nor the intended-arch sub-docs propose exposing pullmd publicly or proxying Vercel→B1. Every canonical option keeps extraction server-side. The ONLY place those two appear is a 2026-06-15 survey-agent diary (mempalace, see Part 3), which lists them as the alternative it would NOT take:

Consequence for retirement: Before retiring Surfaces A/B, you must either: 1. Move HTML extraction logic server-side (into cocoindex worker) where pullmd is reachable, OR 2. Expose pullmd via a public HTTP ingress so the Vercel app can call it directly

That note frames (1) [=option c] and (2) [=option a/ingress] as the two choices; the canonical docs already chose (1).

Quote that most settles Q3: pullmd-wiring-design-s300.md RATIFIED banner — “Option A (cocoindex remote/custom source) confirmed as primary, with B2 as the fallback… Land as a new Task.” The reachability mitigation is “do the extraction in the worker,” and the wiring is ratified as a cocoindex remote URL source (A) with a fetch-worker fallback (B2). Public ingress / Vercel proxy are NOT canonical.


Q4 — AGPL boundary constraint (the Inv-9 boundary)

Section titled “Q4 — AGPL boundary constraint (the Inv-9 boundary)”

Verdict: pullmd MUST stay a separately-deployed, self-hosted network service called over HTTP. The AGPL “network service” clause is the reason it is a separate service — and that constraint is fully satisfied by the server-side (option c) topology. It does NOT forbid a TS client or a public ingress per se (HTTP calls don’t trigger AGPL), but the docs flag forking pullmd source into KH as the actual AGPL risk, and no canonical doc proposes a TS client because Vercel was never meant to call pullmd.

  • 03-tech-stack.md §7.4 (line 170):

    pullmd is AGPL v3 (vs Docling MIT). License compatibility per phase-b-prerequisite-2d-docling-bakeoff.md §3.3: KH calls pullmd as a separate self-hosted network service via HTTP, so the AGPL “network service” clause does not propagate to KH platform code.

  • 03-tech-stack.md §7.5 (line 172): “pullmd runs as a self-hosted Docker stack (80 MB base + 3.7 GB Playwright sidecar when enabled).”

  • phase-0-investigation/0.8.4-pullmd-evaluation.md §10 / PM-Q2 (line 536) + PM10 (line 459):

    PM-Q2 | AGPL v3 acceptable for self-hosted use? | If KH never forks pullmd source into the platform, AGPL “network service” clause likely doesn’t trigger. But Liam should confirm | Liam call — likely yes PM10 | AGPL v3 self-hosted is licence-compatible with KH’s posture? | LIKELY YES (self-hosted, no fork into KH), but Liam should sign off given AGPL’s “network service” clause | 80%

  • reference/decision-graph.md CX.8 (line 313):

    | CX.8 | pullmd AGPL v3 — direct re-use vs fork? | PROVISIONAL | Direct re-use OK; fork only if needed (modification triggers AGPL). Per wave-08 D7.

Effect on a TS client / public ingress: The AGPL constraint is about not forking pullmd source into KH code, not about who calls it over HTTP. An HTTP call from Vercel (whether direct via ingress, or via a proxy) would NOT itself trigger AGPL — so AGPL is not the blocker to a public ingress. The blocker to the ingress route is purely the topology decision (extraction belongs server-side, Q1-Q3) plus the operational/SSRF surface of exposing pullmd publicly. The deployed compose already enforces the separate-service boundary host-locally (http://pullmd:3000 / http://pullmd-staging:3000, no public ingress).

Quote that most settles Q4: 03-tech-stack.md §7.4 — “KH calls pullmd as a separate self-hosted network service via HTTP, so the AGPL ‘network service’ clause does not propagate to KH platform code.” The boundary is satisfied by HTTP-from-the-worker; AGPL does not by itself rule out an ingress, but no canonical doc wants one.


Q5 — Sequencing / deferral: is ID-42 {42.11-13} / Firecrawl removal / Vercel cutover already named as deferred/blocked, and on what?

Section titled “Q5 — Sequencing / deferral: is ID-42 {42.11-13} / Firecrawl removal / Vercel cutover already named as deferred/blocked, and on what?”

Verdict: YES. The {42.10}-{42.13} retirement is explicitly DEFERRED in the sequencing doc; the reachability defect that blocked it ({62.1}) is recorded as DISSOLVED at S288 (move to IONOS co-location). The Firecrawl removal is ratified but timing-gated on wire-up. The reachability mitigation gap is NOT in the deferral-register (both S299 and S300 explicitly note it is still unrecorded there).

reference/canonical-pipeline-sequencing.md (the live “where are we / what next”, S281→S299) carries the deferral and the dissolution:

  • Lines 60-62 + 333-336 (retirement deferred; pullmd already deployed host-local):

    ID-42 {42.11}-{42.13} HTML-extraction retirement (pullMD already deployed+healthy on-prem at http://pullmd:3000 — nothing to deploy; retirement is dead-code cleanup) The RETIREMENT of the app-side HTML-extraction path ({42.10}-{42.13}) is deferred to a future session — {42.10}‘s non-mocked E2E proof needs a deployed worker with VPC access (the pullmd Service is ingress:internal)…

  • Lines 189-192 — the reachability defect explicitly DISSOLVED by co-location:

    now LIVE on IONOS via Coolify (S288)** — a 4-service co-located compose (cocoindex + pullmd + …). This dissolves the {62.1} cocoindex→pullmd reachability defect (now a host-local http://pullmd:3000 call)…

  • Lines 205-207 + 338-342 — the prior (S281) confirmed-broken framing (Cloud Run ingress=internal, zero VPC egress) — i.e. the historical defect this dissolution replaces:

    HTML datapath confirmed BROKEN as deployed ({62.1} reachability, ID-62)… both Cloud Run services are ingress=internal and the cocoindex caller has ZERO VPC egress

Crucial nuance: the dissolved {62.1} defect is the cocoindex→pullmd (worker→pullmd) reachability — which co-location fixed. The defect that scoped THIS task — Vercel app→pullmd — is a different edge. The intended architecture never has Vercel call pullmd (Q1-Q3), so the Vercel-side cutover means deleting Surface A/B Vercel code once the worker-side URL ingest (S300 Option A) lands, NOT making Vercel reach pullmd.

Deferral-register status: the PullMD/URL-wiring gap is not in deferral-register.md (verified — its tables V11/V2/PL/C cover q_a_pairs privacy, admin UI, sidecars, mempalace, etc., but no pullmd/URL row). Both design docs flag this:

  • pullmd-cocoindex-intent-s299.md §4.4 item 7 (line 388): “this gap is NOT yet in …/deferral-register.md (verified — no PullMD/URL entry). The next session should add it…”
  • pullmd-wiring-design-s300.md §5.4 (line 448): “the PullMD/URL wiring gap is still not recorded in …/deferral-register.md (verified — no PullMD/URL row). The new Task should add it…”

reference/operational-pre-decisions-register.md covers cutover operational decisions (walk cadence, datapath-watch cron, Vercel cron re-enable) but does NOT name the Surface A/B retirement or a Vercel-reachability mitigation.

Quote that most settles Q5: canonical-pipeline-sequencing.md — “The RETIREMENT of the app-side HTML-extraction path ({42.10}-{42.13}) is deferred to a future session” + “This dissolves the {62.1} cocoindex→pullmd reachability defect (now a host-local http://pullmd:3000 call).” The retirement is deferred; worker→pullmd reachability is fixed by co-location; the Vercel-side cut is the dead-code removal that this new Task owns.


Tooling status: mempalace_search MCP tool worked (no wing filter, per the known-broken-filter guidance). The archive palace CLI is UNAVAILABLEmempalace --palace ~/.mempalace-archive search … returned: “No palace found at /Users/liamj/.mempalace-archive — Run: mempalace init

then mempalace mine .” So the archive (point-in-time) leg could NOT be run; flagging explicitly rather than failing silently.

Most load-bearing hit (the single richest, decision-relevant memory): a 2026-06-15 survey sub-agent drawer, source_file: agent-acd25bb8bcaa6e02d.jsonl, wing: sessions / room: technical (this is the sibling survey for THIS same task — note the cmux-events path bfcbad16-…/survey-42-retire.md). It independently re-derived the exact reachability situation and the same two-option framing the canonical docs already resolved:

  • The defect, verbatim:

    pullmd is deployed as a service internal to the compose stack only, reachable via host-local DNS aliases (http://pullmd:3000 or http://pullmd-staging:3000). It has NO public ingress and runs in a separate Docker Compose network from the Vercel app.

  • The mitigation framing (the (c)-vs-(ingress) choice):

    Consequence for retirement: Before retiring Surfaces A/B, you must either: 1. Move HTML extraction logic server-side (into cocoindex worker) where pullmd is reachable, OR 2. Expose pullmd via a public HTTP ingress so the Vercel app can call it directly

  • The current live traffic map (decisive for “what is still live”):

    | A (Firecrawl) | lib/intelligence/feed-poller.ts::pollWebSource() line 430 | Every web-source poll (unless HEAD-304) — PAID credit cost | | A (Jina) | content-extractor.ts Tier 2.5 line 244 | RSS articles failing Tier 1-2 → free Jina fallback | | B (readability) | app/api/ingest/url/route.ts line 115 | Manual URL ingest → reference_items (ID-75 destination changed, not extraction code) | | B (unpdf) | app/api/ingest/url/route.ts line 97 | PDF URL ingest → reference_items |

  • And the key status line:

    Key finding: Surface B has been redirected to reference_items (not content_items), but the extraction code itself is unchanged — readability + unpdf are still running. Surface A (Firecrawl) is untouched and actively incurring credits on every web-source poll.

Other hits (S369-era / current corpus, 2026-06-15):

  • agent-a3e9cd525848ac00f.jsonl (planning) confirms the URL-source substrate already partly exists in code: scripts/cocoindex_pipeline/url_source.py with FeedUrlSource / UrlItem, mounted at {75.11} as “the passed-ledger snapshot source app_main mounts” — i.e. ID-75 has begun implementing S300 Option A (a cocoindex source that yields URLs). flow.py line 143-147: “ID-75 WP-C — URL-source substrate… FeedUrlSource is the passed-ledger snapshot source.” This is strong corroboration that the worker-side URL ingest (Option A) is the path being built.
  • b6u0c0jgt.txt (problems) — id-110-url-import-reference-items/RESEARCH.md: ID-75 “already ratified the disposition (PRODUCT BI-9/BI-23; TECH §6.3)” for url_import→reference_items; ID-110 (=bl-298) frames the remaining manual-URL producer questions for Liam. This is where Surface B’s sync-vs-async producer decision actually lives.
  • agent-ad902acf78cb6ac56.jsonl (technical): “ID-110=bl-298 (url_import→reference_items)” — confirms the bl/ID mapping.

No mempalace memory proposed a pullmd public ingress or a Vercel→B1 proxy as a chosen approach — only the survey-agent’s option-2 framing (which it presents as the alternative to the server-side route).


Part 4 — Synthesis (best-supported reading of what the new Task should be FOR)

Section titled “Part 4 — Synthesis (best-supported reading of what the new Task should be FOR)”

The new standalone Task should be approach (a): finish collapsing Surface A and Surface B into the cocoindex/B1 pipeline, wired via a cocoindex remote URL source (S300 Option A, B2 fallback), then delete the Vercel-side extraction code. It should NOT stand up a pullmd ingress (b) or a Vercel→B1 proxy (c); both are off-intent.

Why this is the grounded reading:

  1. Every canonical doc places extraction server-side, behind cocoindex, and ratifies retiring the Vercel cascade. Surface A (content-extractor.ts, Firecrawl, Jina) and Surface B (lib/extraction/*, readability, unpdf) are all [RATIFIED-RETIRE] ”→ pullmd + cocoindex” (collapse-list §5.1/§5.2/§7; CX.6/CX.13). The Vercel app is intended only to “read results from Postgres” (03-tech-stack §3.2). “pullmd unreachable from Vercel” is therefore the intended topology, not a defect to mitigate by making Vercel reach it.

  2. The reachability mitigation is already ratified as “do it in the worker.” S300 RATIFIED Option A (cocoindex remote/custom URL source) primary + B2 (fetch worker writing canonical tables) fallback, and explicitly says “Land as a new Task” with ID-42 closing on its real (deploy + adapter + provenance) scope. Your new Task IS that S300-named new Task. Liam’s S299 steer (“URL/RSS feeds are inherently remote, never on localfs”) points the same way and rules out the Option-C pointer-file for live feeds.

  3. The worker→pullmd reachability is already solved by S288 co-location (host-local http://pullmd:3000), which “dissolves the {62.1} reachability defect.” So building Option A means the worker calls a reachable pullmd; nothing new on the reachability front is required. The only remaining work is (i) wire the cocoindex URL source (already begun in url_source.py / FeedUrlSource under ID-75), (ii) re-prove {42.10} against a real URL, (iii) delete Surface A/B Vercel code ({42.11}-{42.13}), (iv) drop Firecrawl/Jina/readability/unpdf deps, and (v) re-ingest the bounded existing feed_articles/url corpus (ID-45 batch note).

  4. AGPL is not a blocker and not a driver of a different design. The separate-service-over-HTTP boundary is satisfied by the worker calling pullmd; AGPL only forbids forking pullmd into KH source (CX.8, PM-Q2). So neither a TS client nor an ingress is AGPL-blocked — they are simply off-architecture.

Where the docs are SILENT or CONTRADICTORY (what Liam still has to decide):

  • (SILENT — the big one) Surface B’s synchronous UX. Intended-arch models ingest as async file-walked flow and never reconciles “the manual /api/ingest/url route returns the created reference_items row synchronously” with an async B1 pipeline. The async producer question is open at ID-110 / bl-298 (and ID-75 OQ-T1), not in intended-arch. Liam must decide: keep a synchronous Vercel fetch+write for the manual single-URL case (a deliberate exception to “extraction runs in the worker”), or make manual ingest async (enqueue a URL → worker extracts → row appears later, changing the UX). This is the load-bearing product decision; the rest is mechanical.

  • (STALE, needs noting) Cloud Run framing throughout 02/03-arch. All the §4 “Cloud Run sidecar” text is superseded — Cloud Run torn down S298, pipeline now IONOS/Coolify. The placement (pullmd behind cocoindex, server-side) is unchanged, but the new TECH spec must be written against IONOS/Coolify, not Cloud Run (S300 §5.1 makes this an explicit reason for a fresh Task vs amending ID-42).

  • (SILENT) JSDOM retain caveat. collapse-list §7 line 208 flags JSDOM as [RATIFIED-RETIRE]verify — “may be RETAINED by the ontology path — confirm before dropping (ID-42 {42.11-13}).” So the dependency-removal slice must check JSDOM’s non-extraction (ontology) callers before deleting it.

  • (SILENT) cocoindex 1.0.x custom-source viability for Option A. S300 §5.2/§6 makes Option A conditional on empirically verifying that cocoindex accepts a usable custom/remote source (the historical bind_target non-functional-placeholder risk); if it fails, fall back to B2. The pin has since moved to 1.0.7 (mempalace), and url_source.py/FeedUrlSource already exists under ID-75 — so this risk may already be retired, but the new Task should confirm.

  • (NOT CONTRADICTORY, but split ownership) Surface A live-feed wiring (S300 Option A) vs Surface B manual-URL producer (ID-110/bl-298) vs the bounded existing-corpus re-ingest (ID-45) are three adjacent threads. The new Task should explicitly state which it owns and cross-link the other two, or it will collide with ID-75/ID-110/ID-45.

One-paragraph synthesis: The intended architecture has, consistently and across every canonical doc, decided that HTML/URL extraction runs server-side in the cocoindex/B1 worker behind pullmd, and that the Vercel Surfaces A and B (Firecrawl/Jina cascade + readability/unpdf URL ingest) are [RATIFIED-RETIRE] into that pipeline. The reachability “mitigation” is therefore not new infrastructure but the already-ratified worker-side topology, with the URL→pullmd wiring ratified at S300 as a cocoindex remote URL source (Option A) and a fetch-worker fallback (B2) — explicitly to be landed as a NEW Task, which is exactly this one. The new Task should be FOR: wiring the cocoindex worker-side URL ingest, re-proving the HTML datapath against a real URL, and deleting the now-dead Vercel Surface A/B code + Firecrawl/Jina/readability/unpdf deps — NOT for a pullmd public ingress or a Vercel→B1 proxy, both of which are off-intent. The single decision Liam still owns is whether the manual single-URL ingest keeps its synchronous return UX (a deliberate Vercel-side exception) or becomes async — a question intended-arch is silent on and that lives at ID-110/bl-298.


Appendix — file/anchor index of load-bearing cites

Section titled “Appendix — file/anchor index of load-bearing cites”
  • intended-architecture/02-data-flow.md §2.1 (L34), §2.2 (L36), §3.1 table (L62-67, esp. L63 HTML-via-pullmd)
  • intended-architecture/03-tech-stack.md §3.2 (L50), §4 (L70), §7.2-7.5 (L150-176; AGPL L170; “behind cocoindex” L176)
  • intended-architecture/07-collapse-list.md §5.1 (L138-159: P1 L140, P4 L142), §5.2 (L150-159: lib/extraction L152, html.ts L153, content-extractor.ts L154), §7 (L205-212: firecrawl L210, readability L207, JSDOM L208 ⚠verify, unpdf L209), §10 L262 (pullmd NOT-CONSOLIDATING), §11.2 (L280 firecrawl lockstep)
  • reference/decision-graph.md CX.6 (L228), CX.7 (L229), CX.8 (L313), CX.13 (L231), CX.30 (L242), Q2.7 (L188)
  • reference/pullmd-cocoindex-intent-s299.md — NOT ratified (L12); §4.0 Liam steer (L251-273); §4.1 pointer-file (one option, L275-310); §4.4 gap not in deferral-register (L388)
  • reference/pullmd-wiring-design-s300.mdRATIFIED S300 banner (L19-28); §3 Options A/B/C (L162-301); §3.5 recommend A+fold-in-B (L325-359); §5.1 new-Task landing (L379-407); §5.4 deferral-register gap (L448)
  • reference/canonical-pipeline-sequencing.md — {42.11-13} deferred (L60-62, L333-336); {62.1} dissolved by S288 co-location (L189-192); prior confirmed-broken (L205-207, L338-342)
  • reference/deferral-register.md — verified: NO pullmd/URL row (tables §1-§4)
  • reference/operational-pre-decisions-register.md — cutover operational decisions only (walk cadence, datapath-watch, Vercel cron); no Surface A/B retire row
  • phase-0-investigation/0.8.4-pullmd-evaluation.md §1 (L17, L33 self-hosted network service), PM10 (L459), PM-Q2 (L536), §6 self-hosted topology (L368-372)
  • spike-findings/0.9-spike-S4-pullmd-bakeoff.md — proven as remote-URL fetcher (GET http://localhost:3000/api?url=), Firecrawl PAID per-call (L320), drop-Firecrawl verdict (L402)
  • mempalace: agent-acd25bb8bcaa6e02d.jsonl (survey, 2026-06-15) — reachability defect + (c)/(ingress) framing + live-traffic map; agent-a3e9cd525848ac00f.jsonlurl_source.py/FeedUrlSource/UrlItem already in code (ID-75 WP-C, {75.11}); b6u0c0jgt.txt — ID-110/bl-298 url_import→reference_items
  • Archive palace CLI: UNAVAILABLE (no palace at ~/.mempalace-archive) — archive leg not run.