Repository navigation
Use unspaced requirement anchors in the specification template #141
Description
Activity
- added a commit that references this issue
on Aug 2, 2026 MariusStorhaug commented
on Aug 2, 2026 MemberAuthorMore actionsCorrected the description. Two claims in the original were wrong, both from insufficient checking on my part.
- "No specification here uses the FR/NFR anchor format" — false.
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. - "This has not surfaced in this repository yet" — false, and it inverts the situation.
.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:
- Rewrote the context to state the workaround and why it does not travel: a repository generated from the ecosystem template inherits the anchor form but not this repository's linter config or its link checker, so it gets the broken references with none of the compensations.
- Reversed the "change the documentation, not the linter configuration" decision. The disable exists because of the spaced form, so leaving it in place after converting the anchors would keep a disabled correctness check with a comment that has become false. Re-enabling is now attempted and kept only if the repository is clean; unrelated findings restore the disable and get tracked separately rather than fixed in passing.
- Added the
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. - Widened scope to all 31 occurrences plus the
{ #id }examples inTest-DocumentationLink.ps1's comment-based help, which would otherwise keep teaching the form being removed. - Replaced the implementation plan with per-file counts and a gated step for the linter change.
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.- "No specification here uses the FR/NFR anchor format" — false.
MariusStorhaug commented
on Aug 2, 2026 MemberAuthorMore actionsCorrected 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.
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 indeployment/spec.md. Cross-file references of the form[FR1](spec.md#fr1)are never examined by markdownlint at all and depend entirely onTest-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-cli2v0.23.2 (markdownlint v0.41.1):The unspaced anchor passes; the spaced one does not.
This repository already hit the problem and worked around it.
.github/linters/.markdown-lint.ymlsetsMD051: 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.mdhas 14 anchors andprocess-psmodule/spec.mdhas 11, alongside the 6 on this page. Older specifications such as merge-automation predate the format and are unaffected.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
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_listaccepts{#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.ps1matches 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 inTest-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
src/docs/Ways-of-Working/Spec-Driven-Development.md— the Requirements prose and the specification templatesrc/docs/Capabilities/deployment/spec.mdConvert 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.github/scripts/Test-DocumentationLink.ps1Linter configuration
MD051: falsefrom.github/linters/.markdown-lint.ymland run the linter across the repositoryVerification
Test-DocumentationLink.ps1resolves every converted anchormarkdownlint-cli2is clean across the repository