Checkpoint 1 · ADR-082

Passage Card (CA6)

A formatted, sectioned rules/passage card — one new archetype that plugs into the existing Flashcard Carousel with a single RevealView case. Two decisions needed.

ADR-082 · Proposed UI-authored ENR-implemented FSRS coupling = zero No carousel fork
Checkpoints Latest →
The archetype

What the Passage Card (CA6) is

A generalized "formatted, multi-section answer" card — one archetype, many use-cases.

WordSense currently has five card archetypes (CA1–CA5). They all share one thing in common: the answer face is a handful of flat fields — definition, examples, translation, forms. None of them can render a long-form, headed, multi-section answer.

The Passage Card (CA6) fills that gap. It is a card whose answer is a formatted passage split into sections: each section has a heading and a body made up of paragraphs, lists, callouts, or key/value pairs. Think of it as a structured document that a learner must recall as a single unit.

The driving example is the Four Bodhisattva Vows — four titled vows each with a brief explanation, plus a collapsible "how to practice" section. But the archetype is content-agnostic:

One t310 row, one FSRS schedule, the same grade pipeline as every other card. The archetype only controls what the answer face displays — not how the card is scheduled.

Design gallery

Three live instances

Screenshots from qa-exploration/passage-card/passage-card-gallery.html — Dark Hero palette, 390-wide "phone" shells, live collapsed sections.
Left to right: Bodhisattva Vows (spiritual) · Wet Lab checklist (procedural) · Fair-Use Factors (legal rule-set). Each shows the prompt face (title + question) and the revealed sections. The "How to practice" / "How courts weigh it" sections are collapsed by default (chevron row); tap to expand.
Passage Card gallery — three instances side by side in Dark Hero palette
Architecture

How it plugs into the existing carousel

No fork. One RevealView case. Carousel chrome, swipe, grade pipeline, FSRS write path — untouched.
UI work (later plan — gated on ADR acceptance)
1
Add case 'passage-sections' to RevealView.tsx switch dispatcher
2
Add new PassageSectionsBlock component — reuses existing Card/Heading/block primitives
3
Add passage?: PassageContent to CarouselWordData in types.ts + carousel-config.ts
ENR work (gated on ADR acceptance — briefed as BRIEF-2026-06-27-003)
4
Add 'passage' value to card_archetype CHECK constraint on t070 (ADR-081 slot)
5
Store content in archetype_extras.passage jsonb — sections + blocks per section
6
Thread archetype + passage through session-loader.ts → carousel-card-adapter.ts
FSRS / grade pipeline
ONE t310 row per passage card. processReview, CL10–CL80 lifecycle, growth stages — completely unchanged. CARD-LIFECYCLE-FLOW.md and FSRS-CARD-STATE.md need zero edits.
Why no fork? RevealView.tsx is already a config-driven block dispatcher: config.revealBlocks → switch(block.id) → per-block renderer. An archetype only changes which blocks render and what content shape they read. The prompt face (CardPrompt.tsx) already renders a passage title via the existing headword element. Nothing else in the carousel needs to know about this archetype.
Frank's two decisions

What needs your call

Both items were deliberately left open in ADR-082. Your answers unblock schema migration and implementation planning.
1
What should we name this archetype?
The stored value goes into t070.card_archetype CHECK constraint (ADR-081) and must stay stable — it is a data value in the database, not just a label. The display name is separate and softer.
passage Recommended
Display name: "Passage Card" (CA6). Colloquial alias: "rules card". Most general — covers vows, checklists, legal rule-sets, creeds, doctrine without prejudicing toward one use-case.
rules
Display name: "Rules Card". Foregrounds the rule-set / checklist use-case. Reads narrow for vows and creeds ("the Rules of Compassion" sounds odd). OK if the corpus will primarily be procedural/legal.
reference
Display name: "Reference Card". Reads as a reference document. Collides with the vault's docs/vault/references/ directory naming — could create casual confusion in conversation.
Frank picks
2
How should passage content be stored and rendered?
This controls how authors write content and whether we add a formatting library. The security stakes are real — answer content is displayed to every learner.
Option A — Structured blocks Recommended (AI judgment)
Libraries added: none. Bundle increase: 0 KB. Sanitizer needed: none.
Content is stored as a JSON array of typed blocks. Each block is one of: paragraph, list, callout, or kv (key/value pairs). Every text field is plain text — React renders it as a text node, which is auto-escaped. There is no HTML injection surface at all.
Validated at the session-loader boundary with zod (already a dependency). Fits cleanly into the existing RevealView block architecture. LLM enrichment can emit structured JSON reliably.
Security win: The sanitization problem is eliminated by construction — there is no HTML to sanitize. This is not a matter of "trusting the sanitizer to hold" — it is the absence of an injection surface.
Option B — Sanitized markdown Pre-blessed fallback
Library stack: react-markdown@10.1.0 + remark-gfm@4.0.1 + rehype-sanitize@6.0.0. Bundle: ~+90 KB unpacked. No rehype-raw, no MDX (those are rejected for answer content — see ADR-082 security table).
Allows authors to hand-write freeform rich text (tables, nested formatting, headings) instead of JSON blocks. More flexible for human-authored content; harder to emit reliably from an LLM pipeline.
Pick this if: content authors will hand-write markdown rather than emit structured blocks from enrichment, and the block vocabulary (paragraph/list/callout/kv) cannot express what they need.
► Two options the ADR explicitly rejected — why

C — markdown-it + DOMPurify: heavier (two-library sanitize dance), SSR requires isomorphic-dompurify/jsdom shim, higher maintenance. No advantage over react-markdown if we have React.

D — MDX: Rejected outright for answer content. MDX compiles to executable JavaScript — embedded expressions and components execute at render time. It cannot be sanitized (it is code, not HTML). Only ever appropriate for trusted, author-controlled content like documentation pages.

Frank picks — A or B (ADR pre-blesses B as the named fallback so a future plan doesn't re-litigate security)
Decisions ledger

What's already settled

Explicit decisions Frank stated, plus implied ones from prior work — the misread-catcher.
E1
Runs inside the existing Flashcard Carousel — no new carousel
Confirmed by Frank in design recon. There is no "Phrase Carousel" or bespoke container. The archetype plugs in via RevealView's existing config-driven switch.
Decided by Frank
E2
FSRS coupling is zero — one t310 row, one schedule, no lifecycle edits
TERMINOLOGY.md §262/281 + CARD-LIFECYCLE-FLOW + FSRS-CARD-STATE confirm: card_archetype is a display discriminator only. processReview, CL10–CL80, growth stages — unchanged. Neither cardinal doc needs an edit.
Decided by Frank
E3
card_archetype is the right discriminator — ADR-081 reserved new values for exactly this
ADR-081 (Accepted 2026-06-11) made card_archetype a NOT NULL CHECK-constrained column with reserved slots for new archetypes. A new value is the clean path; no new column needed.
Decided — ADR-081
E4
The whole passage is studied as one unit — NOT per-section scheduling
Architectural decision in the ADR. Per-section scheduling (container → child cards, each with their own t310 row) is a different design and out of scope. If a future use-case needs it, it uses the existing container→child pattern, not this archetype.
Decided — ADR-082
E5
Answer content is NEVER rendered via dangerouslySetInnerHTML — binding regardless of name/option chosen
Security rule stated in ADR-082 §3. Applies to whichever option Frank picks. If Option B (markdown) is chosen, rehype-sanitize is mandatory and rehype-raw + MDX are prohibited for this content surface.
Decided — ADR-082
I1
Schema migration + session-loader wiring is ENR work, gated on this ADR's acceptance
Inferred from cross-role discipline (ADR-050) and the existing briefing (BRIEF-2026-06-27-003). ENR implements; UI consumes the read surface via the briefing contract. No ENR work starts until Frank accepts this ADR.
Inferred from cross-role rules
I2
How enrichment (or an admin screen) authors passage content is out of scope here
The ADR proposes the content shape and validation contract. How a human or LLM pipeline actually writes those blocks into archetype_extras.passage is a separate planning item — deferred behind ADR acceptance.
Inferred from ADR scope
Focus

Worth your eye

Things that might read differently on the page than in conversation.
🔒
The security gap between Option A and Option B is real, not theoretical. Option A has no HTML injection surface — there is no sanitizer to misconfigure, no dependency to exploit, no rehype-raw slip to make. Option B is safe with the named library stack, but it requires the sanitizer to hold across upgrades. That is a fundamentally different security posture, not just a preference.
🔌
The "one RevealView case" claim is verified, not asserted. RevealView.tsx lines 1145–1236 were read and confirmed: it is a config-driven switch dispatcher. Adding a case is the same pattern as every other archetype. The TSX mockup in qa-exploration/ proves out the block renderer with no new HTML sinks.
📋
The archetype name is a database value, not a display label. Whatever stored value Frank picks ('passage', 'rules', 'reference') goes into the t070 CHECK constraint and into every passage-card row in the corpus. Renaming it later requires a migration + data backfill. Pick once. The display name ("Passage Card", "Rules Card") is softer and can be changed in code.
🎓
Adding this archetype carries zero risk to the review pipeline. FSRS, the grade API, CL10–CL80 lifecycle, growth stages — all untouched. A Passage Card is scheduled, graded, and advanced exactly like a Sense Card. The only new surface is the answer face renderer.