Skip to content

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. genericFallbackRate stays null after this slice.
  • 4b-ii — the genericFallbackRate metric (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)

  1. 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.15 score guarantees this, but verify).

  2. 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.mjs passes (schema ↔ dispatcher ↔ file all present).
  • CMS build + deploy; force a fcr.generic-section block 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.