PR: #580: feat(brief): BriefContent render layer + light preset on the four Brief blocks [Story-2275] by @anyamshwr
View PR →

Story 2275: the one-line change that wasn't — bold/italic on the Matter Brief

WHY. Counsel cannot bold a word in the Brief. The machinery to do it has been sitting in the repo since Story-2264 shipped Ctrl/Cmd+B — but those shortcuts only fire on a block with a light or full preset, and all four Brief blocks are on plain. The gun was loaded and nobody connected the trigger. This Story connects it — and the reason that took an Epic, two repos, and a gate is the whole lesson.

🛰️ Satellite: a preset is a serializer, not a stylesheet

The naive fix is one line: preset="light" on four call sites. Detonate it.

A preset does not decide which buttons appear. It decides what bytes get written to the database:

PresetMarksWhat it SAVES
plainnoneeditor.getText() → plain text
lightbold, italic, linkeditor.getMarkdown() → Markdown
full+ headings, lists, quoteeditor.getMarkdown() → Markdown

Flip the preset and the moment counsel bolds a phrase, defective pipes stops being stored as defective pipes and starts being stored as **defective pipes**. Every consumer that prints those bytes literally now leaks raw asterisks at a user. This is a data-contract change wearing a UI toggle's clothes.

✈️ Airplane: who actually reads those bytes

The original framing named three consumers. Two of them do not exist, one exists somewhere nobody expected, and the one that does break was never named at all.

Claimed consumerReality
PDF/DOCX export ❌ Does not exist — in any repo. document_render in pramaan-functions is inbound: LibreOffice converting uploaded Office files to PDF for viewing. Built here as a tested seam, because an exporter is planned.
Search / matching ⚠️ Real, but unreachable from TypeScript. Search is Postgres FTS + a trigram index + a byte-literal ILIKE fallback, over a blob that Python renders server-side. No function in this repo can influence those bytes. It became the gate: pramaan-spec#2274.
LLM / agent (Vega) ⚠️ The app never sends frame text — the broker assembles current_frame server-side. Markdown passes through untouched. No app work.
(unnamed) Vega proposal strip ✅ The one real leak in this repo. vega-proposal-strip.tsx rendered proposed values through a hardcoded <p>. Flip the preset without fixing it and counsel sees **bold** in the strip while the Brief directly above renders real bold — the same field, contradicting itself.
The reframe that shaped the code. "Route every consumer through one layer" is right. "Give that layer a search target" is wrong — it would be a function that looks authoritative and affects nothing, which is the most dangerous kind of code. The layer ships three targets, not four, and the strip that keeps search honest lives in Python.

🔬 Microscope: the layer, and the two things it refuses to do

BriefContent(value, format, target)

  target=ui      -> React nodes: <strong> / <em> / text     (view mode, proposal strip)
  target=export  -> [{ text, bold, italic }]                (a future PDF/DOCX emitter)
  target=agent   -> the Markdown string, unchanged          (LLM consumers)

  format=plain   -> PASSTHROUGH on every target. Never parsed.

WHY it refuses to build an HTML string

The obvious build is Markdown → HTML → sanitizer whitelist → dangerouslySetInnerHTML. We emit React elements instead. A text node holding <script>alert(1)</script> renders as those literal characters — React escapes it — so injection is impossible by construction rather than by a filter that has to be correct. The repo has no sanitizer dependency, and now needs none. The whitelist is enforced by what the file can emit: <strong>, <em>, text. Nothing else has a code path.

WHY it refuses to use a regex

Legal prose is full of literal asterisks. A regex cannot tell a delimiter from a character that merely looks like one, because that distinction is contextual. This is not theoretical:

A real citationRegex \*(.+?)\*CommonMark parser
Breach at cl. 5 * and cl. 7 * Breach at cl. 5  and cl. 7
both markers eaten
Breach at cl. 5 * and cl. 7 *

The regex pairs two unrelated footnote markers into a phantom emphasis span and deletes both, silently eating the text between them. Nothing reports it. The parser refuses to open emphasis on a * followed by whitespace (not left-flanking), so both survive.

Both repos parse with the same CommonMark implementation — markdown-it here, markdown-it-py in pramaan-functions — so the screen and the search index cannot disagree about which asterisk is a delimiter.

The component tree

MatterBriefPage
 |
 |-- save(field) ------------------- declares contentFormat: {field: "markdown"}
 |                                   for the four prose fields  [the WRITE path]
 |
 |-- BriefHeader
 |    `-- FrameSummaryHistory  preset="light"        <-- frame_summary
 |         `-- EditableFieldWithHistory
 |              |-- EditableSurface     (edit)  --.
 |              `-- SnapshotPreview      (view)  --+-- pre-hydration fallback
 |                                                 |    renders through
 |-- EditableField  preset = isMarkdownCapableBriefField(field)                |
 |    |    <-- core_dispute / theory_of_case / settlement_posture              v
 |    `-- EditableFieldWithHistory ............................. MarkdownContent
 |                                                                    ^
 `-- VegaProposalStrip                                                |
      `-- BriefContent(target=ui) --------------------------------- -'
           [THE LEAK, now fixed]

One predicate drives two things. isMarkdownCapableBriefField(field) picks the editor's preset and decides whether the write path declares contentFormat: "markdown". They cannot drift apart: a field that saves Markdown is exactly a field that says it does.

The flash nobody would have caught

EditableSurface and SnapshotPreview render a raw-text fallback before TipTap mounts (SSR + first paint). On a Markdown preset that fallback dumped **bold** on screen for a frame before the editor swapped it. Both now render through MarkdownContent, which is why the primitive lives in shared/ and the Brief-facing vocabulary wraps it in brief/ — a feature module cannot be imported from shared/ without inverting the layering.

What changed where

FileChange
shared/markdown-content.tsxNew. The parser + React renderer. Emits <strong>/<em>/text, nothing else.
brief/brief-content.tsxNew. The Brief-facing layer: three targets, the format reader, the four-field vocabulary.
brief/vega-proposal-strip.tsxThe leak. Renders through BriefContent instead of a raw <p>.
shared/editable-surface.tsx, shared/snapshot-preview.tsx Pre-hydration fallbacks no longer flash literal asterisks.
brief/matter-brief-page.tsxThe write path declares contentFormat alongside the bytes.
brief/matter-brief-sections.tsxThe preset flip. The last and smallest step — and the only line that changes what is saved.
src/gen/**, openapi/app-api.jsonCodegen-sync of contentFormat from the merged pramaan-functions contract (7 files, additions only).
One sentence to remember. Flipping a TipTap preset changes what is stored, not just what is shown — so the last and smallest step of this Story had to be the preset flip, and everything before it existed to make that one line safe.

Files changed

15 file(s), +854 / -9 lines

Path Role + -
package.jsonConfig+2-0
pnpm-lock.yamlConfig+66-0
src/components/matter-detail/brief/brief-content.test.tsxComponent+144-0
src/components/matter-detail/brief/brief-content.tsxComponent+132-0
src/components/matter-detail/brief/matter-brief-page.tsxComponent+15-1
src/components/matter-detail/brief/matter-brief-sections.tsxComponent+17-1
src/components/matter-detail/brief/presentation.test.tsComponent+18-1
src/components/matter-detail/brief/presentation.tsComponent+23-1
src/components/matter-detail/brief/vega-proposal-strip.stories.tsxComponent+20-0
src/components/matter-detail/brief/vega-proposal-strip.tsxComponent+11-1
src/components/shared/editable-surface.tsxComponent+14-1
src/components/shared/markdown-content.stories.tsxComponent+110-0
src/components/shared/markdown-content.test.tsxComponent+135-0
src/components/shared/markdown-content.tsxComponent+129-0
src/components/shared/snapshot-preview.tsxComponent+18-3

Not in this Story

Underline, headings, links, lists, tables. If any is requested later, the store moves to structured TipTap JSON — we do not extend the Markdown-in-field model. And search: that is pramaan-spec#2274, the gate, which merged first.