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/
BOTTOM LINE (read this first)
Section titled “BOTTOM LINE (read this first)”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]|firecrawlpruned from CV 16baseline_valueswhen 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-jsafter 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.fnwrapper | Producecontent_text: strfrom 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.fnwrappers…
(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 isapp/api/ingest/url/route.tsand 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 Task… OQ-3 decided: the original-URL provenance is a new
source_documents.source_urlCOLUMN — 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_rowschema — still server-side, still in the worker. (§3 Option B, lines 225-266.) - Option C (DEMOTED) =
.urlpointer-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.mdRATIFIED 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.mdCX.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 athttp://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 isingress: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:3000call)… -
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=internaland 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-localhttp://pullmd:3000call).” 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.
Part 3 — Mempalace search results
Section titled “Part 3 — Mempalace search results”Tooling status: mempalace_search MCP tool worked (no wing filter, per the known-broken-filter guidance). The archive palace CLI is UNAVAILABLE — mempalace --palace ~/.mempalace-archive search … returned: “No palace found at /Users/liamj/.mempalace-archive — Run: mempalace init
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:3000orhttp://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.tsTier 2.5 line 244 | RSS articles failing Tier 1-2 → free Jina fallback | | B (readability) |app/api/ingest/url/route.tsline 115 | Manual URL ingest → reference_items (ID-75 destination changed, not extraction code) | | B (unpdf) |app/api/ingest/url/route.tsline 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.pywithFeedUrlSource/UrlItem, mounted at {75.11} as “the passed-ledger snapshot sourceapp_mainmounts” — 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…FeedUrlSourceis 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:
-
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. -
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.
-
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 inurl_source.py/FeedUrlSourceunder 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 existingfeed_articles/url corpus (ID-45 batch note). -
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/urlroute returns the createdreference_itemsrow 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_targetnon-functional-placeholder risk); if it fails, fall back to B2. The pin has since moved to 1.0.7 (mempalace), andurl_source.py/FeedUrlSourcealready 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.mdCX.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.md— RATIFIED 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 rowphase-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.jsonl—url_source.py/FeedUrlSource/UrlItemalready 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.