Skip to content

Session handover — 2026-05-10

End-of-day pickup note. Today shipped the CMS multi-tenant block adapter pattern (10 new adapters, all visually verified on the deployed worker), then surfaced and fixed an entire class of silent-failure bugs at the primitive layer via a deliberate audit pass. Two ADRs landed — one durable (0004 adapter-pattern-and-loud-fail), one DRAFT (0005 image-pipeline). Three new pattern entries, six new known-issues entries.

The session principle: silent failure is unacceptable at any layer. Carried it through the block dispatcher, the primitive layer, the build-time guard, and back into matcher-layer queue work. See ADR-0004.


Where we ended up

  • Branch: master — primitive fixes + adapter wiring + ADRs uncommitted in working tree, ready to commit (no batch landed yet).
  • Worker deployed: https://replatform-emdash-demo.cathaldempsey.workers.dev (personal Cloudflare account, signed in as cathaldempsey@gmail.com, not the FCR Media account).
  • Verification: /adapter-test page renders all 10 new adapters with realistic seed data, including intentional bad-data rows that prove every loud-fail placeholder fires.
  • R2: replatform-emdash-media bucket public access enabled at pub-4939c2fb5cd540d0862e9c02ac6644ba.r2.dev. One real image uploaded at test/garvanbay-hero.jpg to verify the production image pipeline works (/_image returns 200 webp; placehold.co failures earlier in the session were synthetic-test artefacts).

Today's work — what shipped

CMS adapter buildout (10 new block types)

apps/cms/src/components/fcr-blocks/: - Sections: TeamGrid, ServiceGrid, Reviews, Gallery, LogoStrip, Contact, Map — wired into FcrBlocks.astro MAP. - Site-globals: StickyMobileCta, FloatingCta, FloatingSocial — wired into Base.astro globals lookup.

Build-time guard scripts/check-renderers.mjs now passes 19/19 with no ALLOW_MISSING_RENDERERS=1 bypass.

Primitive audit + 5 silent-failure bugs fixed

Audit covered 22 primitive families (all UI primitives consumed by canonical blocks). Five silent-failure bugs identified and fixed in one pass with a consistent pattern (visible red dashed placeholder + console.warn): - Icon — also rewritten to explicit static map (see below) - Image, LogoImage, AvatarImage — silent on missing src - Rating — silent on 0 / NaN / non-numeric

Two canonical block fixes: - reviews-1.astro — avatar conditional on item?.image?.src to avoid placeholder noise when CMS schema legitimately has no headshot field (intentional absence vs missing input — see known-patterns.md). - team-grid.astro — removed {image?.src && ...} short-circuit so the Image primitive's loud-fail can fire on missing imageUrl.

Two adapter de-filterings: - LogoStrip.astro and Gallery.astro were silently dropping items with missing src via .filter(l => l?.src). Same class of silent-failure bug at the adapter layer — pattern entry added.

Icon primitive rewritten

packages/components-v3/src/components/ui/icon/icon.astroimport.meta.glob("node_modules/lucide-static/...") replaced with explicit static-map of imports. Glob silently resolved to an empty set when consumed via Vite alias from a workspace package (no node_modules adjacent), making every icon name fail to render. Hit in production with arrow-right failing across every ServiceGrid tile + StickyMobileCta phone/mail icons. Static map fails at build time when a name is misspelled — loud failure mode.

LinkedIn isn't in simple-icons (licensing); the static map omits it and the loud-fail placeholder fires. Cheap fix when needed (lucide-staticlucide or custom SVG); deferring.

Decisions captured

  • ADR-0004 — adapter pattern and loud-fail discipline. Captures Path A vs B vs C analysis (50-commits/week datapoint settled it), the adapter layer, loud-fail at every layer, the build-time guard, and the static-map Icon rewrite. See decisions/0004-adapter-pattern-and-loud-fail.md.
  • ADR-0005 — CMS image pipeline (DRAFT). Pre-decision scoping. Three options compared (single bucket path-keyed, per-site bucket, hot-link from wixstatic); recommends Option A at ~3 days; cost is rounding error ($0.18/mo storage + ~$45/mo /_image transforms at full traffic). Promote out of DRAFT when start time is picked. See decisions/0005-image-pipeline-DRAFT.md.

Known-patterns additions

Three new entries under "Components / pipeline": - "Primitive emits nothing for unrecognised input (silent-fail class)" - "Adapter silently filters incomplete items via .filter(x => x?.src)" - "Avatar-conditional vs loud-fail (intentional absence vs missing input)"

Known-issues additions

  • CMS image migration pipeline (cross-linked to ADR-0005-DRAFT)
  • Spike-vs-production naming unsettled
  • Copied-vs-shared fulldev primitives (not verified)
  • Schema ↔ component type validation absent
  • Unmatched-Wix-section behaviour at the matcher layer
  • LinkedIn icon falls to placeholder

What's actually working end-to-end now

  • /adapter-test on the deployed worker renders 10 new section blocks + 3 site-globals with realistic seed data plus intentional bad-data rows that prove every loud-fail placeholder fires (TeamGrid Dave with no imageUrl → red "no image" placeholder; LogoStrip Revenue Commissioners with no src → "logo? Revenue Commissioners"; Reviews row with rating "five" → "rating? (five)"; FloatingSocial linkedin → "linkedin" placeholder).
  • R2 → /_image → optimized webp confirmed working for real production-shaped image URLs. The placehold.co failures earlier in the session were a /_image 500 specifically on placehold.co (synthetic test data), not a real-pipeline issue.
  • Build-time renderer guard refuses to ship if any plugin-schema block type is missing a renderer. 19/19 pass; ALLOW_MISSING_RENDERERS=1 is loud about being a smell.
  • Loud-fail placeholders at three layers consistent: MissingBlock.astro (block dispatcher), ui-missing-{icon,image, logo,avatar,rating} (primitive layer), check-renderers.mjs (build).

Afternoon session — ADR-0005 promotion + Slice 1 + protocols

Followed pre-promotion review process: six implementation specifics locked into ADR-0005, then promoted Accepted. Slice 1 landed end-to-end on /adapter-test (Alice Doherty's avatar resolves from r2:test/garvanbay-hero.jpg via PUBLIC_MEDIA_BASE). Three commits landed in logical order to restore repo coherence.

Commits (this date): - 9c99bd2 — feat(cms): logical r2: image refs at render — accept ADR-0005, Slice 1 - f6b9798 — chore(components-v3): relocate src/{blocks,ui} → src/components/{blocks,ui} - 0a83fe1 — feat(cms): adapter pattern + loud-fail audit + 10 adapters + ADR-0004 (the foundation Slice 1 sits on; landed after 9c99bd2 to restore repo coherence) - (this commit) — chore(docs): session protocols + CLAUDE.md + alias-trap pattern + handover

Decisions: decisions/0005-image-pipeline.md Accepted with §1–§6 implementation specifics and Decision history (V1 raw basename → V2 pure SHA → V3 hybrid stem-plus-8hex). Spike-vs-production for apps/cms/ resolved as graduated, no rename — see updated apps/cms/README.md.

Pattern: known-patterns.md Components/pipeline § "Workspace-package path alias: literal-vs-regex trap" — surfaced when adding resolve-image-ref.ts alongside utils.ts.

Protocol: new pages under docs/pipeline/working-with-claude-code/session-start.md, implementation-slice.md, session-end.md. Root CLAUDE.md points at them. User-level /session-start and /session-end skills wired on Cathal's machine (not in repo).

Memory: two feedback memories saved (deferral triggers, provenance in data path) — both surfaced as user pushback during ADR-0005 review.


Evening session — Slice 2 of ADR-0005 + slice reorder

Slice 2 (R2 upload helper, wrangler-shellout baseline) landed end-to-end. Validation: 9/9 normaliser unit assertions, 158/158 wfpainters images uploaded first run, 158/158 idempotent on second run, 3/3 pub URLs reachable, /_image returns webp against an uploaded key.

Files added: - apps/cms/scripts/upload-image.mjsuploadImage(localPath, domain) → { logicalRef, key, alreadyExisted }. SHA-256 + 7-step transliteration normaliser per ADR-0005 §3. Idempotency check via wrangler r2 object get --pipe --remote exit code (the only viable wrangler-side existence path; no list/head verb exists). Source comment + pinned wrangler@4.90.0 in apps/cms/package.json guard against a future CLI change silently breaking idempotency. - apps/cms/scripts/upload-image.validate.mjs — re-runnable validator. §3 normaliser unit assertions + corpus round-trip + pub-URL spot check. - apps/cms/package.jsontransliteration@^2.6.1 added; wrangler pinned ^4.80.04.90.0.

Slice reorder driven by perf finding. Wrangler-shellout was ~8s/image (Windows + npx + wrangler startup). 158 wfpainters images took 1253s first run. Extrapolated to 290k portfolio images that's ~640 hours single-threaded — throughput-blocking, not "polish". Slice 4 (SDK auth swap, new addition) executes ahead of Slice 3 (transformer integration), because transformer integration emits the helper at every call site and would inherit a broken-perf helper. Slice numbers stay stable identifiers — the SDK swap is Slice 4 (a new slice that didn't exist in the original plan), not a renumbering of Slice 3:

Slice Scope Status
1 Logical-ref rendering done (commit 9c99bd2)
2 Upload helper (wrangler) done (this session)
3 Transformer integration follows Slice 4
4 SDK auth swap (new) next

The Slice 4 done criteria are tightened in decisions/0005-image-pipeline.md "Slice tracker" + repeated in known-issues.md "R2 upload — wrangler-shellout baseline": SDK swap, set x-amz-meta-original-name going forward, backfill wrangler-era objects via CopyObject, throughput target <30 min for 290k images parallelised.

ADR-0005 §6 metadata gap accepted in writing. Wrangler r2 object put exposes no metadata flags. The ADR design (BOTH sibling field AND R2 metadata) means no data is lost during the shellout window — Slice 3 writes the Portable Text sibling on every D1 row, which is the primary forensic path; the missing R2 header is a redundancy gap closed by the Slice 4 backfill, not a data gap.

Known-patterns additions (two): - known-patterns.md Components/pipeline § "Lock current behaviour as expected behaviour (don't sneak fixes into tests)" — the Test_20cm.jpg → test-cm.jpg rule. Imperfections documented in an ADR get asserted as expected behaviour; fixing them is an ADR amendment, not a sneaky test edit. - known-patterns.md Components/pipeline § "Lib doesn't quite match ADR — post-process to match the spec". Slugify-vs-§3 divergence fixed with a 1-line [^a-z0-9]+ → - post-process. Single divergences post-process; multiple divergences signal "wrong lib".

Protocol: working-with-claude-code/implementation-slice.md canonical example replaced with Slice 2 (richer real-stop / false-stop / regression-N/A walkthrough than Slice 1).


Open items for tomorrow

1. Slice 4 of ADR-0005 — SDK auth swap (executes next)

New slice; reordered ahead of Slice 3 (transformer integration) after Slice 2's perf finding. Done criteria locked in decisions/0005-image-pipeline.md "Slice tracker" + 4 explicit sub-criteria in known-issues.md "R2 upload — wrangler-shellout baseline":

  1. @aws-sdk/client-s3 against R2's S3-compatible endpoint with R2 API tokens.
  2. x-amz-meta-original-name set on every upload from the SDK forward.
  3. Backfill wrangler-era objects with CopyObject + MetadataDirective: REPLACE, recovering names from local source files (Slice 3 hasn't run yet so D1 doesn't yet hold the Portable Text siblings).
  4. Throughput target: <30 min wall for 290k images, parallelised 20–50 wide with 429-retry.

Helper interface (uploadImage(localPath, domain) → { logicalRef, key, alreadyExisted }) stays stable across the swap. Validator re-runs as the smoke test.

2. Slice 3 of ADR-0005 — transformer integration (follows Slice 4)

Walk seed.json imageUrl refs through the (SDK-backed) helper; rewrite to r2:<domain>/<key>; add originalName Portable Text sibling per ADR §6. Backfill 11 already-seeded garvanbay pages.

3. Confirm copied-vs-shared fulldev primitives (carried over)

packages/components-v3/ upstream-relationship still unverified. Slice 1 did partially answer this for per-site builds (WCP's builds/waterfordcountypainters.ie/package.json declares only astro, no workspace dep — they're snapshots). The canonical itself relative to fulldev/ui upstream is still unconfirmed.

4. Re-think matcher-layer silent failure (carried over)

Apply the loud-fail audit shape upward to lib/dom-pipeline.js matchers. When an unrecognised Wix section type appears, behaviour is silent drop. Same class of bug as the primitive layer pre-2026-05-09. Not blocking.

5. infra/llm-gateway/ workstream (carried over)

Untracked LiteLLM docker-compose + config sitting in infra/llm-gateway/. Separate workstream from CMS/replatform work. Decision needed: sibling project that gets its own commit, or noise to clean up?


What's NOT yet done (deferred this session)

  • /contact static page still uses pre-canonical hand-rolled fcr-blocks layout. Works, but bypasses the FcrBlocks dispatcher.
  • emdash admin upload UX. Operator hand-edits D1 via SQL. A real "upload an image through admin → R2 → DB" path is a separate workstream from the migration script.
  • ServiceGrid layout collapse at 1280px viewport. Tiles render with correct data; the canonical's SectionGrid container-query layout doesn't trigger the multi-column variant at this viewport. Cosmetic, lives in the canonical.
  • Custom domain for R2 assets. r2.dev is fine for migration; map a custom domain pre-launch. ~30 minutes DNS + R2 config. Update PUBLIC_MEDIA_BASE env when this happens.
  • .gitignore update for .tmp/ paths. apps/cms/scripts/.tmp/ and scripts/.tmp/ keep showing up in git status. Add **/.tmp/ next session.

Cross-references

  • decisions/0004-adapter-pattern-and-loud-fail.md — adapter pattern
  • loud-fail discipline (the architecture today's afternoon ADR-0005 builds on)
  • decisions/0005-image-pipeline.md — Accepted; six implementation specifics in §1–§6, Slice tracker (Slices 1+2 done, Slice 4 next ahead of Slice 3), Decision history at the bottom
  • known-patterns.md — three morning entries (silent-fail class, adapter filter pattern, avatar-conditional) + one afternoon entry (workspace path alias literal-vs-regex trap) + two evening entries (lock current behaviour as expected, lib post-process to match ADR)
  • known-issues.md — six entries (image migration cross-linked to 0005, R2 upload wrangler-shellout baseline with Slice 4 done criteria, fulldev primitive sourcing, schema validation, matcher-layer silent fail, LinkedIn placeholder); the spike-vs- production entry was deleted (resolved)
  • working-with-claude-code/{session-start,implementation-slice,session-end}.md — session protocols; implementation-slice.md canonical example updated to Slice 2
  • CLAUDE.md (root) — skinny pointer at the protocols + sources of truth