Skip to content

Lint: an unclosed code fence in block prose swallows the rest of the rendered document #39

Description

@jbeda

Found while reviewing #37. Pre-existing on main, not a regression from that PR — verified byte-identical between a main-built binary and the #37 branch. Filing separately so it doesn't widen that PR.

What happens

A block-level prose field (entity.definition, model.description, enum.description, entity.derivation, scenario.description) may contain a fenced code block. If the author opens a fence and never closes it, everything after it in the rendered .md — later sections, the attributes table, the Mermaid diagram — falls inside the code block.

Repro

kind: DomainModel
version: v1
title: Unclosed fence
description: Probing an unterminated fence in block prose.
entities:
  Ticket:
    definition: |
      An example of the wire format:

      ```xml
      <ticket id="1"/>

    attributes:
      - name: id
        type: string
        description: The identifier printed on the ticket.
$ modelith lint fence.modelith.yaml
0 error(s), 0 warning(s)

$ modelith render fence.modelith.yaml --stdout
...
```xml
<ticket id="1"/>

**Attributes**

| Name | Type | Description |
...

**Attributes**, the table and the trailing ```mermaid block are all inside the unterminated fence as far as any Markdown parser is concerned.

Severity

Not a security issue. Fence content is literal even when the fence is unterminated, so the <ticket id="1"/> above is inert — #37's escaping rule is not defeated. The damage is a broken page: on a Docusaurus build the rest of the document renders as code, or the build fails outright.

Lint passes at every severity, which is the real gap — the model is malformed in a way the tool could catch and doesn't.

Why lint and not the renderer

The renderer now uses goldmark (as of #37) to decide what prose is literal. goldmark exposes no "was this fence closed" flag on ast.FencedCodeBlock — an unterminated fence simply runs to the end of the document, which is CommonMark-correct behaviour, not a parser bug. Detecting it inside the renderer means hand-scanning the source again, which is exactly the layer #37 removed.

A lint rule is the right home: it is an authoring mistake, it is cheap to detect (count fence openers per block-level prose field), and lint is where the model's other structural problems are already reported.

Suggested shape

  • A semantic warning, or an error — worth deciding, since the rendered output is genuinely broken rather than merely suboptimal.
  • Message should name the field and the line, e.g. /entities/Ticket/definition: code fence opened but never closed — everything after it renders as code.
  • Fixture-driven test alongside the other lint rules.

Related: #37 (the renderer hardening that surfaced this), ADR-0014 (prose is Markdown, not HTML).

🤖 Filed by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions