Story 2274: the asterisk that ate the citation — Brief content_format

WHY. Counsel wants to bold a phrase in the Matter Brief. The editor is already capable of it — Story-2264 shipped Ctrl/Cmd+B. The catch is what bolding persists: on the light preset TipTap saves Markdown, so defective pipes becomes **defective pipes**. That string is not private to the editor. It flows into matters.matter_frame_text, which is the search corpus. Bold a phrase with no preparation and you make that matter unfindable by the phrase you just emphasised. This Story is the gate: it makes the corpus safe before pramaan-app is allowed to flip a single preset.

🛰️ Satellite: a formatting feature is a data-contract change

The naive fix is one line — flip the four Brief blocks to preset="light" in the app and ship it. Detonate that: the preset is not a display setting, it is a serializer selection. plain saves editor.getText(); light saves editor.getMarkdown(). Flipping it silently changes the bytes that land in the database, and three things downstream read those bytes literally.

ConsumerWhereWhat ** does to it
FTS search_vector matter/entity.py:200, migration 0131 — a GENERATED ALWAYS … STORED tsvector over matter_frame_text Degraded. The simple config carries the asterisks into tokens.
Trigram index ix_matters_trgm_matter_frame_text (matter/entity.py:103) Breaks. Byte-literal.
ILIKE substring fallback MatterService._matter_text_match (matter/service.py:417) Breaks. A search for defective pipes cannot match **defective pipes** — the delimiters are wedged inside the phrase.

Note where the damage lands: not in the tsvector everyone worries about, but in the humble substring fallback nobody thinks about. That is the one that matches raw bytes.

✈️ Airplane: the flag goes in the envelope, not in the frame

The obvious home for a content_format flag is the frame itself — it is a dict[str, Any], after all. That is a trap:

class FrameOutput(BaseModel):
    """Mirror of the canonical FrameOutput - Vega's Matter Frame contract."""
    model_config = ConfigDict(extra="forbid")   # ← the door is closed

FrameOutput is Vega's contract, and it is parity-gated against a vendored pramaan-agents schema artifact (Story-1995, test_matter_frame_envelope.py). Adding a key to it turns a UI formatting ticket into a three-repo agent-contract migration, and the parity gate goes red the moment you try.

The reframe. How counsel's text is encoded is not a legal fact Vega produces about the dispute. It is app-authored metadata about the bytes. So it does not belong in the frame at all. The envelope already has compartments for exactly this kind of thing — frame / resolution / provenance. content_format becomes the fourth compartment. Vega's contract is untouched; the parity gate stays green.
envelope = {
    "frame":          {...},   # Vega's contract. Untouched.
    "resolution":     {...},
    "provenance":     {...},
    "content_format": {"core_dispute": "markdown"},   # ← new, and SPARSE
}

Sparse is load-bearing. plain is the default, so it is stored as absence. An all-plain frame writes no compartment at all — which means every envelope written before today is byte-identical to what it was, and there is no backfill and no migration. A row becomes markdown only when a human edits it through a Markdown-serializing editor.

🔬 Microscope: why a regex is forbidden, and how we proved it

The ticket says the strip must use a real Markdown parser, never a regex. That sounds like taste. It is not. Legal prose is full of literal asterisks — ***REDACTED***, footnote markers, arithmetic. A regex cannot tell a delimiter from a character that merely looks like one, because that distinction is contextual and only a real CommonMark parse resolves it.

The repo rule is "no green counts until it has been red" (AGENTS.md). So the first draft of the test file was checked against the forbidden implementation — and it passed. The tests were vacuous. Every case had only one lone asterisk, and a regex survives those. Real corruption needs two:

Input (a real citation)Regex \*(.+?)\*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 *
intact
Interest 5 * 3 * 2 per annum Interest 5  3  2 per annum Interest 5 * 3 * 2 per annum

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 stay literal.

test_the_regex_this_forbids_would_actually_corrupt_legal_text pins that exact regex and asserts it destroys the citation the shipped parser keeps. The constraint is now enforced by a test, not by a comment.

Agents write plain text

One subtlety that decides correctness. A field's format must survive unless an agent just overwrote its value, because agents author plain prose. A stale markdown flag on agent-authored text lies in both directions: it tells the strip to parse text with no markup, and tells the app to render literal text as Markdown.

PathWhat the agent authoredWhat keeps its encoding
Vega ingest the whole frame only the locked fields — the lock overlay restores counsel's value, so a locked, bolded field keeps its Markdown flag along with its Markdown text
Mira command only changed everything else (locks are discarded from changed, so they carry too)
Counsel acceptnothing — retypes not one charactereverything
Counsel rejectclears the field to its contract defaulteverything but that field — an emptied field has no encoding

Both agent paths collapse to one helper, _carried_content_format(prior, agent_authored=…).

What changed where

FileChange
matter_frame/content_format.pyNew. The vocabulary (plain | markdown), the four Markdown-capable prose fields, the parser-based strip_markdown, and normalize_content_format (tolerant, because it also reads stored rows).
matter_frame/render.pyrender_matter_frame_text(frame, content_format) — the four prose fields route through the strip. Omit the arg and the output is exactly what it always was.
matter_frame/dtos.pycontent_format on MatterBriefFrameView (read) and MatterFrameEditRequest (write).
matter_frame/service.pyThe compartment on every write path; _validated_content_format (a declaration may not name fields it is not editing, nor flag a structured field as prose) and _carried_content_format.
requirements.in / .lockmarkdown-it-py (MIT).
openapi/app-api.jsonRegenerated (R9). Surfaces as contentFormat for pramaan-app's codegen-sync.
One sentence to remember. Bolding a word changes what is stored, and the store is the search corpus — so the Brief's formatting feature is really a contract change, and the flag that describes the bytes belongs beside the frame, never inside it.

Not in this Story

Rendering. This Story makes the corpus correct and hands pramaan-app the flag. The BriefContent render layer, the Vega proposal strip, and the actual preset flip are pramaan-spec#2275 — which is blocked on this one, because no preset may flip until search is safe.