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.
- Slice 3 of ADR-0005 — CMS image migration (commit
1da50b6). Closed the broken-images known-issue. - 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. - Reseed remote D1 + About paragraph fix (commits
7a5ffa3,608f385). D1 had stale pre-Slice-3 data; newreseed-d1-from-json.mjsbrings it in sync with the current seed.json (emdash seedonly writes localdata.db). About bodies were collapsing to single paragraphs — the CMS adapter was splitting on\n\nonly; the assembler'sparagraphsFromTextalso splits on sentence boundaries. Shared helper atpackages/components-v3/src/lib/paragraphs-from-text.tsso both consumers stay aligned. - CMS theme tokens (commit
2f22d38). Generatedbuilds/garvanbay.ie/theme.jsonvialib/theme-extractor.js; newapps/cms/scripts/emit-theme-css.mjs(prebuild step) writesapps/cms/src/styles/global.cssfrom the assembler's sharedemitGlobalCss(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, latest2f22d38. Working tree clean. - Worker deployed:
https://replatform-emdash-demo.cathaldempsey.workers.dev— latest version7b0bf8e8-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 undergarvanbay/(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
imageUrland current seed.json content (post-reseed). - theme.json: garvanbay's extracted theme tracked at
builds/garvanbay.ie/theme.jsonvia a new!builds/*/theme.jsongitignore 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 fromstatic.wixstatic.com/media/(transformer downloads on demand instead of runningdom-pipeline.jsas a separate step — false-stop simplification resolved during execution). - (b) Block-level scalar
imageUrl/logoUrldata 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 viagetImageSrc/resolveImageRefhelpers.npm run check:renderersgreen.npm run buildgreen. - (d) Transformer at
apps/cms/scripts/transform-seed-images.mjs. Idempotent — re-runs hitalreadyExisted: trueand 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.mjsimports the samemigrateBlockshelper from the transformer. 33 fields rewritten across 11 rows; verification via post-write D1 read confirmed object shape. - (f) Smoketest verified:
/,/smoketest,/contact,/adapter-testall return HTTP 200 with zero broken/assets/images/refs in the rendered HTML./adapter-testcarries 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
exportsvsastro.config.mjsVite alias /tsconfig.jsonpaths). 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:renderersgreen. ✓ - (c)
npm run buildgreen. ✓ - (d) Visual smoketest at deployed
/,/contact,/smoketest,/adapter-test: zerofcr-{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-testcarries 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.
Header / Footer / TopBar variant choice¶
-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: falseignored — canonical always renders the input.Header.itemsis 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):
- Footer partner logo (
cd883381 / website-design-by-FCR-logo.png) —translateFooterinlib/assembler-fulldev/translate.js:409emitsprops.credit = { src, alt }, but the CMS transformerfromFooteratlib/cms/transformer.js:264readsprops.partnerLogos(which doesn't exist in the translator output). Real bug, clean fix. - Welcome-section feature images (
07f676+5abdd442+d9ebbc23) — Wix sectioncomp-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-emittedimages[]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 capturedgarvanbay.body.htmlis 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
fromFooterto mapprops.credit→partnerLogos[]. Hand-patch seed.json with the 4 known-missing image refs (footer partner logo + welcome thumbnails as a best-guessfcr.galleryblock). Runtransform-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.js→lib/cms/seed-from-build.jsto 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].astrodynamic 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.devis 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
imageUrlshape 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].astronot 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.
/contactstatic page — still uses pre-canonical hand-rolled fcr-blocks layout (carried)..gitignoreupdate for.tmp/paths — still showing ingit 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 script608f385— About paragraphs — shared paragraphsFromText helper2f22d38— 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 rowknown-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.jsongitignore exception) - Generated:
apps/cms/src/styles/global.css(prebuild output) - Package exports widened:
packages/components-v3/package.json./lib/*wildcard - theme-extractor
BUILDS_DIRenv-var support:lib/theme-extractor.js
Deployed Worker versions (chronological)¶
8c83c3a6— post-Slice-30e823a0b— post-Path-A7bedb93c— post-About-paragraph-fix7b0bf8e8— post-theme-tokens (current)