Skip to content

📖 [Docs]: Specifications written from the template now pass the Markdown linter - #144

Merged
Marius Storhaug (MariusStorhaug) merged 8 commits into
mainfrom
docs-141-anchor-braces
Aug 9, 2026
Merged

📖 [Docs]: Specifications written from the template now pass the Markdown linter#144
Marius Storhaug (MariusStorhaug) merged 8 commits into
mainfrom
docs-141-anchor-braces

Conversation

@MariusStorhaug

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented Aug 2, 2026

Copy link
Copy Markdown
Member

Requirement anchors in specifications now use the one form that both the documentation site and the Markdown linter understand, so a specification written by copying the template out of Spec-Driven Development Templates lints clean with no hand-editing afterwards — in this repository and in any repository that inherits the standard without inheriting this one's linter configuration.

Fixed: References to requirements no longer report as broken links

The Requirements section tells authors to give each requirement an explicit anchor and to reference it as [FR1](#fr1). Written with spaces inside the braces, that anchor is invisible to markdownlint rule MD051: the heading keeps its slugified anchor instead, and same-page references to the identifier resolve to nothing. Following the page therefore produced a document that failed the linter the ecosystem runs in CI.

Anchors are now written without the inner spaces:

### FR1 — <what the capability does, behavioral, testable, no technology> {#fr1}

Nothing else about the form changes. The anchor is still the identifier alone, still append-only, and still referenced as [FR1](#fr1) on the same page and [FR1](spec.md#fr1) across pages. Both brace forms render identically under Python-Markdown's attr_list, which this site enables, so the published pages are byte-for-byte the same as before.

The specification that still used the spaced form has been converted, along with every skeleton in the templates page, so what an author copies matches the rule the standard now enforces.

Changed: A reference to a heading that does not exist now fails the build

MD051 was switched off in this repository precisely because it could not read the spaced anchors, with a script covering the gap instead. With the anchors converted, the rule is on again and a link to a heading that does not exist fails the lint job like any other error. Contributors get that caught in CI rather than discovering it as a dead link on the published site.

Changed: The Markdown standard now documents the anchor form and the rule that enforces it

The anchor syntax previously appeared only in Spec-Driven Development, framed as a convention for specifications — which is how the broken form came to be copied into pages and repositories that had nothing to do with specifications. It is now written down in the Markdown standard as a rule for any heading on any page, together with the limitation worth knowing: the linter checks same-file fragments only, so a clean lint run does not prove that a cross-file spec.md#fr1 link resolves. Custom heading anchors are not defined by original Markdown, CommonMark, or GFM; this site gets {#id} from its enabled Python-Markdown attr_list extension, so the braces render literally in GitHub's file view but become the heading ID on the published site.


Technical details

Six commits, each independently reviewable:

  1. 31d41cd — the anchors. Mechanical conversion of { #id } to {#id}, 16 occurrences over 2 files — the Spec-Driven-Development prose that introduces the form (2, on one line) and deployment/spec.md (14, FR1–FR10 and NFR1–NFR4). No rewording, no restructuring, no reflowing.

  2. 7b21fee — the linter disable that the spaced form caused. MD051: false removed from .github/linters/.markdown-lint.yml. Gated, not assumed: the line was removed first and markdownlint-cli2 v0.23.2 (markdownlint v0.41.1) run with that real configuration over every tracked Markdown file, reporting 0 issues. Clean, so the removal stands. Had it surfaced unrelated findings, the disable would have been restored with a corrected comment and the findings tracked separately rather than fixed in passing.

  3. a2c9a0d — the checker's own help. The three { #id } examples in the comment-based help and parsing comment of .github/scripts/Test-DocumentationLink.ps1 now show {#id}. Examples only; the matching expression is untouched. The colon-prefixed {: #id ... } in the same comment is left as it was — the expression genuinely tolerates that variant, so converting it would have made the help wrong.

  4. f5fa802 — the standard that should have carried the rule. src/docs/Coding-Standards/Markdown.md states that the shared configuration is the source of truth and then enumerates the rules in two tables; MD051 was in neither. It had been disabled in the config and never recorded, so the page was already out of step before this branch. Commit 2 made that omission live — the rule now fails the build for every page, not just specifications — so the page gains an MD051 row in Enforced rules, the anchor syntax as one bullet in Style beyond the linter, and the same-file limitation stated once. Verified MD051 is not present in Relaxed on purpose; it was not, so nothing was removed.

  5. 1a7c25c — the anchors that arrived while this branch waited. MSXOrg/docs#160 moved the skeletons out to Spec-Driven-Development-Templates.md and added a worked requirement example, both written in the spaced form — which was still what the standard taught, and which nothing would have caught while MD051 was disabled. Six anchors converted: 5 in the new templates page, 1 in the new example. All six sit inside fenced code blocks, so neither markdownlint nor Test-DocumentationLink.ps1 examines them and no check was failing; they matter because a skeleton is the text an author copies, and that is the vector that carried the broken form into 📖 [Docs]: Markdown object model specified as a section tree PSModule/Markdown#33.

  6. 9a66332 — whose syntax this actually is. The page opens by saying documentation is authored in GitHub Flavored Markdown, and commit 4 then made {#id} an enforced rule — but GFM defines no attribute syntax, and neither does CommonMark nor Gruber's original. The construct belongs to individual flavors: PHP Markdown Extra and Python-Markdown, kramdown's inline attribute lists, Pandoc. This site gets it from attr_list. Verified against the renderers rather than their documentation — GitHub's POST /markdown with mode=gfm returns <h3>FR1 {#fr1}</h3>, and the same literal output for { #fr1 } and {: #fr1 }, while Python-Markdown 3.10.2 with attr_list turns all three into <h3 id="fr1">. The rule now says so where it is stated, so an author who sees braces in a repository file view knows that is expected rather than a mistake, and knows {#id} wins as an intersection of implementations rather than by any specification.
    Rebased twice while waiting for review, each time onto current main with no content from the incoming work reverted:

  • MSXOrg/docs#158 retired the process-psmodule pages in favour of that capability's canonical site, deleting a file this branch had edited. A modify/delete conflict, resolved by keeping the deletion — the 11 anchors converted there went with the page.
  • MSXOrg/docs#160 rewrote Spec-Driven-Development and replaced its inline templates with a link to the new templates page. A content conflict, resolved by taking main's prose in full; the template conversions this branch used to carry now apply to the new page instead, in commit 5.

How much MD051 was actually catching. MD051 validates same-file fragments only, and skips fenced code blocks entirely. Across the spaced anchors on this branch, exactly 2 references were being reported — [FR8](#fr8) and [FR9](#fr9), both in deployment/spec.md. Cross-file references of the spec.md#fr1 form were never checked by MD051 at all and depend on Test-DocumentationLink.ps1, which is why that script remains the broader of the two checks and stays in CI. This is the measurement that makes removing the disable safe: there were only ever two findings to clear, and they are cleared. The reach of the defect was in what the templates taught downstream repositories, not in the volume of errors produced here.

Why {#id} and not {: #id }. attr_list accepts {#fr1}, { #fr1 } and {: #fr1 }; markdownlint understands only the unspaced one, so it is the single form that satisfies the renderer and CI at once. Test-DocumentationLink.ps1 matches with \{\s*:?\s*#([-\w]+)[^}]*\}\s*$, where \s* permits zero spaces, so the converted anchors resolve under the existing checker with no change to it.

Verification after the second rebase, using only tooling the repository already provides:

  • Test-DocumentationLink.ps1All documentation links resolve (119 file(s) scanned), exit 0.
  • Update-DocumentationIndex.ps1 -Check — exit 0, no diff.
  • markdownlint-cli2 with .github/linters/.markdown-lint.yml, MD051 now enabled, across all 128 tracked Markdown files — 0 issues.
  • Invoke-PesterSuite.ps1 — 54 passed, 0 failed, across 4 suites.

Implementation plan progress — all five steps of #141 are delivered here: the documentation conversions, the linter-configuration removal with its gate, and the comment-based help examples. The plan's process-psmodule/spec.md step is satisfied by that file's removal in #158. Commits 4 and 5 are beyond the issue's plan and close drift the plan itself would otherwise have left behind. Nothing is deferred to a follow-up.

Issue convergence sweep — scope was every open issue in MSXOrg/docs. #143 (naming the downstream artifacts a standard governs, so changing it has a known blast radius) is the closest match, and commit 4 is an instance of exactly that concern rather than a resolution of it — the general mechanism it asks for is not delivered here, so it takes no closing keyword. #142 (cross-repository links the Markdown standard tells authors to write) and #105 (co-locating Gherkin acceptance tests with the FR/NFR they verify) concern different surfaces. No additional issue is fully satisfied.

Changed surface Standards checked Framework docs checked Result
src/docs/** (Markdown) Markdown Documentation Model, Spec-Driven Development Aligned
.github/linters/** (linter configuration) Markdown Repository Standard Fixed in this PR — the configuration and the standard documenting it now agree
.github/scripts/** (PowerShell) PowerShell — Scripts, Documentation Repository Standard Aligned
Relevant issues (or links)

Related work

markdownlint's MD051 recognises a custom heading anchor only when the
braces contain no surrounding whitespace. With the spaced form the
heading keeps its slugified anchor, so same-page references such as
[FR1](#fr1) -- the form Spec-Driven-Development.md tells authors to
use -- are reported as broken link fragments, and a spec written by
following that page fails the linter the ecosystem runs in CI.

Convert 16 occurrences: 2 in the Spec-Driven-Development.md prose that
introduces the form, and 14 in deployment/spec.md. Both forms render
identically under Python-Markdown's attr_list, which this site enables,
so the published output is unchanged. Test-DocumentationLink.ps1
matches the id with \s* around the brace contents, so it resolves
either form.

Two pages this commit used to touch have moved on: #158 retired
process-psmodule/spec.md with the rest of that capability's pages, and
#160 moved the specification templates out to
Spec-Driven-Development-Templates.md. The anchors that arrived with
#160, there and in its new worked example, are converted in the
commit that follows this one.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The rule was disabled because the documentation wrote attr_list anchors
with spaces inside the braces, a form markdownlint cannot resolve, and
Test-DocumentationLink.ps1 covered the gap instead. The anchors are now
unspaced, so the stated reason no longer holds.

MD051 is a genuine correctness check: it catches references to headings
that do not exist, which is the failure mode append-only requirement
identifiers exist to prevent. Removing the disable and running
markdownlint-cli2 v0.23.2 with this configuration over all 123 tracked
Markdown files reports 0 issues, so the check is kept on rather than
left disabled against a justification that no longer applies.

Test-DocumentationLink.ps1 still runs and still validates fragments
markdownlint does not see -- MD051 checks same-file fragments only, so
cross-file references such as spec.md#fr1 continue to depend on it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The comment-based help and the parsing comment illustrated an explicit
attr_list id as '## Heading { #id }', the spaced form the documentation
no longer uses. The script's help is where a reader learns the anchor
form while working on the checker itself, so leaving it kept teaching
the form being removed.

Only the examples change. The matching expression is untouched and still
resolves every variant it did before, including the colon-prefixed
'{: #id ... }' the comment continues to name.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The page claims the shared configuration is the source of truth and then
enumerates the rules, but MD051 appeared in neither table. It had been
disabled in .github/linters/.markdown-lint.yml and never written down,
so the standard was already out of step with the configuration; enabling
the rule in this branch made the omission a live one, because a link to
a heading that does not exist now fails the build for every page rather
than only for specifications.

Add MD051 to the enforced rules, state the anchor syntax once in the
style section, and record the limitation that matters: MD051 validates
same-file fragments only, so a clean lint run is not evidence that a
cross-file fragment resolves.

The anchor syntax belongs here rather than only in
Spec-Driven-Development.md. That page owns the FR/NFR identifiers; the
brace form applies to any heading on any page, and keeping it in a page
about specifications is how the spaced form spread in the first place.
The two pages cross-link instead of restating each other.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
#160 moved the specification skeletons out to
Spec-Driven-Development-Templates.md and added a worked requirement
example to Spec-Driven-Development.md. Both were written with the
spaced brace form, which was still what the standard taught at the
time and which nothing in CI would have caught, since MD051 was
disabled.

These six anchors sit inside fenced code blocks, so neither markdownlint
nor Test-DocumentationLink.ps1 examines them and no check was failing.
They matter because they are the text an author copies: the skeleton is
the vector that carried the broken form into PSModule/Markdown#33 in
the first place. A template that disagrees with the rule the same
standard now enforces is the defect this branch exists to remove.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The page opens by saying documentation is authored in GitHub Flavored
Markdown, and then, since the previous commit, requires custom heading
anchors written as {#id}. Those two statements disagree, and the reason
is broader than GFM: no Markdown specification defines heading
attributes at all. Not Gruber's original syntax, not CommonMark, not
GFM. The construct belongs to individual flavors -- PHP Markdown Extra
and Python-Markdown, kramdown's inline attribute lists, Pandoc -- and
this site gets it from Python-Markdown's attr_list extension.

Verified against the renderers rather than the documentation. GitHub's
own POST /markdown with mode gfm returns <h3>FR1 {#fr1}</h3>, and the
same literal output for { #fr1 } and {: #fr1 }. Python-Markdown 3.10.2
with attr_list, which is what Zensical publishes with, turns all three
into <h3 id="fr1">.

Name that where the rule is stated, so an author who sees braces in the
repository file view knows it is expected rather than a mistake, and
knows the rule is a property of this toolchain rather than something a
specification settles. {#id} remains the form to write: it is the only
one markdownlint parses, which is an intersection of implementations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 9, 2026 16:57
@MariusStorhaug Marius Storhaug (MariusStorhaug) added release:none No release and removed NoRelease No release required labels Aug 9, 2026
@MariusStorhaug
Marius Storhaug (MariusStorhaug) merged commit d82802e into main Aug 9, 2026
20 checks passed
@MariusStorhaug
Marius Storhaug (MariusStorhaug) deleted the docs-141-anchor-braces branch August 9, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:none No release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use unspaced requirement anchors in the specification template

1 participant