📖 [Docs]: Markdown object model specified as a section tree - #33
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Drop the front matter title so the H1 is the only document title, tighten the requirement anchors to the form markdownlint recognises, and capitalise Markdown in prose. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
No Significant Changes DetectedThis PR does not contain changes to files that would trigger a new release:
Build, test, and publish stages will be skipped for this PR. If you believe this is incorrect, please verify that your changes are in the correct locations. |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
markdownlint's default MD025 pattern counts a front matter title as a top-level heading, so a capability document could not carry both. Configure front_matter_title as empty and restore the title the mkdocs navigation reads. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
No Significant Changes DetectedThis PR does not contain changes to files that would trigger a new release:
Build, test, and publish stages will be skipped for this PR. If you believe this is incorrect, please verify that your changes are in the correct locations. |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
No Significant Changes DetectedThis PR does not contain changes to files that would trigger a new release:
Build, test, and publish stages will be skipped for this PR. If you believe this is incorrect, please verify that your changes are in the correct locations. |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
…and a body heading (#39) Markdown pages in repositories generated from this template can now carry both a front matter `title:` and a body `# Heading` without the linter objecting. MkDocs keeps using the front matter title for navigation and the page title, and the body heading keeps rendering on github.com, where front matter is shown as a table rather than a title. ## Fixed: A front matter title is no longer counted as a second top-level heading The markdown linter shipped with the template applied markdownlint's default `front_matter_title` pattern, so a front matter `title:` was treated as the document's top-level heading. Any page that also opened with `# Heading` was reported as `MD025/single-title/single-h1 Multiple top-level headings in the same document`, and the author was pushed toward dropping one of the two — losing either correct site navigation or a readable page on GitHub. Nothing needs to change in your repository. Repositories generated from the template inherit the fix, and existing repositories pick it up the next time they take the template's linter configuration. MD025 still does its job: a page with two body `# Heading` lines is reported exactly as before. --- <details> <summary>Technical details</summary> `.github/linters/.markdown-lint.yml` gains one block in the "Rules by id" section, in numeric order between `MD024` and `MD026`: ```yaml MD025: front_matter_title: '' # Allow a body H1 alongside the front matter title ``` - **Empty pattern rather than `MD025: false`.** `frontMatterHasTitle()` in markdownlint treats a defined-but-falsy pattern as "ignore front matter" while leaving the rest of the rule active, so a genuinely duplicated body `# Heading` is still reported. Disabling the rule outright would stop catching that too, which is a real defect rather than a formatting preference. - **Converges with MSXOrg/docs**, which carries `front_matter_title: ""` with the same comment in its copy of this file. Single quotes are used here to match the one existing quoted value in this file (`punctuation: '.,;:!。,;:'`); `''` and `""` are the same empty YAML scalar, and markdownlint sees an identical value either way. - **No other rule was touched.** The wider drift from MSXOrg/docs — `MD013` line length (808 vs 3000), `MD041`, `MD051`, `MD060` — is visible but deliberately out of scope. - `PSModule/Markdown` carries a byte-identical copy of this file and hit this exact problem in PSModule/Markdown#33, where `title:` had to be stripped from three documentation files to get a green lint run. Fixing the template is what stops that repeating in every module repository. **Verification.** Throwaway fixtures outside the repository, ephemeral `npx`, nothing added to the repo. Run against both `markdownlint-cli@0.45.0` (the CLI super-linter drives) and `markdownlint-cli2@0.23.2` (markdownlint v0.41.1); the two agree on every case. | Case | Config | Result | | --- | --- | --- | | Front matter `title:` + body `# Heading` | before | `MD025` reported at the body heading — reproduces the bug | | Front matter `title:` + body `# Heading` | after | Clean, exit 0 | | Two body `# Heading` lines | after | `MD025` still reported at the second heading | | The repository's own 8 Markdown files | after | Clean, exit 0 | Implementation plan progress: both plan sections in #38 — configuration and verification — are complete. Nothing deferred. | Changed surface | Standards checked | Framework docs checked | Result | | --- | --- | --- | --- | | `.github/linters/**` (linter configuration) | MSXOrg Coding Standards | Repository Defaults | Aligned | Issue convergence sweep: the scope was every open issue in `PSModule/Template-PSModule`. #38 is the only one, and it is the closing issue for this pull request. </details> <details> <summary>Relevant issues (or links)</summary> - Fixes #38 </details> 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>
…own linter (#144) 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](https://msxorg.github.io/docs/Ways-of-Working/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](https://github.com/DavidAnson/markdownlint) rule [MD051](https://github.com/DavidAnson/markdownlint/blob/main/doc/md051.md): 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: ```markdown ### 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`](https://python-markdown.github.io/extensions/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](https://msxorg.github.io/docs/Coding-Standards/Markdown/) 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. --- <details> <summary>Technical details</summary> 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.** [#160](#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 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: - [#158](#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. - [#160](#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.ps1` — `All 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 | </details> <details> <summary>Relevant issues (or links)</summary> - Resolves #141 ### Related work - References #143 - References #158 - References #160 - References PSModule/Markdown#33 </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Markdown module now has a written, normative contract for the object model a Markdown document parses into — and that model is organised the way a document actually reads: a document holds a tree of sections, and a section owns its heading, its own content, and the sections nested inside it, recursively. Nothing in the module behaves differently yet. This is documentation of the intended state, published before the code exists so that anyone who will depend on the model can read it, disagree with it, and shape it while that is still free.
New: The Markdown object model is documented as a capability
docs/markdown-object-model/holds the capability in three documents side by side —index.mdintroduces it and shows the shape at a glance,spec.mdstates why the model exists and what it must do, anddesign.mdexplains how it is built. The spec is normative and testable: thirteen functional requirements and three non-functional ones, each individually addressable, acceptance criteria written as scenarios, and CommonMark conformance measured against the specification's own published example set rather than against hand-picked samples.New: Sections are the unit the model is built around
The spec settles the questions that decide what working with a parsed document feels like. A section owns the content that follows its heading up to the next heading of the same or a lower level. A section with no subsections is the same type as one that has them, holding an empty collection — there is no separate leaf type to branch on. The document is the same kind of container as a section, minus a heading and plus the metadata part, so content before the first heading belongs to the document and the same code walks both. A heading inside a block quote or a list item sections that container and leaves the document's outline alone. A skipped heading level nests without inventing a section that is not in the document, and every heading re-renders at its original level.
New: What the model will not do is stated as plainly as what it will
Byte-exact round-tripping is a non-goal — the model preserves the stylistic choices a reader would notice and normalises the rest. Rendering to formats other than Markdown is a non-goal, because the model is plain data that any general-purpose serializer can already take somewhere else. Replacing the composition DSL is a non-goal:
Set-Markdown*stays the way Markdown is written from nothing, and the object model is how existing Markdown is read and changed. The existing DSL keeps working unchanged, as a requirement rather than a promise.Technical details
docs/markdown-object-model/spec.mdanddocs/markdown-object-model/index.md).design.mdis added alongside them because the Documentation Model pairs a spec with a design in the same capability folder. The Model, Parser and renderer, Tests, and Documentation tasks in that plan remain open..github/linters/.markdown-lint.ymlgains anMD025entry withfront_matter_title: "". markdownlint's default pattern counts a front mattertitleas a top-level heading, so a document could not carry both the title mkdocs navigation reads and theH1GitHub renders. The setting is tightly coupled to these documents — the capability folder cannot exist without it — so it lands here rather than separately. The identical gap in the inherited baseline is tracked as Allow a body heading alongside the front matter title in the markdown linter config Template-PSModule#38..github/linters/required it: the requirement anchors were tightened from{ #fr1 }to{#fr1}, which is the form MD051 recognises and Python-Markdown'sattr_liststill accepts, andmarkdownwas capitalised in prose to match theMarkdownterminology term in.textlintrcand the existing README. The Specification template these documents follow still shows the spaced form, tracked as Use unspaced requirement anchors in the specification template MSXOrg/docs#141, so these files are ahead of the standard rather than departing from it. Verified locally withmarkdownlint-cli2andtextlintrun against those exact config files; both are clean.docs/folder is not published to GitHub Pages today: the site is assembled from generated function help,src/en-USabout topics,README.md, and the icon, andStructure-Site/src/main.ps1copies nothing else. These pages are therefore read in the repository, and the{#fr1}anchors stay inert until mkdocs renders them. Tracked as Publish hand-authored documentation from a repository's docs folder Process-PSModule#437.NoReleaselabel entirely, even though.github/release.ymlalready excludesNoReleasefrom generated release notes — so that exclusion could never fire, and every documentation and maintenance pull request has been landing under "Other Changes". A sweep of the organization found 58 repositories referencingNoReleaseinrelease.ymland this one as the only one without the label. The label now exists and is applied here alongsidedocumentation, which makes both correct. The change also touches no important file pattern (^src/,^README\.md$), so no release is produced either way.docs/**(Markdown).github/linters/.markdown-lint.yml,.github/linters/.textlintrc.github/linters/**Relevant issues (or links)