Skip to content
1 change: 0 additions & 1 deletion .github/linters/.markdown-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

#################
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/Test-DocumentationLink.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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])
Expand Down
28 changes: 14 additions & 14 deletions src/docs/Capabilities/deployment/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
4 changes: 4 additions & 0 deletions src/docs/Coding-Standards/Markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions src/docs/Ways-of-Working/Spec-Driven-Development-Templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ description: <One line: what this capability guarantees.>

## Functional requirements

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

#### Behavioral scenarios

Expand All @@ -62,11 +62,11 @@ Scenario: <the case that makes FR1 concrete>
Then <observable result>
```

### FR2 β€” <...> { #fr2 }
### FR2 β€” <...> {#fr2}

## Non-functional requirements

### NFR1 β€” <a quality attribute as a measurable condition: latency, availability, redaction, retention, cost> { #nfr1 }
### NFR1 β€” <a quality attribute as a measurable condition: latency, availability, redaction, retention, cost> {#nfr1}

#### Behavioral scenarios

Expand Down Expand Up @@ -138,7 +138,7 @@ description: <One line: what this feature adds to the capability.>

## Functional requirements

### FR1 β€” <feature-specific behavior> { #fr1 }
### FR1 β€” <feature-specific behavior> {#fr1}

#### Behavioral scenarios

Expand All @@ -151,7 +151,7 @@ Scenario: <...>

## Non-functional requirements

### NFR1 β€” <feature-specific quality attribute, measurable> { #nfr1 }
### NFR1 β€” <feature-specific quality attribute, measurable> {#nfr1}
````

## Design
Expand Down
4 changes: 2 additions & 2 deletions src/docs/Ways-of-Working/Spec-Driven-Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 β€” <statement> { #fr1 }` for functional, `### NFR1 β€” <statement> { #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 β€” <statement> {#fr1}` for functional, `### NFR1 β€” <statement> {#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/<feature-name>.md#fr2`. Because identity includes the page, moving a set of requirements onto a new feature page never forces a renumber.

Expand All @@ -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

Expand Down
Loading