stacklok / stacklok/modelith

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

Open
#39 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
32
Forks
5
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the modelith lint entry point and read the existing lint rules and fixture-driven tests. Trace how block-level prose fields are checked, then add coverage for an unterminated fence in a field such as /entities/Ticket/definition; done means lint reports the field and line with an agreed warning or error severity.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, markdown
Domain
cli, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.