Skip to content

Session handover — 2026-05-12

End-of-day pickup note. Today: four slices shipped end-to-end — each one closed a gap the previous slice surfaced.

  1. Slice 3 of ADR-0005 — CMS image migration (commit 1da50b6). Closed the broken-images known-issue.
  2. ADR-0004 Path A enforcement (commit 58a5b87). Once images stopped 404'ing, the user spotted that the deployed CMS rendered nothing like the per-site assembled garvanbay build — 9 of 19 CMS adapters were hand-rolled with their own CSS, violating the Path A pattern ADR-0004 had documented but not enforced. Rewritten to delegate to canonical fulldev blocks.
  3. Reseed remote D1 + About paragraph fix (commits 7a5ffa3, 608f385). D1 had stale pre-Slice-3 data; new reseed-d1-from-json.mjs brings it in sync with the current seed.json (emdash seed only writes local data.db). About bodies were collapsing to single paragraphs — the CMS adapter was splitting on \n\n only; the assembler's paragraphsFromText also splits on sentence boundaries. Shared helper at packages/components-v3/src/lib/paragraphs-from-text.ts so both consumers stay aligned.
  4. CMS theme tokens (commit 2f22d38). Generated builds/garvanbay.ie/theme.json via lib/theme-extractor.js; new apps/cms/scripts/emit-theme-css.mjs (prebuild step) writes apps/cms/src/styles/global.css from the assembler's shared emitGlobalCss(theme). Deployed CMS now carries garvanbay's actual oklch palette + Montserrat font, not the canonical's hardcoded fallback colours.

Three principles applied today: - When an editor schema's expressive power doesn't match the data shape an ADR specifies, the data shape wins. Captured in ADR-0005 §6 amendment. - Phase 3 regression checks need to compare the deployed output against the alternative consumer of the same canonical, not just "did I touch shared files." Slice 3's "no shared canonical surface touched" check missed the pre-existing visual divergence; the user surfaced it post-deploy. Captured as a new known-pattern entry. - One emit, two consumers. theme.js's emitGlobalCss and the paragraphs splitter are now genuinely shared between the assembler-fulldev (per-site Pages builds) and the CMS Worker (multi-tenant). Drift between the two consumers is what the baseline-vs-CMS comparison was tracking.


Where we ended up

  • Branch: master — five commits today, latest 2f22d38. Working tree clean.
  • Worker deployed: https://replatform-emdash-demo.cathaldempsey.workers.dev — latest version 7b0bf8e8-ce88-4d98-8614-f08b35d30182 (theme slice). Earlier versions in session: 8c83c3a6 (Slice 3), 0e823a0b (Path A), 7bedb93c (paragraph fix).
  • R2 bucket: replatform-emdash-media — 40 garvanbay objects uploaded under garvanbay/ (16 fresh + 24 idempotent). wfpainters Slice-4 corpus (158 objects) untouched.
  • D1: replatform-emdash-demo — 11 garvanbay pages + 1 globals
  • 1 adapter-test row, all with object-shape imageUrl and current seed.json content (post-reseed).
  • theme.json: garvanbay's extracted theme tracked at builds/garvanbay.ie/theme.json via a new !builds/*/theme.json gitignore exception.

Today's work — what shipped

Slice 3 of ADR-0005 — transformer integration (done)

Done criteria status:

  • (a) builds/garvanbay.ie/public/assets/images/ populated with 40 source images via direct fetch from static.wixstatic.com/media/ (transformer downloads on demand instead of running dom-pipeline.js as a separate step — false-stop simplification resolved during execution).
  • (b) Block-level scalar imageUrl / logoUrl data is now { src, originalName?, alt? } shape across seed.json and live D1. Editor schema declarations stay scalar (emdash schema constraint documented in §6 amendment) — data ↔ schema decoupling is the contract.
  • (c) 10 image-touching adapters under apps/cms/src/components/fcr-blocks/ (Hero, About, ServiceGrid, TeamGrid, CtaStrip, BlogPosts, Gallery, LogoStrip, Header, Footer) read images via getImageSrc / resolveImageRef helpers. npm run check:renderers green. npm run build green.
  • (d) Transformer at apps/cms/scripts/transform-seed-images.mjs. Idempotent — re-runs hit alreadyExisted: true and skip already- migrated fields. 16 uploaded + 24 idempotent re-uploads first pass; 0 + 40 second pass.
  • (e) D1 backfill at apps/cms/scripts/backfill-d1-image-refs.mjs imports the same migrateBlocks helper from the transformer. 33 fields rewritten across 11 rows; verification via post-write D1 read confirmed object shape.
  • (f) Smoketest verified: /, /smoketest, /contact, /adapter-test all return HTTP 200 with zero broken /assets/images/ refs in the rendered HTML. /adapter-test carries 3 deliberate-fixture loud-fail placeholders (Dave's missing avatar, missing logo, bogus social platform) — expected, not regressions.
  • (g) ADR-0005 §6 amendment landed (block-level scalar fields table, editor schema decoupling, adapter helper contract, videoUrl + slideshowUrls treatment notes).

New helper

packages/components-v3/src/lib/get-image-src.ts. Two functions: getImageSrc(field) returns the resolved public URL (handling both scalar string and object shapes); getImageAlt(field, fallback) for the alt text. Both funnel through resolveImageRef so r2: refs resolve via PUBLIC_MEDIA_BASE at render time.

Workspace exports widened

packages/components-v3/package.json exports map: replaced literal "./lib/utils": "./src/lib/utils.ts" with wildcard "./lib/*": "./src/lib/*.ts". Required so astro check (TSC) can resolve @fcr/components-v3/lib/get-image-src and @fcr/components-v3/lib/resolve-image-ref imports — Vite already handled them via the existing regex alias, but TSC consults the exports map first.

Decisions captured

decisions/0005-image-pipeline.md: - §6 amendment (2026-05-12, Slice 3) — block-level scalar fields table; editor schema decoupling; adapter contract; videoUrl / slideshowUrls treatment. - Slice tracker: Slice 3 marked Done with the artifacts list.

Known-patterns additions (one)

  • "Workspace-package exports map: literal-file vs wildcard trap" — the package.json-side counterpart to the 2026-05-10 entry "Workspace-package path alias: literal-vs-regex trap". Same detection rule, different file (package.json exports vs astro.config.mjs Vite alias / tsconfig.json paths). Both fail the same way: a literal pin works for one helper, breaks the second.

Known-issues changes

  • Deleted: "CMS image migration pipeline (broken images in production)" — Slice 3 closed it. The deployed CMS smoketest renders all images via R2.

Phase 3 regression check (declared)

Shared canonical surface is untouched: the only modifications under packages/components-v3/ are (1) a new helper file with no preexisting consumers, (2) widening exports's ./lib/* mapping (no consumer of the prior narrow path). Canonical block files (blocks/*.astro, components/ui/**) are untouched, so the assembler-fulldev path can't regress on this slice. Declared per the implementation-slice protocol.

Limit of this check (surfaced by user post-deploy). "No shared canonical surface touched" was true at the file level, but the deployed CMS's visual output didn't match the per-site assembled garvanbay build because 9 hand-rolled adapters at apps/cms/src/components/fcr-blocks/ weren't using the canonical blocks at all. The Phase 3 check should have included a deployed-output comparison between the two consumers of the canonical (assembled site vs CMS), not just a file-level "did I touch shared code" check. Followup slice (below) addressed the divergence.


Followup slice — ADR-0004 Path A enforcement (also done today)

Deployed Worker version after this slice: 0e823a0b-800c-4cf6-9ab4-a97e2098e2c3.

Done criteria status

  • (a) 9 hand-rolled adapters rewritten to delegate to canonical fulldev blocks: Hero → hero-4 / hero-3 (variant-picker); About → content-1; CtaStrip → cta-wcp; FAQ → faqs-1; BlogPosts → posts-wcp; Checklist → checklist; Header → header-wcp; Footer → footer-wcp; TopBar → topbar-wcp. Each adapter is now ~15–60 LOC, no inline <style>, just field translation. ✓
  • (b) npm run check:renderers green. ✓
  • (c) npm run build green. ✓
  • (d) Visual smoketest at deployed /, /contact, /smoketest, /adapter-test: zero fcr-{hero,about,cta-strip,faq,checklist, blog-posts,header,footer,topbar} legacy class hits across all four pages; 16–31 canonical block markers each (hero-3, content-1, cta-wcp, faqs-1, posts-wcp, header-wcp, footer-wcp, floating-cta-wcp, etc.). Image refs route through /_image?href=...&f=webp (Astro image pipeline at the canonical Image primitive). ✓
  • (e) Loud-fail discipline preserved: /adapter-test carries 3 deliberate-fixture placeholders (Dave's missing avatar, missing logo, bogus social platform) — expected, not regressions. ✓
  • (f) ADR-0004 "Path A enforcement" follow-up section landed. ✓

Variant-picker logic (Hero)

hasMedia = slides.length > 0 || imageUrl || videoUrl. If true → hero-4 (full-bleed media + white overlay text). Otherwise → hero-3 (brand-coloured text-only banner). Mirrors ADR-0003's variant table.

-wcp variants (not -1) — matches what the assembler-fulldev emits per lib/assembler-fulldev/translate.js. Verified before locking adapter targets.

Known follow-up gaps (not regressions)

  • About.imagePosition: 'left' renders with canonical's default direction. Adding direction support is a canonical-side prop change.
  • FAQ.showSearch: false ignored — canonical always renders the input.
  • Header.items is single-level (CMS schema constraint); canonical supports submenus but they're inaccessible from CMS.

D1 ↔ seed.json drift — fixed via reseed script

After the followup slice landed, the deployed CMS still rendered an empty hero (text-only hero-3) because the D1 home row had been seeded from a pre-Slice-3 snapshot with empty heading, subheading, body, and imageUrl fields. The Hero adapter was correctly routing the empty data → hero-3, but the user-visible result didn't match the expected garvanbay content.

emdash seed (npm run seed) targets the local data.db, not the remote D1 bound to the deployed Worker. Wrote apps/cms/scripts/reseed-d1-from-json.mjs — a one-shot that reads the current seed.json, generates DELETE-then-INSERT SQL per page (by slug, to clear pre-existing rows with mismatched ids on the unique (slug, locale) constraint), and applies via wrangler d1 execute --remote --file. Globals upsert by id.

Post-reseed verification on /: hero-4 (with image), cta-wcp, 5× content-1 each carrying the right heading + body + side image (PAYROLL / MANAGEMENT ACCOUNTING / COMPANY SECRETARY / Accounts Preparation / Tax and Advisory), faqs-1, posts-wcp with 3 blog cards. Site-globals: header-wcp logo + nav, topbar, footer-wcp, sticky-mobile-cta, floating-cta, floating-social.

Operational note: any future seed.json edits need to be applied to remote D1 via node apps/cms/scripts/reseed-d1-from-json.mjs, not npm run seed. The local data.db is a dev-time mirror; the deployed worker reads from remote D1.

Files touched (followup)

9 adapters under apps/cms/src/components/fcr-blocks/: Hero.astro, About.astro, CtaStrip.astro, FAQ.astro, BlogPosts.astro, Checklist.astro, Header.astro, Footer.astro, TopBar.astro. Plus docs/pipeline/decisions/0004-adapter-pattern-and-loud-fail.md "Followups" section.


Followup slice — About paragraph splitter (commit 608f385)

The user spotted that the deployed CMS's About sections rendered as single run-on paragraphs (~300 chars each, no breaks), while the pre-emdash baseline at master.garvanbay-fulldev.pages.dev split each service body into 2–4 paragraphs.

Root cause: the matcher emits About body as a single sentence-run with no \n\n boundaries. The assembler's lib/assembler-fulldev/translate.js#paragraphsFromText splits on either \n\n or a sentence boundary ((?<=[.!?])\s+(?=[A-Z])) — that's what gives the baseline its multi-paragraph output. The CMS About adapter only split on \n\n, so the same input collapsed.

Fix: New canonical helper packages/components-v3/src/lib/paragraphs-from-text.ts carrying the same regex; About adapter imports it. The assembler's local copy remains (CJS context — doesn't import from the package). Two consumers, same shape; drift between them is the failure mode this slice closes.

Post-fix: 14 substantive About paragraphs on the deployed CMS, matching the baseline exactly.


Followup slice — CMS theme tokens (commit 2f22d38)

The user pushed for visual fidelity vs the baseline. After Path A enforcement the canonical fulldev blocks were rendering, but the CMS was relying on the canonical's hardcoded fallback colours (cyan #2aa1db, yellow-green #C9D900) baked into each block's CSS. By coincidence those are garvanbay's actual brand colours, so the home page looked approximately right — but fonts were Inter (canonical default) not Montserrat (garvanbay's actual), and service-accent tokens, welcome-heading-color, etc. were unset.

Slice shape: - lib/theme-extractor.js patched to honour BUILDS_DIR env var. Ran it against live garvanbay.ie → captured builds/garvanbay.ie/theme.json (Montserrat fonts, primary #2aa1db, accent-secondary #cad401, service accentPosition: 'above', textAlign: 'center'). - New apps/cms/scripts/emit-theme-css.mjs (prebuild step) imports the assembler's emitGlobalCss(theme) via CJS interop, writes the emitted CSS to apps/cms/src/styles/global.css. Same emit function, two consumers now. - Base.astro imports global.css (generated) + theme.css (operator-override layer, still empty by default). The legacy --fcr-primary/--fcr-accent inline-style block dropped; body reads canonical --foreground/--background/--font-sans tokens. - TeamGrid.astro switched from var(--fcr-primary, ...) to var(--primary, ...). - tw-animate-css added as a CMS dep (used by emitGlobalCss). - .gitignore got a !builds/*/theme.json exception so per-site theme files travel with the repo (avoids re-running playwright on every build).

Verified on deploy: the Base.css bundle now contains --primary: oklch(67.2% .131 236) (extracted #2aa1db), --accent-secondary: oklch(83.4% .186 113) (extracted #cad401), --font-sans: "Montserrat", ..., plus all the service-* tokens. Token usage counts in the bundle: --primary 18×, --accent 14×, --foreground 10×, --background 4×.

Multi-tenant deferral. Today's CMS deploy serves one domain. For multi-tenant, theme moves from a build-time global.css to a runtime D1 lookup or per-host static asset. The helper contract (theme.json → CSS string) stays stable; only the storage flips. Captured in ADR-0004 "Shared theme emission" followup section.


Visual comparison vs live garvanbay (post-all-slices)

After all four slices landed, side-by-side metric comparison deployed CMS / vs live www.garvanbay.ie:

LIVE CMS (themed)
HTML size 1,380 KB 66 KB (Wix runtime bloat vs SSR)
<img> (deduped) ~14 unique 10 unique
Substantive About paragraphs (multi-<p>) 14 — matches baseline
--primary (Wix-obfuscated) oklch(67.2% .131 236) = #2aa1db ✓
--accent-secondary (n/a) oklch(83.4% .186 113) = #cad401 ✓
--font-sans Montserrat (Wix-private) "Montserrat"

Heading-text comparison: every visible h1/h2/h3 on live has a counterpart on the CMS (heading levels differ in spots — cosmetic).

Remaining visual deltas (4 missing images):

  1. Footer partner logo (cd883381 / website-design-by-FCR-logo.png) — translateFooter in lib/assembler-fulldev/translate.js:409 emits props.credit = { src, alt }, but the CMS transformer fromFooter at lib/cms/transformer.js:264 reads props.partnerLogos (which doesn't exist in the translator output). Real bug, clean fix.
  2. Welcome-section feature images (07f676 + 5abdd442 + d9ebbc23) — Wix section comp-l60h58h6 (the welcome block carrying the "GARVANBAY ACCOUNTING" h1 + intro paragraphs + 3 secondary thumbnails) isn't represented as a distinct block in seed.json. Likely the matcher captured it as part of the hero without preserving the secondary visuals, OR the matcher-emitted images[] array wasn't picked through the transformer's About→content+gallery split path. Needs a fresh garvanbay crawl + matcher/translator inspection to debug properly — no captured garvanbay.body.html is locally available.

Open as image deficit in known-issues — pending scope decision (Option A quick-fix vs Option B fresh crawl, see "Open items" below).


Open items for tomorrow

1. Image deficit — Option A vs Option B (decision pending)

Session ended mid-scoping. The footer-credit transformer bug is a known easy fix; the 3 welcome-section images are a harder matcher/transformer trace. Two scoped options on the table:

  • Option A (quick win, ~0.5 day). Fix fromFooter to map props.creditpartnerLogos[]. Hand-patch seed.json with the 4 known-missing image refs (footer partner logo + welcome thumbnails as a best-guess fcr.gallery block). Run transform-seed-images.mjs + reseed-d1-from-json.mjs. Lands the footer cleanly; welcome thumbnails placed by best guess.
  • Option B (proper, 1–2 days). Fresh garvanbay crawl via lib/dom-pipeline.jslib/cms/seed-from-build.js to regenerate seed.json from scratch. Likely surfaces multiple matcher/translator gaps including the footer credit. Fix as they surface. Heavier but covers the unknowns.

User has not picked yet. Tracked as a known-issues entry with deferral contract; see known-issues.md "Image deficit — 4 sources not in seed.json".

2. Drive toward 50-site end-to-end gate

Slice 3 closes the per-site rendering gap for one site (garvanbay, the existing seeded data). The pipeline now needs to be exercised across the next batch of sites:

  • The CMS app needs a [slug].astro dynamic route to render any page slug from D1 (currently only / renders the home page; /about, /payroll, etc. 404). Route adds + adapter integration is small; gating now.
  • A second site's seed.json needs to land. The next candidate from the wfpainters pre-staged data, or a fresh garvanbay-style crawl on a different domain.
  • verify-design coverage across the new batch.

This is the precondition for revisiting throughput architecture (per ADR-0005 §"Slice 4 throughput sub-issue" reopen contract).

3. seed-adapter-test.mjs object-shape exercise (polish)

The fixture seeds adapter-test rows with all-scalar imageUrl strings (most are external URLs, some are r2: legacy strings). The new object shape isn't exercised by adapter-test; updating one fixture per adapter to use { src, originalName, alt? } would extend coverage. Not blocking — getImageSrc already supports both shapes. ~30 min when picked up.

4. Carried over from prior sessions

  • Confirm copied-vs-shared packages/components-v3/ upstream relationship.
  • Re-think matcher-layer silent failure (apply loud-fail audit shape upward).
  • infra/llm-gateway/ workstream decision — sibling project that gets its own commit, or noise to clean up?
  • Custom domain for R2 assets (r2.dev is fine for migration; map pre-launch).

What's NOT yet done

  • Per-edit upload UX through emdash admin — operators still edit D1 via SQL or out-of-band; the structured imageUrl shape isn't exposed in the editor UI (emdash schema scalar-only constraint per §6 amendment). Separate workstream.
  • Slug-based dynamic routing in CMS app[slug].astro not added; 4 of 11 garvanbay pages aren't reachable via URL yet. Small slice when picked up.
  • Throughput optimisation — held per ADR-0005 throughput sub-issue reopen contract.
  • /contact static page — still uses pre-canonical hand-rolled fcr-blocks layout (carried).
  • .gitignore update for .tmp/ paths — still showing in git status (carried; small).

Cross-references

Today's commits (master)

  • 1da50b6 — Slice 3 of ADR-0005 (transformer + D1 backfill)
  • 58a5b87 — ADR-0004 Path A enforcement (9 adapters rewritten)
  • 7a5ffa3 — Reseed D1 from current seed.json script
  • 608f385 — About paragraphs — shared paragraphsFromText helper
  • 2f22d38 — CMS theme tokens — shared emitGlobalCss

Documents

  • decisions/0004-adapter-pattern-and-loud-fail.md — two new "Followups" sections (Path A enforcement; shared theme emission)
  • decisions/0005-image-pipeline.md §6 amendment + Slice tracker Slice-3 Done row
  • known-patterns.md — new entries: "Workspace-package exports map: literal-file vs wildcard trap"; (today) "emdash seed targets local data.db, not remote D1"; "Phase 3 regression check needs deployed-output comparison, not just shared-surface file audit"
  • known-issues.md — "CMS image migration pipeline" entry deleted (resolved by Slice 3); (today) "Image deficit — 4 sources not in seed.json" added with deferral contract

Code touchpoints

  • Adapters (10 image-touching, 9 rewritten Path A): all under apps/cms/src/components/fcr-blocks/
  • Scripts: apps/cms/scripts/{transform-seed-images,backfill-d1-image-refs,reseed-d1-from-json,emit-theme-css}.mjs
  • Canonical helpers: packages/components-v3/src/lib/{get-image-src,paragraphs-from-text,resolve-image-ref}.ts
  • Theme inputs: builds/garvanbay.ie/theme.json (tracked via !builds/*/theme.json gitignore exception)
  • Generated: apps/cms/src/styles/global.css (prebuild output)
  • Package exports widened: packages/components-v3/package.json ./lib/* wildcard
  • theme-extractor BUILDS_DIR env-var support: lib/theme-extractor.js

Deployed Worker versions (chronological)

  • 8c83c3a6 — post-Slice-3
  • 0e823a0b — post-Path-A
  • 7bedb93c — post-About-paragraph-fix
  • 7b0bf8e8 — post-theme-tokens (current)