Scope — ADR-0009 Slice 4b (GenericSection) — split into 4b-i / 4b-ii¶
Date: 2026-07-08
ADR: 0009 §2, §3 — the measured
GenericSection fallback is the bounded "reconstruction" element for unmatched
sections.
Precedes build. Scope-before-build per the implementation-slice protocol.
The split (decided 2026-07-08, user-confirmed)¶
Slice 4b as written bundles a renderer and a metric with a hard
dependency: genericFallbackRate counts how often we fell back to
GenericSection, which is impossible to compute until GenericSection exists to be
counted. So 4b splits — same move as 4a/4b, inverted (renderer first, meter
second):
- 4b-i — the GenericSection renderer (both lanes). Independently shippable +
verifiable: deploy, force a section through it, watch it render; closes the CMS
silent-drop.
genericFallbackRatestaysnullafter this slice. - 4b-ii — the
genericFallbackRatemetric (sidecar + join + gate veto + validate). Depends on 4b-i having a matcher name to attribute.
Sequencing rationale mirrors 4a ("build the meter before the remediation") — here the remediation (renderer) must exist before its meter.
4b-i — GenericSection renderer (build first)¶
Deliverables¶
| # | Piece | File | Shape |
|---|---|---|---|
| 1 | Matcher | lib/matchers/GenericSection.js |
Auto-registers via structure-matcher.js#loadMatchers dir-scan (any non-_ .js exporting match+extract). match() returns a tiny score (~0.05) for any tag==='section' — strictly below About's 0.15 fallback floor (lib/matchers/About.js:42), so it wins only when nothing else scored > 0. extract() walks headings/paragraphs/images in DOM order into an ordered items[] ({kind:'text'|'image', text?/html?, src?, alt?}) + heading. |
| 2 | Canonical block | packages/components-v3/src/components/blocks/generic-section.astro |
Composes Section(tone,align) → SectionContent/SectionProse (<slot/> for ordered heading + prose HTML) and branches: side image → SectionSplit+SectionMedia; multi-image → SectionGrid columns; text-only → centred prose. A superset of content-1/content-2/features-3. Shared by both lanes automatically. |
| 3 | CMS schema | apps/cms/src/plugins/marketing-blocks/index.ts |
New block type:"fcr.generic-section", category:"Sections" (keep category within the 500-char parse window of type). Fields: heading + a repeater of ordered items (kind select text|image, text multiline, imageUrl media_picker, caption) + TONE_FIELD + ALIGN_FIELD + COLUMNS_FIELD. |
| 4 | CMS adapter | apps/cms/src/components/fcr-blocks/GenericSection.astro |
Thin, modeled on About.astro: normalize tone/align/columns, map images via getImageSrc, text via paragraphsFromText, emit ordered <h2>/<p> + images into the canonical block. |
| 5 | Dispatcher | apps/cms/src/components/FcrBlocks.astro |
Add the import + "fcr.generic-section": GenericSection to MAP (satisfies check-renderers.mjs). |
| 6 | CMS translator + close the silent-drop | lib/cms/transformer.js |
Add fromGenericSection to SECTION_TRANSLATORS. The bare silent continue at transformer.js:388 (if (!entry.matcher \|\| entry.droppedAsDuplicate) continue;) and :405 (no PT translator): add a loud ⚠ warn (ADR-0004) so anything the always-matches matcher still doesn't claim surfaces instead of vanishing. Add fcr.generic-section to ALIGN_ELIGIBLE/COLUMNS_ELIGIBLE. |
| 7 | Static translator | lib/assembler-fulldev/translate.js |
translateGenericSection keyed by matcher name in the module.exports map → {component:'generic-section', props, slot} (or an array to split grid blocks). |
Two flagged risks (regression-check scope)¶
-
Shared-registry change hits both lanes.
lib/matchers/is scanned by both the CMS lane (seed-from-build.js) and the static lane (assembler-fulldev.js). An always-matches GenericSection changes static-lane output too: sections that today get loud-warned-and-skipped (assembler-fulldev.js:215-219) will now render as generic blocks. That is the intent, but it is a shared-primitive change — regression check: re-run a built site (garvanbay/WCP) through both lanes and confirm nothing that should stay dropped now renders as an ugly generic block, and no previously-matched section got stolen (the< 0.15score guarantees this, but verify). -
Static-lane tone/align is house-default, by deferral. The per-site static assembler consumes no
section-backgrounds.json(known-issues — deferred "Option A"). So static GenericSection lays out text/images in order but gets no measured tone/align/columns — those stay CMS-lane-only until Option A is taken up. Not a bug; the honest boundary. The block still accepts the props, so the static lane wires up for free when Option A lands.
Verification (4b-i)¶
check-renderers.mjspasses (schema ↔ dispatcher ↔ file all present).- CMS build + deploy; force a
fcr.generic-sectionblock through the real path (surgical D1 insert on the garvanbay/WCP demo, then revert — per known-patterns "prove the render surface another way"), confirm it renders with ordered content, correct tone/align/columns via computed styles on the deployed page (not class presence). - Static lane: build a site with an unmatched section, confirm GenericSection emits and the block compiles/renders.
- Both-lane regression per risk 1 above.
4b-ii — genericFallbackRate metric (build after 4b-i)¶
Contract: near-match-scorecard-contract.md §1 (S sub-metric), §3 (definition
= "fraction rendered by the measured GenericSection; every use is matcher debt"),
§4 (veto: GenericSection on a role-weight ≥ 1.3 region → HOLD).
Deliverables¶
| # | Piece | Where |
|---|---|---|
| 1 | Build-time sidecar {sectionId→matcher} |
Emit from lib/cms/seed-from-build.js/transformer.js (where entry.matcher.name + entry.section are in scope) → builds/<domain>/section-matchers.json, alongside the seed.json write. |
| 2 | Join + compute | Load the sidecar in lib/structural-diff.js (or join in scorecard.js and pass into diffSite). Compute genericFallbackRate = #generic / #ours in rollupMatcherGap (replaces the null at structural-diff.js:309). |
| 3 | Gate veto | Emit a per-region flag; wire into collectStructuralVetoes (scorecard.js:76) — GenericSection on role-weight ≥ criticalRoleWeight (1.3) → veto → HOLD, mirroring criticalSectionDropped. |
| 4 | Validate | lib/structural-diff.validate.mjs — flip line 161 ("genericFallbackRate deferred … null") to assert the computed fraction; add fixtures supplying matcher identity per ours section; keep the error-page-skip coverage. |
Load-bearing design decision (4b-ii)¶
The sidecar keys on a section id, but the metric must attribute a matcher to each
ours-capture section — and our clean Astro output carries no Wix comp-*
id, so a DOM-id join is unreliable. Recommendation: reuse the
heading + content-order join makeStyleLookup already proved
(seed-from-build.js:143-172) — emit the sidecar as an order-indexed list
(content order matches the ours-capture contentIndex) plus heading, and join
on that, not on DOM id. Alternative (heavier, rejected unless the order-join
proves flaky): have the Astro Section primitive emit a stable
data-section-id the capture probe records.
References¶
- ADR-0009 §2/§3 + Slice tracker (rows 4b-i / 4b-ii after this split).
- Metric contract:
near-match-scorecard-contract.md§1/§3/§4/§6/§7. - Matcher registry:
lib/structure-matcher.js#loadMatchers; low-score fallback precedent:lib/matchers/About.js:42. - Loud-fail discipline: ADR-0004; exemplar
apps/cms/src/components/MissingBlock.astro. - Render primitives:
packages/components-v3/src/components/ui/section/(Section/SectionProse/SectionGrid/SectionSplit/SectionMedia). - Reference blocks:
content-1.astro,content-2.astro,features-3.astro. - CMS join precedent:
lib/cms/seed-from-build.js#makeStyleLookup.