diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index a5eb09c..4c0f98c 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -21,7 +21,6 @@ MD029: false # Ordered list item prefix MD033: false # Allow inline HTML MD036: false # Emphasis used instead of a heading MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception -MD051: false # Link fragments: docs use python-markdown attr_list anchors ({ #fr1 }) that markdownlint can't resolve; Test-DocumentationLink.ps1 validates fragments instead MD060: false # Table column style — content mixes compact and aligned tables ################# diff --git a/.github/scripts/Test-DocumentationLink.ps1 b/.github/scripts/Test-DocumentationLink.ps1 index 6e99417..6767f1a 100644 --- a/.github/scripts/Test-DocumentationLink.ps1 +++ b/.github/scripts/Test-DocumentationLink.ps1 @@ -14,7 +14,7 @@ - A heading anchor ('target.md#section', or a same-page '#section') must match a heading in the target file. Slugs are computed the same way the site's Markdown processor does, including the '_1', '_2' suffixes for duplicate - headings; an explicit attr_list id ('## Heading { #id }') is recognised as + headings; an explicit attr_list id ('## Heading {#id}') is recognised as the heading's anchor. External links (http, https, mailto, tel), absolute paths, links inside fenced @@ -80,7 +80,7 @@ function Get-HeadingSlug { .DESCRIPTION Return each heading's anchor, matching the duplicate-slug suffixing ('_1', '_2', ...) the Markdown processor applies to repeated headings. A - heading may also carry an explicit attr_list id ('## Heading { #id }'), + heading may also carry an explicit attr_list id ('## Heading {#id}'), which the site renderer uses as the anchor verbatim, overriding the text slug; those are recognised so links to '#id' validate. Fenced code blocks are skipped. @@ -106,7 +106,7 @@ function Get-HeadingSlug { if ($inFence) { continue } if ($line -match '^#{1,6}\s+(.+?)\s*$') { $text = $matches[1] - # An explicit attr_list id ('{ #id }' or '{: #id ... }') wins over + # An explicit attr_list id ('{#id}' or '{: #id ... }') wins over # the text slug, exactly as python-markdown's attr_list assigns it. if ($text -match '\{\s*:?\s*#([-\w]+)[^}]*\}\s*$') { $slugs.Add($matches[1]) diff --git a/src/docs/Capabilities/deployment/spec.md b/src/docs/Capabilities/deployment/spec.md index 12afc4f..acb5a65 100644 --- a/src/docs/Capabilities/deployment/spec.md +++ b/src/docs/Capabilities/deployment/spec.md @@ -79,85 +79,85 @@ Out of scope: Requirements use [BCP 14](https://www.rfc-editor.org/info/bcp14) keywords. -### FR1 — A change is expressed as code and proposed for review { #fr1 } +### FR1 — A change is expressed as code and proposed for review {#fr1} Every change to managed resources MUST be expressed as code and proposed for review before it is deployed. A change that reaches resources without review MUST be possible only through the emergency path ([FR8](#fr8)). -### FR2 — The effect of a change is computed and shown for review { #fr2 } +### FR2 — The effect of a change is computed and shown for review {#fr2} For each environment the change will pass through, the process MUST compute the change's **effect on the managed resources** — what it creates, updates, and destroys — and make every one of these effects available during review. Computing the effect successfully MUST be a condition of acceptance. -### FR3 — Approval covers the code change together with its effect { #fr3 } +### FR3 — Approval covers the code change together with its effect {#fr3} Acceptance MUST approve the code change **and** its computed effect on every environment in the promotion path, as one decision. Approving the code alone, without its effect, MUST NOT satisfy this requirement. -### FR4 — The approved effect is exactly what is deployed { #fr4 } +### FR4 — The approved effect is exactly what is deployed {#fr4} The deployment to each environment MUST carry out the exact effect that was approved for that environment. The process MUST NOT re-derive the effect between approval and deployment in a way that could act on resources no one approved. -### FR5 — Deployment fails closed when the approved effect is no longer valid { #fr5 } +### FR5 — Deployment fails closed when the approved effect is no longer valid {#fr5} If the approved effect for an environment is no longer valid for that environment's current state at deployment time, the deployment MUST stop without carrying out a divergent effect. Resolution MUST be to recompute the effect and have it approved again — never to force the invalid one. -### FR6 — Changes against the same resources are serialised { #fr6 } +### FR6 — Changes against the same resources are serialised {#fr6} At most one change MUST be deployed against a given set of resources at a time. Concurrent deployments against the same resources MUST be prevented, and a deployment in progress MUST NOT be interrupted partway. -### FR7 — The process runs its automated tests { #fr7 } +### FR7 — The process runs its automated tests {#fr7} The process MUST run its defined automated tests as part of carrying out a change, and a failing required test MUST stop the promotion before it reaches the next environment. -### FR8 — An audited emergency path exists { #fr8 } +### FR8 — An audited emergency path exists {#fr8} An emergency path MUST be able to deploy a change without the standard review. It MUST require a recorded reason and an incident reference, MUST be restricted to authorised people, MUST still compute and show the effect before it deploys, and MUST open a follow-up that brings the change back through the standard path. -### FR9 — Every deployment documents the actions it took { #fr9 } +### FR9 — Every deployment documents the actions it took {#fr9} Every deployment, standard or emergency, MUST record the actions it performed — who approved, who deployed, when, against which environment, the effect applied, and the outcome of its tests — retained as an immutable record. -### FR10 — Drift is detected and surfaced { #fr10 } +### FR10 — Drift is detected and surfaced {#fr10} Divergence between the real resources and the recorded state SHOULD be detected on an ongoing basis and surfaced for action. -### NFR1 — No standing secrets { #nfr1 } +### NFR1 — No standing secrets {#nfr1} Access to managed resources MUST use short-lived, federated identity. Zero long-lived credentials or static secrets exist in the repository or its automation. -### NFR2 — Least privilege separated by phase { #nfr2 } +### NFR2 — Least privilege separated by phase {#nfr2} Computing a change's effect MUST use read-only access; deploying it MUST use write access scoped to the target environment; the two MUST be distinct. -### NFR3 — Per-environment identity boundary { #nfr3 } +### NFR3 — Per-environment identity boundary {#nfr3} Each environment MUST have its own access identity, so an effect approved for one environment cannot be deployed to another. Zero cross-environment deployments. -### NFR4 — Complete auditability { #nfr4 } +### NFR4 — Complete auditability {#nfr4} 100% of deployments, including emergency ones, MUST produce a retained, attributable record ([FR9](#fr9)). diff --git a/src/docs/Coding-Standards/Markdown.md b/src/docs/Coding-Standards/Markdown.md index bb12aad..6bdff8e 100644 --- a/src/docs/Coding-Standards/Markdown.md +++ b/src/docs/Coding-Standards/Markdown.md @@ -31,9 +31,12 @@ These overrides and defaults are active, so author to them: | MD026 | Headings do not end with trailing punctuation (`. , ; : !`). | | MD046 | Code blocks are **fenced** (` ``` `), never indented. | | MD048 | Code fences use **backticks**, not tildes. | +| MD051 | Link fragments resolve — a `#anchor` must match a real heading. A heading's custom anchor is written `{#id}`, with no spaces inside the braces. | Beyond these the linter runs the default ruleset, so also honour its common defaults: headings tagged with a language on every code fence, no trailing whitespace, and a single trailing newline. +MD051 validates **same-file** fragments only. A cross-file fragment such as `spec.md#fr1` is never examined by the linter, so a clean lint run is not evidence that cross-file anchors resolve — a repository that relies on them needs its own link checker as well (in this repository, `.github/scripts/Test-DocumentationLink.ps1`). + ## Relaxed on purpose These rules are disabled or widened so they do not flag valid documentation — do not work around them: @@ -54,6 +57,7 @@ These rules are disabled or widened so they do not flag valid documentation — - **Surround headings, lists, and fenced blocks with a blank line** for readability, even though the linter no longer enforces it. - **Prefer relative links** within a repository; use the canonical published URL for cross-repository references. - **Give a repeated or long link a reference-style definition** (`[text][ref]`, with `[ref]: url` listed below) so the prose stays readable and one edit updates every use. +- **Write a heading's custom anchor as `{#id}`** when it must stay linkable under a stable identifier. No Markdown specification defines heading attributes — not [the original syntax](https://daringfireball.net/projects/markdown/syntax), not [CommonMark](https://spec.commonmark.org/), not [GFM](https://github.github.com/gfm/) — so the form belongs to a flavor rather than to Markdown itself: [`attr_list`](https://python-markdown.github.io/extensions/attr_list/), which the site enables and GitHub does not, which is why the braces become an `id` on the published page but show as literal text in a repository file view. `attr_list` accepts `{ #id }` and `{: #id }` as well, but markdownlint reads only the unspaced form — the others leave the heading on its slugified anchor, so same-page references to the identifier are reported as broken fragments and cross-file ones break silently. [Spec-Driven Development](../Ways-of-Working/Spec-Driven-Development.md#requirements) owns the FR/NFR identifiers themselves; the syntax applies to any page. - **Tag every code fence with a language** (` ```bash `, ` ```yaml `) so it is highlighted and converts cleanly when published. - **Wrap code, commands, filenames, and identifiers in backticks** rather than bold or italic, so they read as code and do not lean on the emphasis the linter now allows freely. - **Give every image descriptive alt text** — `![what the image shows](diagram.png)` — so it serves screen readers and still says something when the image fails to load; use a relative path for images kept in the repository. diff --git a/src/docs/Ways-of-Working/Spec-Driven-Development-Templates.md b/src/docs/Ways-of-Working/Spec-Driven-Development-Templates.md index c238245..dff9499 100644 --- a/src/docs/Ways-of-Working/Spec-Driven-Development-Templates.md +++ b/src/docs/Ways-of-Working/Spec-Driven-Development-Templates.md @@ -51,7 +51,7 @@ description: ## Functional requirements -### FR1 — { #fr1 } +### FR1 — {#fr1} #### Behavioral scenarios @@ -62,11 +62,11 @@ Scenario: Then ``` -### FR2 — <...> { #fr2 } +### FR2 — <...> {#fr2} ## Non-functional requirements -### NFR1 — { #nfr1 } +### NFR1 — {#nfr1} #### Behavioral scenarios @@ -138,7 +138,7 @@ description: ## Functional requirements -### FR1 — { #fr1 } +### FR1 — {#fr1} #### Behavioral scenarios @@ -151,7 +151,7 @@ Scenario: <...> ## Non-functional requirements -### NFR1 — { #nfr1 } +### NFR1 — {#nfr1} ```` ## Design diff --git a/src/docs/Ways-of-Working/Spec-Driven-Development.md b/src/docs/Ways-of-Working/Spec-Driven-Development.md index 60c1ef0..a4a50d6 100644 --- a/src/docs/Ways-of-Working/Spec-Driven-Development.md +++ b/src/docs/Ways-of-Working/Spec-Driven-Development.md @@ -106,7 +106,7 @@ Requirements are testable statements of what must be true — never how it is bu **Functional** requirements describe what the capability does, as observable behavior. **Non-functional** requirements are the quality attributes the capability must hold — performance, security, reliability, availability, compliance, observability, and cost — each stated as a measurable condition with a threshold; a non-functional requirement without a number is an opinion. For platform and infrastructure work these are often the point of the change rather than an afterthought — latency, redaction, retention, and blast radius decide whether the thing is fit to run. -Give each requirement its own heading with a stable, explicit anchor — `### FR1 — { #fr1 }` for functional, `### NFR1 — { #nfr1 }` for non-functional. The anchor is the identifier alone, so the heading can be reworded without breaking a single reference. Identifiers are **append-only**: assign the next unused number, never renumber, and never reuse — a removed requirement simply disappears, and git holds the history. +Give each requirement its own heading with a stable, explicit anchor — `### FR1 — {#fr1}` for functional, `### NFR1 — {#nfr1}` for non-functional. The anchor is the identifier alone, so the heading can be reworded without breaking a single reference. Identifiers are **append-only**: assign the next unused number, never renumber, and never reuse — a removed requirement simply disappears, and git holds the history. Numbering is scoped **per page**. Every spec page — a core spec and each of its [feature addenda](#core-and-feature-addenda) — starts at `FR1` and `NFR1`. Identity is the page plus the anchor, so a requirement is referenced as `[FR1](#fr1)` on the same page and `[FR1](spec.md#fr1)` across pages; a feature reference has the form `features/.md#fr2`. Because identity includes the page, moving a set of requirements onto a new feature page never forces a renumber. @@ -115,7 +115,7 @@ Numbering is scoped **per page**. Every spec page — a core spec and each of it Each requirement owns a `#### Behavioral scenarios` subsection immediately beneath it, holding one or two Given / When / Then scenarios that make that one requirement concrete in place. These are the acceptance criteria for that requirement, stated once, where the requirement is stated. ````markdown -### FR1 — A request MUST succeed while any healthy upstream provider remains { #fr1 } +### FR1 — A request MUST succeed while any healthy upstream provider remains {#fr1} #### Behavioral scenarios