The documentation contract

This is the contract every document under docs/ in this repository — and every document elsewhere that is listed in docs/registry.yml — is held to. If you are an agent about to write or edit a document, or a person trying to understand why a file has a YAML block at the top, this page is the whole story. It does not assume you have read the design spec it comes from, though if you want the reasoning behind a decision rather than the decision itself, that spec is docs/superpowers/specs/2026-08-25-documentation-standard-design.md.

Two classes, not six categories

Before this contract, documents here fell into six informal categories — specs, plans, notes, handoffs, migration guides, and a handful of things that fit none of them, like qa/howto.md and infra/README.md. Sorting by topic turned out to be the wrong axis. The distinction that actually matters is whether a document claims to describe the world right now.

Records are dated minutes. A spec, a plan, a note, a handoff, a migration guide — each one is a snapshot of what was true, decided, or measured on the day it was written. A record's body is never rewritten and never corrected. If it turns out to be wrong, it is superseded, not edited: only its metadata moves — or, when only one section of it was overtaken, a pointer is added above that section and nothing else changes; see "When a record is obsolete only in part" below. Adding front matter to a record written before this contract existed is metadata, not revision — which is exactly what happened to the 65 records this contract was retrofitted onto. A record cannot go stale, because it never claimed to be current. Its only failure mode is being read as if it were, which is why records carry no freshness clock at all.

References assert the present tense. qa/howto.md, infra/README.md, docs/release-token-setup.md, docs/migrations/README.md, and documents like AGENTS.md outside this repository's scope, all describe how something works today. They can be wrong, they can drift out of date, and unlike records they must be actively maintained or explicitly retired.

The split matters because a freshness requirement that applied to every document would be false advertising within a month. A "reviewed" field that must be kept honest across ten reference documents gets kept honest. Spread across a hundred-plus records, it would get stamped with whatever date was convenient and stop meaning anything. So the review clock in this contract applies only to references. Records are exempt by design, not by oversight.

When a record is obsolete only in part

status is all-or-nothing, and most obsolescence is not. A plan can have one section overtaken by a later design while everything else in it is still the best account anyone has. Marking the whole record superseded misdirects a reader away from the parts that stand; leaving it active with no pointer leaves the stale section looking current, which is the exact failure this contract was built against.

The answer is a partial-supersession note: a dated line added at the head of the affected section — and only that section — naming the id that replaced that part.

> **Superseded in part, 2026-08-23.** The routing model below was replaced
> by [[core:spec:2026-08-23-edge-resolver]]. The rest of this document
> stands.

Three rules make it safe:

  1. It is strictly additive. Nothing already in the document is rewritten, deleted, or softened. The note goes above the text it warns about, and that text stays readable exactly as written.
  2. It points; it does not correct. The note names what replaced the section and where to read it. It does not restate the new answer — that would put a second copy of a live claim inside a dated record, where it would rot.
  3. It does not change status. A record carrying one of these stays active, because most of it still is. superseded remains reserved for a record replaced in whole, and only that case takes superseded_by.

This does not contradict the never-amend rule in "Two classes, not six categories" above. That rule protects the record's honesty as a snapshot: what it said on its date must remain what a reader sees. A pointer added above the text takes nothing away from that. The original claim is still there, still attributed to the day it was made, still a faithful account of what was believed then. Nothing that was true becomes false, and — the direction that actually matters — nothing that was false is quietly made true. What changes is only that a reader now learns, before reading it, that a later document measured this part differently.

None of this is new practice. This repository has been doing it by hand: docs/superpowers/plans/2026-08-08-phase0-tenant-ingress.md carries a "Correction, 2026-08-09." note of exactly this shape, written before this contract existed to name it. The convention is being named, not invented.

Write the pointer as [[id]], because that form is checked. pnpm docs:check extracts every [[<slug>:<kind>:<tail>]] from a document's body and resolves it exactly as it resolves superseded_by and derived_from: an id belonging to this repository that names no existing document is an error, not a warning. A partial-supersession note sits in ordinary prose, so its pointer is validated like any other body link, and renaming the document it points at fails the build until the note is updated. Two deliberate exemptions: an id inside a fenced block or inline backticks is skipped, so an example in this contract is not a dependency; and an id whose slug belongs to another repository is skipped rather than resolved, because a local checkout has no way to see that repository's documents — it is not validated here, and it is not recorded anywhere either.

What stays unchecked is everything except the pointer: the note's prose, the date it carries, which section it was placed above, and whether the supersession it claims ever happened. The link check tells you the target exists. It cannot tell you the target replaced anything.

The seven kinds and where they live

Kind Class Purpose Lives in
spec record A design decision and its rationale docs/superpowers/specs/
plan record An execution plan derived from a spec docs/superpowers/plans/
note record An investigation or measurement at a point in time docs/superpowers/notes/
handoff record Session-to-session context transfer docs/superpowers/handoffs/ (new apps) or top level (existing)
migration record An executable upgrade guide for downstream apps docs/migrations/
runbook reference How to operate a live system anywhere; registered in docs/registry.yml
guide reference Conventions and how to work in this repository anywhere; registered in docs/registry.yml

Documents are not forced into docs/ just to satisfy a folder convention. infra/README.md lives next to the infrastructure it describes, not in a documentation tree, because that is where a person operating a host will actually go looking for it. Anything outside docs/ comes under the contract by being listed as a path in docs/registry.yml — that file is a pure discovery mechanism. It carries nothing but a path; the kind and every other field still come from the document's own front matter, exactly as they do for a document the generator finds by walking the tree. A document outside docs/ that is not registered is invisible to the index, and registering one is a single line.

Root AGENTS.md is registered this way, because it is a reference by every definition above: it asserts how this repository works today, and it is the first document any agent reads. template/AGENTS.md, template/DESIGN.md, template/FOUNDATION.md, and template/PRODUCT.md are deliberately not registered, and this is scope, not oversight. They are not documents about this repository; they are seed content this repository ships into every scaffolded app, and the moment the scaffolder copies them the app that receives them — not this one — is the thing they describe and the party responsible for keeping them true. template/DESIGN.md is generated per-pack, not authored prose; template/PRODUCT.md ships intentionally full of TODOs for the app owner to fill in, so reviewed would either be fabricated or attached to a document that admits it is incomplete by design. Holding any of the four to this repository's freshness clock would assert authority over content this repository stops controlling at the moment it is copied. An app that wants its own copies under the contract can register them in its own docs/registry.yml once scaffolded — that is a decision for the app, not a default this template imposes on it.

Front matter schema

Every field below is checked by pnpm docs:check — checked for form: that required fields are present, that dates are dates, that list fields are lists, that enumerated fields hold a legal value, and that ids resolve. No field is checked for truth, and the two free-vocabulary fields (measured_against and systems) are not checked against their vocabularies at all. Records and references share id, kind, title, and evidence; the rest of the schema differs by class.

Records

---
id: core:spec:2026-08-23-edge-resolver     # required, unique, slug-prefixed
kind: spec                                 # spec|plan|note|handoff|migration
title: The edge resolver                   # required
date: 2026-08-23                           # required
status: active                             # active|superseded|abandoned
superseded_by: core:spec:2026-09-01-foo    # required iff status: superseded
evidence: measured                         # measured|inferred|mixed|unknown
measured_against: [bloomi-prod]            # required iff evidence is measured|mixed
derived_from: [core:plan:2026-08-19-x]     # optional; what this builds on
systems: [edge, atiempo]                   # optional; absent means Unfiled
visibility: internal                       # internal | public; absent means internal
---

References

---
id: core:runbook:infra
kind: runbook                              # runbook|guide
title: Infrastructure
owner: angel
reviewed: 2026-08-25                       # the freshness clock
evidence: measured
measured_against: [bloomi-prod, bloomi-nonprod]
systems: [infra]
visibility: internal                       # internal | public; absent means internal
---

References drop date, status, and superseded_by — a reference isn't an event and is never superseded, it's either current or it's fixed — and add two fields records don't have:

A reference's id still follows <repo-slug>:<kind>:<topic>, just without a date segment, since it is not an event.

measured_against

measured_against is always a YAML list, never a comma-separated string — [prod, staging], not a single string containing a comma — so that the index can group by environment without guessing where one entry ends and the next begins. It is required whenever evidence is measured or mixed, and its entries are drawn from a fixed vocabulary rather than free text, because free text drifts into synonyms (prod, production, bloomi-prod) within a month of nobody enforcing it. The vocabulary today:

This list is extended by editing it, in this file, in the same pull request that introduces the new value. There is no separate registry for it; the vocabulary lives here because this is the one place both humans and agents are pointed at before writing a measured_against line.

The vocabulary is a convention, not a check. pnpm docs:check confirms that measured_against is a YAML list and that it is non-empty when evidence is measured or mixed; it does not compare the entries against this list. A typo like bloomi-production passes. Keeping the vocabulary closed is a review responsibility.

systems

systems is what docs/INDEX.md groups by, so it is the field that decides whether the index is navigable or is one flat table with a hundred rows in it. It is a YAML list, and like measured_against it is drawn from a fixed vocabulary rather than free text — for the same reason. A free-text tag drifts into ui, ui-package and components inside a month, and three groups of one are worse than no grouping at all.

The vocabulary today:

Give a document two entries when it genuinely spans two systems — it will then appear under both headings — and one when it does not. Two is a description; three is usually a way of avoiding a decision. Prefer an existing name over a new one: a system with a single member is a prompt to ask whether it belongs in a neighbouring group.

This list is extended the same way measured_against is: by editing it here, in the pull request that introduces the new value. And like measured_against, it is a convention rather than a check: the validator confirms systems is a YAML list and nothing more, so an invented name silently produces a new heading in docs/INDEX.md rather than an error. That heading, with one document under it, is the signal to look at. A document with no systems at all is filed under "Unfiled" in the index, which is a visible defect, not a default.

The evidence rule

Two levels, because provenance breaks down at the level of an individual claim long before it breaks down at the level of a whole document.

Document level, in front matter: evidence: measured | inferred | mixed | unknown.

Section level, in prose. Any section carrying load-bearing factual claims opens with one line naming what backs it:

> Measured 2026-08-23 on bloomi-prod and bloomi-nonprod.
> Inferred from [[core:spec:2026-08-19-atiempo-provisioning]].

This is not new invention — docs/superpowers/specs/2026-08-23-edge-resolver-design.md already did this by hand before the contract existed to name it. One half of a marker is checked and the other is not: an id written as [[...]] inside one is resolved like any other body link, so an "Inferred from" marker cannot point at a document that does not exist, but the date in a marker is plain prose and is not validated the way date and reviewed are — those two fields are the only dates the validator reads. The marker exists so a reader can see, section by section, what a claim rests on, without having to trust the document-level evidence field to summarize six different provenances correctly.

The rule that ties them together: no document may raise the evidence class of a claim it inherits. If a plan's Outcome section was inferred, a runbook built on it may not describe the same fact as measured or verified. This is the sentence that would have stopped the failure this contract was designed against — three documents in a row treating an inference as established fact, each one written from the one before it.

To make that rule checkable rather than aspirational, inheritance has to be visible: derived_from: [core:plan:2026-08-19-atiempo-tenancy-provisioning]. With that declared, half the rule becomes mechanical and is enforced by pnpm docs:check: a document may claim evidence: measured only if every document listed in its derived_from is itself measured. If any parent is inferred, mixed, or unknown, the most the child may claim is mixed. To earn measured legitimately, an author either remeasures the inherited part directly — at which point the parent is no longer what the claim rests on, and drops out of derived_from — or admits the mixture and writes mixed.

This is a speed bump, not a proof of honesty. The validator checks form: that measured and mixed declare a non-empty measured_against, that derived_from ids resolve, that a measured document's parents are themselves measured. It cannot know whether anyone actually measured anything, and it cannot see whether one specific sentence in a large document was independently remeasured or quietly inherited. What it buys is that claiming more certainty than you inherited now costs a visible, reviewable act — deleting a derived_from line — instead of costing nothing.

Visibility and the leak guard

visibility is internal or public, and internal is applied by absence: a document with no visibility key at all is internal, not a document waiting to be classified. That is deliberate, not an oversight — the 65 records this contract was retrofitted onto, and every document written before this field existed, all needed to stay internal without anyone going back to stamp them one by one. Forgetting the field is safe. Getting it wrong in the other direction — writing visibility: public on something that shouldn't be — is the one mistake this field cannot protect you from, which is why the rest of this section exists. A value that is neither internal nor public is an error from pnpm docs:check, and anything else that reads the field treats a malformed value the same way it treats a missing one: as internal.

The reason this field exists at all: this repository is private, but a GitHub Pages site published from it, on the organisation's team plan, is not. The public site cannot ask who a reader is. There is no login, no per-reader check, nothing that distinguishes a colleague from anyone else who finds the URL. Someone will forget that the moment a document is marked public, it is exactly as available as this contract is, which is why the warning belongs next to the field itself rather than only in the design spec this contract points to.

Because the cost of marking a document public is that absolute, pnpm docs:check runs a leak guard over the body of every document declared visibility: public. It scans for five shapes: ipv4 (an IPv4 address), host-path (a /srv/ path), hostname (a subdomain of bloomi.com.mx), ssh (an ssh invocation), and private-key (a BEGIN … PRIVATE KEY header). A match produces a warning naming the file, the line, the kind, and an excerpt — never a failure. The excerpt is the start of the line, trimmed and capped at 100 characters; on a long line that cap can land before the matched text, so the warning can name a kind and a line without showing the substring that actually matched. The guard does not skip fenced code blocks. A hostname inside a code sample in a published document is exactly as published as the same hostname in a sentence, so hiding it behind three backticks buys nothing.

Warn, not fail, because the guard cannot tell whether publishing is safe. It matches shapes, not judgment: it has no way to know that an IP address in a worked example is fictional, or that a real one in prose is exactly the leak it looks like. What it buys is that marking a document public stops being a quiet act — the moment someone flips visibility to public, the next docs:check run puts every matching line in front of them. A person still decides whether what the guard found is a problem. The guard's job ends at making that decision impossible to skip past by accident.

The guard's coverage has known gaps, accepted deliberately rather than left unnoticed. It does not match IPv6 addresses, because every host in this repository's scope is IPv4-only — there is nothing yet to catch. It does not match a bare /srv with no trailing slash, because every real instance in this corpus is written as /srv/bloomi/...; a bare /srv would need to start matching a word, not a path. There is also a known false positive in the other direction: a four-part version string in prose, like 1.2.3.4, matches ipv4 and warns on something that isn't a host at all. That's the accepted trade — a scanner narrow enough to avoid that false positive would also miss real addresses, and a missed address is the worse failure of the two. And there is a fourth gap that is not hypothetical: the guard scans only the document body, never the front matter. measured_against routinely carries hostnames — the design spec for this very work has measured_against: [bloomi-core, bloomi.com.mx, docs.github.com] — and title and systems are the fields a rendered page shows first. A public document whose only leak lives in its front matter scans completely clean. And there is a fifth gap: the guard scans for five shapes, and a link is not one of them. A public document whose body links to an internal page — See [the runbook](/infra/README.html) — renders that anchor on the public site. The target 404s, so no content leaks, but a reader learns an internal document exists at that path. No pattern is planned for this; it is listed here so the gap stays known rather than discovered.

A generator exists now. pnpm docs:site --scope internal|public renders the corpus to static HTML under .docs-site/, which is git-ignored — nothing it writes is committed. It deploys nothing. Running it produces files you open locally with a browser; there is still no website, no URL anyone else can reach.

The public build is not a second generator. --scope public filters the same documents the internal build sees, keeping only what carries visibility: public, and everything the resulting index prints comes from that filtered set, never from the full corpus: no total document count, no section heading for a system that has nothing public under it. A separate public code path would be a second implementation to keep in step with the first, and the one that drifts is the one that leaks — filtering one input, in one generator, is what keeps the public build's silence trustworthy. Leaking by omission — a heading naming a subsystem with no public document in it, a count that reveals how much stayed internal — is still leaking, so the public build prints nothing it cannot point at an included document to justify.

docs/public.json is the other half of that guarantee, and the reason it exists at all. It is generated by pnpm docs:index, alongside docs/INDEX.md and docs/index.json, committed, and diff-checked in CI exactly as they are: publishing a document changes a manifest, in the same pull request that flips visibility to public. Say plainly what that buys, because it is the point of all of this: the leak guard above is an interrupt for the author, not a gate. Five regexes over a document's body cannot judge whether an IP address in an example is fictional or a hostname in prose is real — they were never built to. If the guard were ever treated as the thing standing between a visibility: public line and a live site, it would be security theatre: a check that looks like a control but cannot stop a leak, only warn about a shape it happened to recognise. What actually controls publication is that it is rare and deliberate, and that docs/public.json makes it greppable: the file's entire content is the list of what is published, so anyone who looks — a reviewer scanning the diff, an agent auditing the repository — sees it immediately, with nothing else to sift through. That is visibility, not enforcement: nothing today forces that look. There is no branch protection on main in this repository and no CODEOWNERS file, so no pull request is required at all, let alone one a particular person has to approve, and a red docs job in CI blocks nothing from merging. What would turn this from visible into a real control is a CODEOWNERS entry for docs/public.json plus required review enabled on main — neither exists yet. Enabling required review is a repository setting only the repository owner can turn on; a CODEOWNERS file without it routes a suggested reviewer and stops nothing.

Worth stating rather than leaving a reader to infer it: marked, the renderer behind docs:site, does not sanitise. Raw HTML written in a document's body passes straight through to the rendered page exactly as written. That is safe only because every document under this contract is authored inside this repository, by someone with commit access here — the trust boundary is "who can push to this repository," not anything the renderer enforces. A pipeline that accepted documents from outside that boundary would need to sanitise before this one's assumptions still held.

Still no site to put any of this on. This field and this guard, and the generator described above, are what a future publishing step will read; none of them publish anything today. Plan C is what would wire .docs-site/ to a destination — a GitHub Pages workflow or a Caddy site with its own auth, both described in docs/superpowers/specs/2026-08-27-documentation-publishing-design.md. Until one of them lands, visibility: public changes nothing about where a document lives — it only changes what docs:check looks for inside it, and now, what a local --scope public build would include if anyone ever served it.

What the four CLIs do

What this contract does not enforce

Said plainly, because a standard that is vague about its own limits repeats the exact failure it exists to prevent — something that reports success while quietly doing less than a reader assumes.

Record immutability is a convention, not a mechanism. Nothing stops someone from editing a record's body after the fact. The docs job in .github/workflows/ci.yml runs pnpm docs:check, then pnpm docs:index, then git diff --exit-code -- docs/INDEX.md docs/index.json docs/public.json. That diff only fails the build when regenerating changes those three committed files — it confirms the index and the public manifest are current, not that a record's prose hasn't changed. A record's body can be rewritten and the docs job stays green as long as its front matter is untouched. Catching a rewritten record is left to code review.

Status transitions are not enforced. The validator sees one snapshot of the repository at a time. It can confirm a status value is legal and that superseded_by resolves, but it cannot detect a superseded record quietly reverting to active, or a superseded_by being repointed to a different id later. Same trade-off, same reason: this is buildable, and it isn't bought here.

The evidence rule is enforced at its edge, not at its centre. derived_from plus the measured_against requirement make an unjustified claim of measurement expensive to write and easy to spot in review. Neither this contract nor the validator can tell whether one particular sentence in a document was independently remeasured or silently inherited, and neither can tell whether anything was actually measured in the first place — only that the document says so and names what it claims to have measured against.

Nothing here checks that documentation is true. What this contract buys is that provenance is visible — that a reader can see, in thirty seconds, what a claim rests on and how far back the chain of inheritance goes. Visibility is the entire product. It is not, and does not claim to be, a guarantee of accuracy.