Skip to content

Markdown formatting standard — Obsidian-first authoring choices

Status: open/accepted

  • One Obsidian-first markdown rule set binds all docs and skill prose: ATX headings, - bullets, explicit numbering, inline links plus wikilinks, hand-formatted tables, language-tagged fences, no raw HTML, prose wrapped at 100 columns (see Formatting axes).
  • Rules split across three enforcement tracks — formatter, schema validator, reviewer (see Enforcement split).
  • S-0007-markdown-formatting codifies the binding rule set; D-0011-markdown-formatting-tool picked the enforcing formatter (rumdl); T-0008 wired it and ran the one-time corpus reformat.

The corpus drifted on markdown formatting choices — line length, heading style, list markers, link form, code fences, table alignment, frontmatter quoting. The drift surfaced two ways: as a stream of small formatting changes during reviews (reviewer and author re-litigating the same axes per PR), and as inconsistency that trips Obsidian, the project’s primary editor (block-id placement, frontmatter quoting, table reflow).

Two partial conventions already exist and are authoritative inputs, not open questions:

SourceRuleStatus
docs/CLAUDE.mdBe SuccinctSuccinct but fully explanatory prose; avoid modifiers that add nothing.Authority — the Standard inherits it.
docs/CLAUDE.mdUse StructurePrefer a table over a free-form list for parallel-shape content.Authority — the Standard inherits it.
docs/CLAUDE.mdNamingRefer to the project as SDLC (uppercase).Authority — the Standard inherits it.
D-0002-entity-identifier-shapePer-type filename rule (AA-NNNN-<slug>.md); the id is identity, the slug is convenience.Settled there — referenced, not restated.

Research may add rules but may not soften these.

Obsidian is the primary constraint. Several Obsidian features must survive any formatter the project adopts (see Obsidian compatibility below); a rule that breaks one of them is rejected on that ground alone.

This ADR settles the choices only. The tool was picked separately — rumdl, chosen in D-0011-markdown-formatting-tool via T-0007 — and wired by T-0008. The Standard S-0007-markdown-formatting codifies the binding rule set that follows from this decision.

The formatting rules below bind every markdown file in scope (the Standard’s applies_to.paths). Each axis carries the chosen rule, its rationale, and its Obsidian implication.

AxisRuleObsidian implication
Line lengthWrap prose at 100 columns. Tables, fenced code blocks, headings, link/wikilink targets, and frontmatter are exempt — never reflow them to satisfy the limit.Soft-wrap renders long lines fine in preview; the wrap is a diff-quality choice for source view. Obsidian source-mode honors hard wraps without rendering them as breaks.
Heading styleATX (#, ##, …) for all headings. One H1 per file, matching the frontmatter title.Obsidian’s outline, heading-fold, and file transclusion target ATX headings reliably; Setext has no ###+ form and complicates heading links.
List markers- only for unordered lists.Author preference; one marker keeps diffs and nesting unambiguous. Obsidian renders all three (-/*/+) identically, so the choice is purely source hygiene.
Numbered listsExplicit (1. 2. 3.), not lazy (1. 1. 1.).Renderers differ on lazy numbering; explicit numbers read correctly in source view, which Obsidian users live in.
Link styleInline links for external URLs ([text](url)); wikilinks () for catalogued entities. No reference-style links (expanded under Options considered).Wikilinks are Obsidian-native and rename-aware; reference-style adds a second lookup with no payoff.
Block-id placementLine-terminal ^anchor at the end of the block it tags.Obsidian requires ^anchor at line end to register a block reference; mid-line anchors are ignored.
Transclusion formfile and file; never paste-and-drift a copy.Obsidian resolves these live; the generated index relies on transclusion.
Code fencesTriple-backtick with a language tag (```ts, ```bash, ```text when none applies). No indented code blocks.Required for syntax highlighting; indented blocks are fragile next to lists.
TablesDo not reflow, do not width-limit. Authors hand-format; the formatter leaves tables untouched (expanded under Options considered).Authors read tables in plain text; reflow makes diffs noisy and hand-edits painful. This is why the line-length rule exempts table rows.
FrontmatterYAML between --- fences at file top. Quote schema_version (string-typed). YAML-list (block) style for tags / related / applies_to.paths. The schema is the authority on shapes.Obsidian renders frontmatter as Properties; consistent shapes keep both the Properties UI and the schema validators happy.
HTMLForbid raw HTML elements in docs/ markdown. <…> placeholder notation in prose and angle brackets inside code/wikilinks are not HTML and are allowed.Mixing HTML with markdown muddies source view and Obsidian preview; defer until a real use-case forces a revisit.
Trailing whitespaceStrip.Safe default; keeps diffs clean. (Note: a markdown hard-break uses two trailing spaces — the project does not use hard-breaks, so stripping is unambiguous here.)
Final newlineExactly one trailing \n.Standard POSIX convention; keeps git diff from flagging “no newline at end of file”.
Wikilink rename safetyGoverned by D-0002-entity-identifier-shape — the id is the stable anchor; the slug never tracks title changes.Renaming a file’s slug does not break AA-NNNN links. Reference D-0002; do not restate the rule here.
Broken-link checkingDeferred. rumdl ships link rules (MD051/MD052/MD057) but they stay disabled in .rumdl.toml until a follow-up task repairs the corpus’s existing broken links.Not required in the first formatter pass; flagged, not blocking.

Not every rule is machine-checkable. The Standard partitions them:

ClassAxesEnforcement
Formatter-enforcedList markers, numbered-list explicitness, code-fence form, trailing whitespace, final newline, prose line-length (with the table/code/heading exemptions)rumdl (D-0011-markdown-formatting-tool), wired by T-0008: the lefthook markdown-fmt pre-commit hook, the fmt-markdown justfile recipe, and the /markdown-format skill, all reading .rumdl.toml.
Schema/validator-enforcedFrontmatter shapes (schema_version quoting, list styles), id/filename shapeThe sdlc entities validate op (plugin/lib/model/ops/validate.ts) + the identifier-shape pre-commit gate; not the markdown formatter.
Reviewer-enforced (subjective)Be Succinct, Use Structure (tables over lists), SDLC-uppercase naming, HTML prohibition, wikilink-vs-inline choiceHuman review; a spell-check / lint target may assist (SDLC casing) but the judgment stays with the reviewer.

Obsidian-first authoring is the forcing constraint: the project is read and edited in Obsidian source/preview, so any rule that degrades the Obsidian experience (broken transclusions, ignored block-ids, reflowed tables) is disqualified regardless of how tidy it looks to a generic markdown linter. Downstream tooling is the second driver — formatters, frontmatter validators, and eval harnesses all parse this corpus, and a settled shape removes the per-PR re-litigation that the drift forced.

Settling the choices in an ADR (the why) plus a Standard (the enforceable rule) means a reviewer cites a rule instead of relitigating it, and the formatter has an unambiguous specification to target.

  • 80 — too aggressive for a corpus full of wikilinks and inline code; would force awkward wraps mid-link.
  • 100 (chosen) — wraps prose to a readable, diff-friendly width while leaving room for inline code and short wikilinks.
  • 120 — defensible and “more modern”, but the hand-check (below) showed that most over-100 lines are tables and fenced code, which we exempt anyway; for prose, 100 is tighter without pain.
  • No limit — rejected: source-view diffs of reflowed paragraphs become unreadable.

The decisive move was scoping the limit to prose only. Once tables and code fences are exempt, the 100-vs-120 gap shrinks to a handful of prose lines, and 100 wins on diff quality.

  • Allow both / lean Setext — Setext only reaches H1/H2; H3+ must be ATX anyway, so a Setext lean produces a mixed file. Worse, Obsidian heading-transclusion and the outline pane are most reliable against ATX.
  • ATX-only (chosen) — one heading syntax across all levels, reliable Obsidian heading links, simpler formatter rule. The Obsidian-reliability ground is decisive.
Section titled “Link style: inline + wikilink vs reference-style”

Markdown offers three link forms; the rule keeps two and bans the third:

  • Reference-style ([text][label] in prose, [label]: url defined elsewhere, usually file bottom) — rejected:
    • It splits a link across two locations. Reading source — where Obsidian authors live — means jumping to the definition block to learn where a link points; preview hides the cost but source view pays it on every read.
    • Its classic payoff is keeping long URLs from blowing out prose wrapping — but link targets are already exempt from the line-length rule (see Formatting axes), so the benefit is void here before it starts.
    • Definition blocks rot: deleting the prose that used [label] leaves an orphaned definition, and editing a URL means finding a definition far from its use site. Both are invisible to a casual diff read.
    • Allowing it means two interchangeable syntaxes for the same external link, which is exactly the per-PR relitigation this ADR exists to end.
  • Inline for external URLs, wikilinks for entities (chosen) — the target is readable at the point of use; wikilinks are Obsidian-native, power the graph/backlink panes, and stay rename-safe via D-0002-entity-identifier-shape. One form per job, no lookup indirection.

Tables: hand-formatted vs formatter-normalized

Section titled “Tables: hand-formatted vs formatter-normalized”

“Hand-format” means the shape the author typed is the shape that ships — the formatter must not restyle pipe-tables in either of the two ways formatters commonly do:

  • Column-padded / aligned (Prettier-style) — every cell is padded so the pipes line up. Rejected: re-padding is global to the table, so a one-word edit in one cell rewrites every row and a 20-line table produces a 20-line diff for a one-line change. Wide cells (this corpus routinely embeds wikilinks and code in cells) force every other row to carry matching padding, and hand-editing then means maintaining that alignment by hand.
  • Width-limited / reflowed — rows wrapped to satisfy the prose line-length limit. Rejected outright: a pipe-table row is one physical line by grammar; wrapping it does not produce a long table row, it produces a broken table. The corpus hand-check (below) makes this concrete — D-ORMG-data-model.md carries a 1012-character row, and nearly all >100-column lines in the sampled docs are table rows, so any width rule that touches tables is unenforceable against the real corpus.
  • Hand-formatted, formatter-exempt (chosen) — minimal diffs (only the edited row changes), no alignment maintenance, and the line-length axis exempts rows so the two rules cannot collide. The cost is ragged pipes in source view; Obsidian preview renders the table identically either way, so the raggedness is cosmetic and confined to source view.

This axis is also a tool-selection constraint, not just a style call: a candidate formatter that cannot leave tables untouched is disqualified in T-0007 (see Consequences).

  • Allow a small allowlist (<br>, <sup>, …) — tempting but opens a slope; every reviewer then debates the allowlist per PR.
  • Forbid (chosen) — simplest rule; relax later if a concrete use-case appears. The prohibition is scoped to raw HTML elements, not the <placeholder> angle-bracket notation that appears in templates and prose (the placeholder scanner already distinguishes these).
  • The pre-existing corpus needed a one-time reformat, which landed with the formatter wiring (T-0008 — ~7k fixes across 433 files).
  • The formatter must support per-construct exemptions (leave tables and fenced code untouched while wrapping prose). This was a real selection constraint on the tool — a formatter that reflows tables is disqualified, which is what eliminated Prettier and dprint and picked rumdl in D-0011-markdown-formatting-tool.
  • Reviewers gain a citable rule set; per-PR formatting debate should drop.
  • Frontmatter rules bind to the existing schema validators, not the new formatter — the two enforcement tracks stay separate.

Complete. The one-time reformat of the pre-existing corpus landed with the implementation task (T-0008), and the Standard S-0007-markdown-formatting is open/active: mechanically enforced at commit time by the lefthook markdown-fmt hook.

The following Obsidian features MUST survive any formatter or rule this ADR endorses. A rule that breaks one is rejected.

FeatureSyntaxRule that protects it
WikilinksAA-NNNN, AA-NNNN-slugLink-style rule keeps wikilinks for entities; line-length exempts link targets so a wrap never splits .
Block-idsline-terminal ^anchorBlock-id placement rule pins ^anchor to line end; trailing-whitespace strip never touches the anchor token.
Heading transclusionfileATX-only headings keep #Heading targets stable; transclusion-form rule mandates the live form.
Block transclusionfileTransclusion-form rule; the generated index depends on .
Frontmatter (Properties)YAML between --- fencesFrontmatter rule keeps shapes schema-consistent so the Properties UI and validators agree.

The formatter selection (T-0007D-0011-markdown-formatting-tool) verified each of these against candidate tools before adoption, including a byte-identical fixture round-trip for the Obsidian syntaxes.

A hand-check during the definition task (T-0006) sampled 10 existing docs against the rules. Findings:

DocObservationImplication
docs/CLAUDE.mdFully conformant (max line 67, - bullets, no HTML, no trailing ws).Short prose files already comply.
D-0002-entity-identifier-shape.md33 lines >100, almost all table rows.Confirms tables must be exempt or the rule is unenforceable.
D-ORMG-data-model.mdOne 1012-char line (a wide table row).Same — table exemption is load-bearing.
D-0007-deterministic-op-substrate.md55 lines >100, mix of tables, fenced code, and a few long prose lines.Prose-only scoping leaves only a few real wrap targets.
S-0005-entity-definition-contract.md17 >100, mostly tables.Table-heavy Standards are the norm.
S-0001-...1 line >100.Near-conformant.
T-0006.md (the originating task)25 >100, tables + the axis matrix.Tables again dominate the overage.
github-ref-leases/protocol.md36 “HTML-like” hits — all false positives (<placeholder> and code angle-brackets, no real HTML).The HTML rule MUST exempt <…> notation and code, or it flags clean files.
D-K9PX-system-architecture.md35 >100, wide tables.Consistent pattern.
D-Q2WR-task-pickup-order.md7 >100.Near-conformant prose.

The ambiguities the check surfaced were settled during tool selection (D-0011-markdown-formatting-tool) and wiring (T-0008), and the resolutions live in .rumdl.toml:

  • Heading-style detection — rumdl owns the check (MD003 style = "atx"); no hand-rolled grep, so Setext underlines are never confused with thematic breaks or table separators.
  • Prose-vs-table line classification — rumdl parses block structure; MD013 wraps prose at 100 with headings, code-blocks, and tables all exempt.
  • HTML-vs-placeholder discrimination — the HTML prohibition stays reviewer-enforced (MD033 disabled), so <…> placeholder notation and code angle-brackets are never false-flagged.
  • Scope of applies_to.paths — skill prose (SKILL.md, S-0006-skill-md-is-direct-instruction) stays in scope at full strictness; irreducible over-length inline-code lines get enumerated per-file carve-outs in .rumdl.toml instead of a looser rule.

← Back to Decisions