Repository navigation
Use unspaced requirement anchors in the specification template #141
Description
Specifications written from the template in Spec-Driven-Development.md fail the Markdown linter that the ecosystem runs in CI.
Request
What is confusing or missing
The Requirements section instructs authors to give each requirement an explicit anchor written as ### FR1 — <statement> { #fr1 }, and both templates at the bottom of the page repeat that spaced form. An author who follows the page produces a specification whose in-page references — [FR1](#fr1), the form the same section prescribes — are reported as broken links by markdownlint rule MD051.
MD051 recognises a custom heading anchor only when the braces contain no surrounding whitespace. The spaced form is invisible to it, so the heading keeps its slugified anchor and a same-page reference to the identifier resolves to nothing.
MD051 checks same-file fragments only, so the error volume is small and the reach is not. Across the 31 spaced anchors in this repository, exactly two references were being reported — [FR8](#fr8) and [FR9](#fr9), both in deployment/spec.md. Cross-file references of the form [FR1](spec.md#fr1) are never examined by markdownlint at all and depend entirely on Test-DocumentationLink.ps1, which is why that script remains the broader check. The defect is not the two findings; it is that the standard teaches a form which fails wherever the local workaround is absent.
Reproduced against markdownlint-cli2 v0.23.2 (markdownlint v0.41.1):
### FR1 — spaced anchor { #fr1 }
### FR2 — unspaced anchor {#fr2}
Link to [FR1](#fr1) and [FR2](#fr2).test.md:9:9 error MD051/link-fragments Link fragments should be valid [Context: "[FR1](#fr1)"]
The unspaced anchor passes; the spaced one does not.
This repository already hit the problem and worked around it. .github/linters/.markdown-lint.yml sets MD051: false, with a comment that cites the spaced form directly: "docs use python-markdown attr_list anchors ({ #fr1 }) that markdownlint can't resolve; Test-DocumentationLink.ps1 validates fragments instead." The rule is disabled here, and a repository-specific script covers the gap.
That workaround does not travel. A repository generated from the ecosystem's template inherits the anchor form — through this page — but not this repository's linter configuration and not Test-DocumentationLink.ps1. It gets the broken references with none of the compensations. That is exactly what happened in PSModule/Markdown#33, where sixteen anchors and four references had to be rewritten by hand to get a green lint run.
The spaced form is in use here: deployment/spec.md has 14 anchors and process-psmodule/spec.md has 11, alongside the 6 on this page. Older specifications such as merge-automation predate the format and are unaffected.
Update, 9 August: #158 retired the entire
process-psmodule/directory in favour of the capability's canonical site, so those 11 anchors no longer exist. 20 remain to convert, across two files.
What is expected
Following the template produces a document that passes the linters the ecosystem already runs, without each repository having to disable a rule or supply its own checker. Both forms render identically under Python-Markdown's attr_list, which is what the documentation site uses, so the unspaced form costs nothing and satisfies both tools.
Acceptance criteria
- The anchor form the Requirements section prescribes passes MD051.
- The specification and design templates use the same form as the prose that introduces it.
- A specification written by copying the template lints clean with no author intervention, in this repository and in any repository that inherits the standard.
MD051: falseand its rationale are revisited, since the reason the comment gives no longer holds once the anchors change.Test-DocumentationLink.ps1continues to resolve the converted anchors.
Technical decisions
Change the documentation, and revisit the linter disable it caused: MD051 is a genuine correctness check — it catches references to headings that do not exist, which is exactly the failure mode append-only requirement identifiers are meant to prevent. It is currently disabled here specifically because of the spaced form, so once the anchors change, the stated reason for the disable is gone. Re-enabling is attempted and kept only if the repository is clean; if it surfaces unrelated findings, the disable stays and those are tracked separately rather than fixed in passing.
Unspaced, not a colon prefix: attr_list accepts {#fr1}, { #fr1 }, and {: #fr1 }. Only {#fr1} is also understood by markdownlint, so it is the one form that works in both the rendered site and CI.
The compensating control keeps working: Test-DocumentationLink.ps1 matches anchors with \{\s*:?\s*#([-\w]+)[^}]*\}\s*$. The \s* permits zero spaces, so the unspaced form resolves under the existing checker with no change to it.
Scope: every occurrence of the spaced form, not just this page — 31 across src/, plus the examples in Test-DocumentationLink.ps1's comment-based help, which otherwise keep teaching the form being removed. A template that disagrees with its own prose is how this reached a downstream repository in the first place; tooling help that disagrees with the standard is the same defect one layer down.
Implementation plan
Documentation
- Convert the 6 occurrences in
src/docs/Ways-of-Working/Spec-Driven-Development.md— the Requirements prose and the specification template - Convert the 14 occurrences in
src/docs/Capabilities/deployment/spec.md -
Convert the 11 occurrences in— moot: #158 retired the wholesrc/docs/Capabilities/process-psmodule/spec.mdprocess-psmodule/directory in favour of its canonical site, so the file and its anchors no longer exist - Convert the examples in the comment-based help of
.github/scripts/Test-DocumentationLink.ps1
Linter configuration
- Remove
MD051: falsefrom.github/linters/.markdown-lint.ymland run the linter across the repository - Keep the removal if clean; if unrelated findings surface, restore the disable, correct its now-stale comment, and track the findings separately
Verification
- Confirm
Test-DocumentationLink.ps1resolves every converted anchor - Confirm
markdownlint-cli2is clean across the repository - Confirm the documentation index script produces no diff
- Confirm a heading with an unspaced anchor still renders with that anchor on the built site
Activity
📖 [Docs]: Markdown object model specified as a section tree (#33)
MariusStorhaug commentedon Aug 2, 2026
Corrected the description. Two claims in the original were wrong, both from insufficient checking on my part.
deployment/spec.mdhas 14 spaced anchors andprocess-psmodule/spec.mdhas 11, on top of the 6 on the standard page. I checked one spec file,merge-automation/spec.md, found none, and generalised from a single sample. Total is 31 acrosssrc/, not 6..github/linters/.markdown-lint.ymlsetsMD051: false, with a comment naming the spaced form as the reason andTest-DocumentationLink.ps1as the compensating control. This repository diagnosed the problem and made a deliberate, documented decision about it. The original description implied the problem was unnoticed here.Both were found by the session implementing the fix, checking the repository rather than trusting the issue.
What changed as a result:
Test-DocumentationLink.ps1anchor regex,\{\s*:?\s*#([-\w]+)[^}]*\}\s*$, and the observation that\s*permits zero spaces, so the existing checker resolves the converted form with no change to it.{ #id }examples inTest-DocumentationLink.ps1's comment-based help, which would otherwise keep teaching the form being removed.The core request is unchanged: the spaced form fails MD051, the unspaced form satisfies both markdownlint and
attr_list, and the standard should prescribe the one that works everywhere.MariusStorhaug commentedon Aug 2, 2026
Corrected an overstatement, flagged by the session implementing the fix. Since this issue closes with the pull request, it becomes the durable record and should be accurate.
The description said the spaced form means "every reference to the identifier resolves to nothing". Measured, that is wrong in scale. MD051 validates same-file fragments only, so across the 31 spaced anchors in this repository exactly two references were being reported —
[FR8](#fr8)and[FR9](#fr9), both indeployment/spec.md. Cross-file references of the form[FR1](spec.md#fr1)are never examined by markdownlint and rely entirely onTest-DocumentationLink.ps1.The measurement came from linting the pre- and post-change files against the repository's own config with only the
MD051line dropped: 2 issues before, 0 after.That does not change the decision, but it changes the argument for it, and the corrected version is the stronger one. The problem was never error volume in this repository — two findings, sitting behind a disabled rule, hurt nobody here. The problem is that the standard teaches an anchor form that fails wherever the local workaround is absent, and the workaround does not travel: a repository generated from the ecosystem template inherits the form but not this repository's linter config and not its link checker. PSModule/Markdown#33 is that case, and it is where the cost actually landed.
It also explains why removing
MD051: falseis safe rather than merely plausible — there were only ever two findings to clear, and they are cleared.Both corrections to this issue came from the implementation checking the repository instead of trusting the issue's framing. That is the right direction of travel: the ticket is a hypothesis until someone measures it.