Scope definition — one site, materially operational (garvanbay)¶
Status: v2 — revised 2026-07-04 after a two-lane adversarial pressure-test (deep-reasoner + Codex, independent, convergent). v1 was the pre-review draft. Date: 2026-07-04 Author: Cathal Dempsey (with Claude) Target site: garvanbay.ie (the single seeded site)
PROGRESS — 2026-07-04 (Phase 0 spike) - Gate 1 (auth) ✅ PASSED — non-dev login to the deployed admin via Cloudflare Access. See
spike-0-auth-cloudflare-access.md. - Gate 2 (text edit → publish → live, no corruption) ✅ PASSED — heading edit published live, images survived. Spike found a real defect: the emdash editor injects a generic Portable Textblocknode that the dispatcher didn't render (loud-fail banner). Fixed by addingProse.astro+ a"block"renderer (commit25112bc). Also surfaced: publish needs Admin role — setdefaultRole: 50+syncRoles(Editor/30 can draft but not publish). - Gate 3 (image round-trip via admin) ⏳ NEXT — the scalar-vs-object image-field test (ADR-0005 §6 / R-1) is not yet run.Net: the risky write-path loop is proven end-to-end for text. Remaining Phase-0 unknown is image editing (Gate 3).
0. Why this document exists¶
Four slices shipped on 2026-05-12 (image migration, Path A adapter enforcement, D1 reseed, theme tokens). The deployed CMS renders the garvanbay home page. But where we got to was not materially operational — it is a single-page, single-site render demo with no working content-editing loop, and (as the review surfaced) no working login to the admin at all.
This document defines what "materially operational" means for one site, and — post-review — reorders the work so the risky write-path loop is spiked before any read-path polish.
What the review changed (v1 → v2)¶
The v1 draft correctly identified the write-path as the crux but
mis-allocated the risk. Both reviewers, reading the actual code and the
emdash package, converged on:
- emdash is a capable CMS — native draft→publish, revisions,
_revoptimistic concurrency, a media picker, pluggable auth. The worry "can emdash even edit these blocks" was misplaced. - The real blockers are FCR-specific: (1) nobody can log into the deployed admin (auth is unconfigured and the dev bypass is DEV-only), and (2) image fields are stored as objects but declared as scalar text inputs, so saving any image-bearing block can corrupt the image — threatening text editing, not just image editing.
- "Publish" is not undefined — emdash defines it (draft → publish → live D1). The scope over-rated that as an unknown.
- Spike the loop first. Routing and the image-deficit are read-path polish worth zero if the loop can't run.
1. The bar — definition of "materially operational"¶
A person who is not Cathal can operate the site end to end, with no SQL, no scripts, and no out-of-band steps. That person can:
- Log in to the admin with their own identity.
- View every page, rendered correctly, themed, all images present.
- Edit any page's text, headings, and images through the emdash admin UI — without corrupting the block.
- Publish those edits so they appear live.
- Submit the contact form and have the enquiry actually delivered.
- Do all of the above without a developer in the loop.
If any of those requires Cathal to run a script or touch D1/SQL, the site is not materially operational.
2. Current state (grounded in the code, 2026-07-04)¶
What works¶
- Deployed Worker
replatform-emdash-demo.cathaldempsey.workers.dev(version7b0bf8e8). Home/renders correctly — canonical fulldev blocks via Path A adapters, garvanbay theme, R2 images. - Render architecture is sound and low-risk:
FcrBlocks.astrodispatcher → thin adapters (apps/cms/src/components/fcr-blocks/) → canonical blocks, with loud-fail at three layers and acheck-renderers.mjsprebuild guard. Both reviewers flagged this as the solid part. - D1 (
replatform-emdash-demo) holds ~10 garvanbay pages + globals. - emdash provides a native draft→publish lifecycle, revisions, and a media picker (details in WS-3/WS-4).
What does NOT work (the operational gaps)¶
-
No admin login on the deployed Worker — the whole loop's step 0.
astro.config.mjspasses noauthconfig → emdash defaults to passkey (node_modules/emdash/src/auth/mode.ts). The dev-bypass door returns 403 when not DEV (.../routes/api/auth/dev-bypass.ts) — which is why local editing appears to work and the deployed admin has never been exercised. Self-signup is doubly gated (needs an email provider — returns 503, none configured — and an enabledallowed_domainsrow — none). No script provisions an operator on remote D1. Result:/_emdash/adminshows a login nobody, including Cathal, can pass. -
Image fields corrupt on save (data↔schema mismatch). Images are stored as objects
{src, originalName, alt}(ADR-0005 §6;apps/cms/seed/seed.json:69-71) but declared in the FCR schema as scalartext_input(apps/cms/src/plugins/marketing-blocks/index.ts—imageUrlat :79,125,155,178,232,373;logoUrlat :392). The editor binds a text widget to an object value; saving the block writes the field back as a string, flattening the object. Because text and image share a block, editing a heading can blank the image. emdash's nativemedia_pickerexists but also writes a URL string, not the object (@emdash-cms/admin/dist/index.js:3515-3536) — so "just use media_picker" does not resolve the mismatch. -
Routing — 8 of ~10 pages 404, and existing routes bypass Path A. No
[slug].astro. Pages are hand-authored files with hardcoded entry keys:index→home(Path A),contact→contact(oldMarketingBlocks, not FcrBlocks —contact.astro:5,63, plus a hardcoded demo form),pricing→pricing(dead template — nopricingentry, renders Acme/demo content). These slugs have D1 data but no route (404):about,payroll,management-accounting,company-secretarial,tax-and-advisory,accountspreparation,accountants-waterford,book-an-appointment. -
Contact form drops enquiries. The
/contactPOST handler just logs with aTODOfor real email/webhook (contact.astro:16-20,39-48). No delivery, no storage, no spam protection. -
Publish path exists but is unproven on deploy. emdash Save writes a draft revision (
pagessupports drafts/revisions,seed.json:17-20); the live loader serves published-only (emdash/src/loader.ts:491); Publish promotes the draft to live D1 (emdash/src/database/repositories/content.ts:916-1000). No redeploy, no reseed — but this has not been demonstrated on the deployed Worker for the FCR block shapes. -
Image deficit — 4 images missing vs live (footer partner logo = a one-line
fromFooterfield-mapping bug; 3 welcome-section images). Tracked in known-issues. -
Silent "needs a developer" leaks. Theme changes require a redeploy (
global.cssis build-time fromtheme.json). Globals (header/footer/topbar) declare no drafts/revisions in seed, so their edit/publish semantics differ from pages — unverified. -
Per-edit uploads would fork the ADR-0005 image pipeline. emdash's generic upload stores ULID keys with no original-name metadata (
emdash/src/astro/routes/api/media.ts:137-147;@emdash-cms/cloudflare/src/storage/r2.ts:41-61) — not the ADR-0005<domain>/<stem>-<8hex>+ provenance contract.
3. Target operational loop¶
LOGIN VIEW EDIT PUBLISH LIVE
───── ──── ──── ─────── ────
Operator → Any page → Edit text / heading → Click → Change
logs into renders, / image in publish live on
/_emdash/ all pages, /_emdash/admin (draft → deployed
admin all images (no corruption) live D1) Worker
+ contact
form
delivers
No terminal. No SQL. No reseed script. No developer.
4. Workstreams¶
Phase 0 (SPIKE) must pass before Phase 1 build starts. Phase 0 proves the write-path loop end-to-end on the deployed Worker; Phase 1 is the (lower-risk) build-out the spike de-risks.
PHASE 0 — Prove the loop (spike, ~1–2 days, 3 gates)¶
WS-0 — Auth: an operator can log in¶
- Decided: Cloudflare Access. Code side is wired (
astro.config.mjsemdash({ auth: access(...) }), inert untilCF_ACCESS_TEAM_DOMAINis set). Remaining work is Cloudflare dashboard config — full click-through runbook atspike-0-auth-cloudflare-access.md. - Key finding (de-risks the "no provisioning script" gap): emdash auto-provisions the first Access login as Admin (role 50); later users get their mapped/default role. No SQL user creation needed.
- Prerequisite: the Worker needs a custom domain — Access can't
front a
*.workers.devURL. (Also the R2 custom-domain we deferred in ADR-0005 — worth doing together.) - Access is scoped to the
/_emdashpath; the public site stays open (emdash never gates public routes). Access mode generalises to N sites. - Gate 1 — done when: a non-Cathal identity logs into the deployed
/_emdash/adminvia Access and reaches the editor, with the public site unaffected.
WS-3 — Text edit → publish, proven on deploy¶
- In the deployed admin, edit a hero heading on an image-bearing
block. Save. Confirm the change is not yet live (draft). Publish.
Confirm
/updates from remote D1. Inspect the D1 row JSON. - Gate 2 — done when: the heading edit publishes live and the block's image is not blanked.
WS-4(spike) — One image field round-trips without corruption¶
- Wire one image field to a fix (see §5 decision b) and prove an
operator can set/upload an image through the admin. Record the exact
value written to
content[].imageUrl. - Gate 3 — done when: an image set via the admin persists in a usable shape and renders live, with provenance preserved (or a consciously-accepted divergence documented).
If gate 2 or 3 fails, the image-shape rework is the real milestone — not routing.
PHASE 1 — Build out (only after Phase 0 passes)¶
WS-1 — Dynamic routing ([slug].astro)¶
- One dynamic route rendering every D1 slug through
FcrBlocks, resolving entries without hardcoding per-site keys. Remove the deadpricing.astro. - Done when: all ~10 garvanbay pages return 200 and render via Path A.
WS-2 — Content completeness (image deficit)¶
- Fix the
fromFootercredit→partnerLogosbug. Resolve the 3 welcome-section images (Option A hand-patch vs Option B fresh crawl — still open, §5). - Sequencing note: do this after WS-4's image shape is fixed — otherwise deficit images get patched into the interim object shape and migrated twice.
- Done when: deployed pages carry every live image (or a documented exception list).
WS-4(full) — Image editing + upload UX¶
- Generalise the WS-4 spike to every image field. If uploads go through emdash, reconcile with the ADR-0005 key/provenance contract (custom upload adapter/hook) or amend ADR-0005.
- Done when: an operator can change/upload any image and see it live, provenance handled.
WS-5 — Contact form: real submission (IN SCOPE — decided)¶
- Move
/contactonto theFcrBlocksdispatcher (retire the hand-rolled layout + demo form). Wire a real submission path: destination (form-to-email provider, D1 store, or third-party endpoint) + spam protection + delivery confirmation. - Done when: a submitted enquiry is actually delivered, and
/contactrenders/edits through the same Path A contract as every other page.
WS-6 — Publish semantics + operator runbook¶
- Confirm and document: Save = draft, Publish = live D1 write, no redeploy/reseed. Cover both pages and globals (globals lack draft/revision support — verify they're editable-and-publishable).
- Retire
reseed-d1-from-json.mjsfrom the operator path (dev/bootstrap only). Add a short operator runbook (log in, edit, preview, publish, recover). - Done when: publish is a button, and a non-dev can follow the runbook.
WS-7 — Acceptance verification¶
- A non-Cathal person runs the full loop touching nothing but a browser: log in → view all pages → edit text → edit an image → preview → publish → confirm live → submit the contact form.
- Done when: that run succeeds with zero terminal/SQL steps.
5. Open decisions¶
- Image deficit — Option A (hand-patch, ~0.5d) vs Option B (fresh garvanbay crawl, 1–2d). Still open. Blocks WS-2. Do it after the image shape is fixed (WS-4) to avoid double migration.
- ~~Contact form~~ DECIDED: in scope — wire real submission (WS-5).
- Image-field fix — DECIDED (target + fallback):
- Target — Option 1: custom emdash field widget that reads/writes
the
{src, originalName, alt}object, preserving ADR-0005 provenance-in-data. More build (widget + ADR-0005-compliant upload). - Fallback — split-scalar variant: if the widget proves heavy,
keep native scalar fields but split provenance into its own scalar
field (
imageUrl+imageOriginalName) — native-emdash-compatible and keeps provenance in the data. - Rejected — plain scalar (drop provenance): contradicts ADR-0005 §6 and the standing provenance-in-data preference.
- The WS-4 spike (gate 3) determines widget cost → picks target vs fallback.
- Copied-vs-shared
packages/components-v3/vs fulldev upstream — still unverified (known-issues). Matters if canonical edits are needed.
6. Non-goals for this milestone (one site, not scale)¶
- 50-site / 1800-site onboarding.
- R2 upload throughput architecture (deferred per ADR-0005).
- Multi-tenant runtime theme — build-time
global.cssstays. (Explicit limitation: brand-colour/font changes need a developer + redeploy. Acceptable; stated so the bar is honest.) - Per-host routing / many sites on one Worker.
- Booking, matcher-layer loud-fail, R2 GC, custom CDN domain, verify-design automation.
Bank-now caveat (don't let one-site choices cement single-tenant):
pick an auth mode (WS-0) and slug resolver (WS-1) that generalise to N
sites; carry domain/tenant context in any new editor/image field
design.
7. Definition of Done (acceptance test)¶
On the deployed Worker, by someone using only a browser:
- A fresh non-Cathal identity can log in to
/_emdash/admin. - Every garvanbay page slug renders at its URL (200, Path A, themed).
- No image is missing vs live (or exceptions documented).
- A text/heading edit publishes live — and does not blank the block's image.
- An image can be changed/uploaded through the admin and appears live, provenance preserved.
- A draft can be previewed before publish (emdash
preview-url.ts). - The contact form delivers a real enquiry.
- The whole loop ran with zero terminal/SQL/reseed steps (reseed permitted only to reset the environment before the test).
- An operator runbook exists (login, edit, preview, publish, recover; covers pages and globals).
8. Risks — re-rated after the review¶
| ID | Risk | Rating (v2) | Evidence |
|---|---|---|---|
| R-0 | Deployed admin login is impossible (auth unconfigured, dev-bypass DEV-only, no provisioning) | SEV-1 — new, highest | astro.config.mjs (no auth); auth/mode.ts; dev-bypass.ts (403 !DEV) |
| R-1 | Image-bearing blocks corrupt on save (object data vs scalar schema; media_picker writes strings) | SEV-1 | seed.json:69-71 vs marketing-blocks/index.ts:79…392; admin/dist/index.js:3515-3536 |
| R-2 | Scalar-only schema blocks image editing | Down-rated — solvable via custom widget/split-scalar; not an upstream-emdash blocker (but media_picker alone is insufficient) | plugin fieldWidgets extension point; media_picker string output |
| R-3 | "Publish" undefined | Down-rated as a code unknown — emdash defines it; still needs deployed verification | loader.ts:491; content.ts:916-1000; [id]/publish.ts |
| R-4 | Contact form / /contact bypasses Path A |
Up-rated to in-scope work (WS-5) | contact.astro:5,16-20,63 |
| R-5 | 10 pages vs dynamic route | Low — WS-1 is small | no [slug].astro; hardcoded keys |
| R-6 | Per-edit upload forks ADR-0005 key/provenance | SEV-2 | media.ts:137-147; r2.ts:41-61 |
| R-7 | Single-tenant leaks (theme, PUBLIC_MEDIA_BASE) |
SEV-3 — fine for one site, bank generalisable choices | wrangler.jsonc:7-9; astro.config.mjs:110-112 |
9. Review provenance¶
Pressure-tested 2026-07-04 by two independent lanes (deep-reasoner / Opus-max, and Codex) against the code and the emdash package. They converged on the verdict and the two SEV-1 blockers; Codex added the media_picker-writes-strings and ADR-0005-upload-fork nuances, deep-reasoner surfaced the auth lockout mechanics. No material contradiction, so no Fable adjudication was needed. A first Fable run hung and was abandoned; the lane was later confirmed healthy.
Verdict carried into v2: not buildable as v1 was ordered. Phase 0 spike (WS-0/WS-3/WS-4) gates the milestone; if gates 2–3 fail, the image-shape rework is the milestone.