Skip to content

Audit the Markdown surface: one shape for a one-object card, and a helper that enforces it #697

Description

@jmrplens

Status 2026-09-12

What is left, the earlier body kept below for context:

Layers 1 and 2 are on main (PRs 708 to 721): toolutil.Card is the one card writer, docs/development/markdown-card.md defines it, every formatter is migrated, and both gates read zero. Layer 3, what remains: (1) flip the card and bool-time contexts into check-md-escaping and make the runtime registry gate fail rather than log (fill mdGateExceptions, turn mdGateLog into a failure); (2) remove the FmtMd* constants from internal/toolutil/markdown.go and their last caller in cmd/gen_llms; (3) the English docs and the Spanish site page on the card; (4) the four findings that stand between here and the flip: a HintAction id in keys that the catalog does not have (and a test validating every HintAction literal against the catalog), the bool-time context judging five values that are not Markdown, a declaration for the elicitationtools consent lines, and the leftovers in prompts and the default surface's own output.


The Markdown a tool returns is what a model reads, and the shape of it is currently decided per file rather than by any rule. Measured on the tree today:

  • 27 packages render a single-object card as a | Field | Value | table.
  • 76 render one as a bulleted list of - **Label**: value.
  • 4 files use both shapes, so the same package answers two questions in two shapes.

internal/toolutil/markdown.go has helpers for a URL, pagination, a list summary, a description and an empty result, and the FmtMd* constants for the commonest bulleted labels. It has no helper for a field row, which is why the choice is made again in every formatter and why the two shapes drifted apart.

What surfaced this

A formatter that appends to a bulleted card was given table rows. A pipe row with no header and no delimiter row is not a table in GFM, so those lines rendered as literal | Runners Token | glrt-… | text in the middle of the card. It was caught and fixed on the 1:1 branch and never reached main, but nothing would have caught it: no gate compares a formatter's output shape against the section it writes into, and the test asserted only that a substring was present.

What the audit should settle

  1. One rule for a one-object card, table or list, and a helper in toolutil that implements it so a formatter cannot pick the other by accident.
  2. The four files that use both, resolved to whichever the rule says.
  3. Whether a gate is worth it: a check that a formatter never emits a pipe row without a preceding delimiter row would have caught the defect above mechanically, in the same spirit as make check-md-escaping.
  4. A sweep for values that should go through an existing helper and do not: booleans printed with %v instead of BoolEmoji, labels re-spelled instead of using the FmtMd* constant, timestamps not passed through FormatTime.

Queued deliberately after the 1:1 field work, not before it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v3.1.0Targeted at the 3.1.0 release

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions