Markdown formatting standard — Obsidian-first authoring choices
Status: open/accepted
Summary
Section titled “Summary”- 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.
Context
Section titled “Context”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:
| Source | Rule | Status |
|---|---|---|
docs/CLAUDE.md → Be Succinct | Succinct but fully explanatory prose; avoid modifiers that add nothing. | Authority — the Standard inherits it. |
docs/CLAUDE.md → Use Structure | Prefer a table over a free-form list for parallel-shape content. | Authority — the Standard inherits it. |
docs/CLAUDE.md → Naming | Refer to the project as SDLC (uppercase). | Authority — the Standard inherits it. |
| D-0002-entity-identifier-shape | Per-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.
Decision
Section titled “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.
Formatting axes
Section titled “Formatting axes”| Axis | Rule | Obsidian implication |
|---|---|---|
| Line length | Wrap 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 style | ATX (#, ##, …) 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 lists | Explicit (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 style | Inline 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 placement | Line-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 form | file and file; never paste-and-drift a copy. | Obsidian resolves these live; the generated index relies on … transclusion. |
| Code fences | Triple-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. |
| Tables | Do 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. |
| Frontmatter | YAML 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. |
| HTML | Forbid 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 whitespace | Strip. | 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 newline | Exactly one trailing \n. | Standard POSIX convention; keeps git diff from flagging “no newline at end of file”. |
| Wikilink rename safety | Governed 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 checking | Deferred. 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. |
Enforcement split
Section titled “Enforcement split”Not every rule is machine-checkable. The Standard partitions them:
| Class | Axes | Enforcement |
|---|---|---|
| Formatter-enforced | List 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-enforced | Frontmatter shapes (schema_version quoting, list styles), id/filename shape | The 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 choice | Human 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.
Options considered
Section titled “Options considered”Line length: 80 vs 100 vs 120 vs no limit
Section titled “Line length: 80 vs 100 vs 120 vs no limit”- 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.
Heading style: ATX-only vs allow-Setext
Section titled “Heading style: ATX-only vs allow-Setext”- 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.
Link style: inline + wikilink vs reference-style
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]: urldefined 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.mdcarries 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).
HTML: forbid vs allow-with-allowlist
Section titled “HTML: forbid vs allow-with-allowlist”- 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).
Consequences
Section titled “Consequences”- 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.
Migration
Section titled “Migration”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.
Obsidian compatibility
Section titled “Obsidian compatibility”The following Obsidian features MUST survive any formatter or rule this ADR endorses. A rule that breaks one is rejected.
| Feature | Syntax | Rule that protects it |
|---|---|---|
| Wikilinks | AA-NNNN, AA-NNNN-slug | Link-style rule keeps wikilinks for entities; line-length exempts link targets so a wrap never splits …. |
| Block-ids | line-terminal ^anchor | Block-id placement rule pins ^anchor to line end; trailing-whitespace strip never touches the anchor token. |
| Heading transclusion | file | ATX-only headings keep #Heading targets stable; transclusion-form rule mandates the live … form. |
| Block transclusion | file | Transclusion-form rule; the generated index depends on …. |
| Frontmatter (Properties) | YAML between --- fences | Frontmatter rule keeps shapes schema-consistent so the Properties UI and validators agree. |
The formatter selection (T-0007 → D-0011-markdown-formatting-tool) verified each of these against candidate tools before adoption, including a byte-identical fixture round-trip for the Obsidian syntaxes.
Corpus hand-check
Section titled “Corpus hand-check”A hand-check during the definition task (T-0006) sampled 10 existing docs against the rules. Findings:
| Doc | Observation | Implication |
|---|---|---|
docs/CLAUDE.md | Fully conformant (max line 67, - bullets, no HTML, no trailing ws). | Short prose files already comply. |
D-0002-entity-identifier-shape.md | 33 lines >100, almost all table rows. | Confirms tables must be exempt or the rule is unenforceable. |
D-ORMG-data-model.md | One 1012-char line (a wide table row). | Same — table exemption is load-bearing. |
D-0007-deterministic-op-substrate.md | 55 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.md | 17 >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.md | 36 “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.md | 35 >100, wide tables. | Consistent pattern. |
D-Q2WR-task-pickup-order.md | 7 >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;
MD013wraps prose at 100 withheadings,code-blocks, andtablesall exempt. - HTML-vs-placeholder discrimination — the HTML prohibition stays
reviewer-enforced (
MD033disabled), 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.tomlinstead of a looser rule.
- Companion Standard: S-0007-markdown-formatting.
- Tool choice: D-0011-markdown-formatting-tool (via T-0007). Wiring / one-time reformat: T-0008.