# Requirements — How I Write a Spec

> Author: **Tim Georg** — © 2026
> Licensed under the **Creative Commons Attribution 4.0 International License (CC BY 4.0)**.
> See https://creativecommons.org/licenses/by/4.0/legalcode.

This is how I write a requirements document: how the files are laid out in a repo,
how a requirement is identified and worded, and the rules that keep a spec from
quietly describing something that is no longer true.

## What a requirements document is

A requirements document answers one question: **what must be true when this is done?**
Not how it is built (that is the plan), not what was built (that is the changelog).

Three properties matter, in this order:

1. **Addressable.** Every requirement has an ID, so a plan, a PR, a test and a postmortem
   can all point at the same thing without quoting prose that may have moved.
2. **Testable.** Every requirement states how you would know it is satisfied. "Fast" is
   not a requirement; "first token within 10 s on a quantized 7B model" is.
3. **Alive.** A requirement that changes silently is worse than no requirement. Change
   goes through the file *and* the changelog, in the same commit.

A spec is not a wish list and not a design document. If a sentence would be equally at
home in an ADR, it belongs in the ADR.

## The folder contract

All product requirements live in `docs/requirements/`, one file per domain, with a
numeric prefix that says what kind of file it is. The number is the file's address; it
does not change when the content does.

| Range | Holds | Examples |
|---|---|---|
| `00` | The index. Navigation only, no content of its own | `00_INDEX.md` |
| `01–02` | Core: vision and principles, system context | `01_VISION_AND_PRINCIPLES.md`, `02_SYSTEM_CONTEXT.md` |
| `10–19` | Feature domains, one per vertical | `10_COLLECTIONS.md`, `13_LIBRARY_VIEW.md` |
| `20–29` | Data contracts: the shapes on disk and over the wire | `21_DATA_MODEL_COLLECTION_JSON.md` |
| `30–39` | Architecture and planning extensions, when a repo needs them | `30_REKORDBOX_INTEGRATION.md`, `34_MCP_SERVER.md` |
| `90–93` | Global files that cut across every domain | `90_NFRS.md`, `91_INVARIANTS.md`, `92_PLANNED_FEATURES.md`, `93_OUT_OF_SCOPE.md` |
| `99` | The changelog | `99_CHANGELOG.md` |

**One number, one file.** Two files sharing a prefix (`15_ACCOUNT_AND_LICENSE.md` beside
`15_MODEL_MANAGER.md`) means the number no longer identifies anything, and every link that
says "see 15" becomes ambiguous. Take the next free number instead.

**Why split at all.** Most work touches one vertical. One file per vertical means
a feature PR touches one requirement file plus the changelog, and two engineers
working on different domains never conflict. The alternative — one `REQUIREMENTS.md`
that everything appends to — turns every change into a merge conflict and makes
"what does the spec say about X" a full-file read.

**A single monolithic `Requirements.md` is frozen, not maintained.** When a repo still
has one from before the split, it stays for historical reference and says so at the top.
Editing both is how the two copies drift apart.

**The thin entrypoint.** If tooling or habit expects `docs/REQUIREMENTS.md`, keep it as a
navigation stub: a status line and links into `docs/requirements/`. It holds no
requirement text. A stub that still carries full sections is a second source of truth
with a shorter deadline.

## Requirement anatomy

One requirement is one headed block:

```markdown
### COL-1 - Create collection from PDF files

Status: Done
Priority: P0

Acceptance criteria:

- AC1: Only .pdf files are accepted; non-PDF selection raises a descriptive error.
- AC2: Duplicate basenames in one request are rejected before any disk write.
- AC3: PDFs are copied to collection sources/ so originals can move/delete safely.
```

**The ID** is a short domain prefix and a stable integer: `COL-1`, `CHAT-3`, `MM-17`,
`LIC-4`. The prefix is the domain's abbreviation, chosen once and reused in plans,
issues, commit messages and test names. The integer never gets reused after a
requirement is dropped — a dead ID is a tombstone, not a vacancy.

**`Status:`** is one of:

| Value | Meaning |
|---|---|
| `Active` | Specified, not built |
| `Partial` | Some acceptance criteria hold, the rest do not |
| `Done` | Every acceptance criterion holds |
| `Proposed` | Written ahead of a decision; may never be built |
| `Deferred` | Deliberately postponed, with a named version or trigger |
| `Superseded` | Replaced by another requirement — name the replacement |
| `Obsolete` | No longer applicable — say why, and when it stopped being true |

`Superseded` and `Obsolete` are not the same thing, and neither is `Done`. A requirement
whose feature was removed is `Obsolete`, not `Done`; leaving it as `Done` tells the next
reader the behaviour still exists.

**`Priority:`** is `P0` (blocks the release), `P1` (needed for the release), `P2`
(after the release), `P3` (someday). One value, no combinations.

**Acceptance criteria** are numbered `AC1…ACn` and each one is observable from outside
the code. A criterion that names an internal function call is an implementation detail
in the wrong document; a criterion that says "works well" is not a criterion.

**Prose is allowed, structure is not optional.** A domain file may be mostly prose when
the requirement is a contract with many clauses. What it may not do is lose the ID: if
there are no `### PREFIX-n` headings, a plan cannot cite the requirement and a
postmortem cannot say which requirement was ambiguous.

## The four global files

Four shapes are shared across every repo, because each one answers a question that does
not belong to any single domain.

**`90_NFRS.md`** — non-functional requirements, one row per requirement.

| ID | Category | Requirement |
|---|---|---|
| NFR-1 | Performance | Collection create (chunking + indexing) for a 100-page PDF completes in under 60 s on a mid-range CPU |
| NFR-3 | Security | No document, query or user data leaves the local machine during normal operation |

Categories are a small closed set: Performance, Security, Reliability, Portability,
Usability, Offline, Data, Architecture, Backup. A number that has to be measured —
a threshold, a budget, a limit — belongs here with the measurement method beside it,
not in a feature file where nobody will find it.

**`91_INVARIANTS.md`** — properties that must never be false, with the place that
enforces them.

| ID | Constraint | Enforcement Points |
|---|---|---|
| INV-1 | Embedding model immutable per collection after first chunk indexing | UI disable, hook validation, backend rejection |
| INV-4 | Collection IDs are UUIDs and validated for all file operations | Path-resolution guard entrypoint |

The third column is what separates an invariant from a wish. If nothing enforces it,
it is either a requirement or a note, and putting it here makes the table lie.

**`92_PLANNED_FEATURES.md`** — the backlog, grouped by release, one line per item,
each pointing at the issue or plan that carries the detail.

| ID | Feature | Notes |
|---|---|---|
| PF-1 | Visual Fabric.js canvas editor for Sets | Drag blocks as nodes, draw connections as flowchart |

A planned feature is not a requirement until it has acceptance criteria. Until then it
is one line and a link.

**`93_OUT_OF_SCOPE.md`** — explicit non-goals, with the reason.

| Topic | Rationale |
|---|---|
| Cloud or remote backend | Conflicts with local-first principle |
| Multi-user sharing | Product is single-user desktop |

This file exists to end the same conversation twice. A non-goal without a rationale gets
re-proposed every few months; with one, the next discussion starts from the answer.

## The changelog rule

`99_CHANGELOG.md` is append-only, newest first, one `## YYYY-MM-DD` section per change,
each naming the requirement IDs it touched and why.

The rule that makes the whole system work: **a behaviour change updates the matching
requirement file and `99_CHANGELOG.md` in the same change.** Not a follow-up commit, not
a cleanup ticket. If the spec is updated later, it is updated wrong, because the person
who knew why has moved on.

The changelog is not a git log. It records *decisions about requirements* — a new
requirement, a changed criterion, a requirement retired as obsolete — and it survives
the history being rewritten.

## The index rule

`00_INDEX.md` lists every file in the folder with a one-sentence summary of what it
specifies, ordered by number, grouped by the ranges above. It is the only way into the
folder; a file that is not in the index does not exist as far as a reader is concerned.

Two conventions collide here and a repo has to pick one:

- The vault convention is `00_Index.md` (capital I), because every Obsidian folder uses
  it and the `folder-index` skill generates it.
- The requirements convention in the existing repos is `00_INDEX.md` (all caps), because
  the core files are all caps.

Keeping both — one as a vault index, one as the requirements index — duplicates the
navigation and guarantees they disagree. Pick per repo and delete the other.

## Changing a requirement

1. Open the domain file, not the index.
2. Change the acceptance criterion, or add the next free ID.
3. If the change is a decision, record it in `99_CHANGELOG.md` in the same commit.
4. If a file was added, renamed or retired, fix `00_INDEX.md`.
5. If the change alters what must be built, the plan and the issue get the same
   treatment — a spec that is right while the plan is stale is only half right.

Tightening a criterion is a change like any other. Loosening one because the build
missed it is how a spec becomes a description of whatever was built.

## What a gate cannot check

A script can check that every file has an index entry, that IDs are unique, and that the
changelog was touched. It cannot check whether a requirement is any good. These stay
with review:

- Whether an acceptance criterion is observable from outside the code.
- Whether an NFR number has a measurement method, or is a number someone liked.
- Whether an invariant's enforcement column is true, or aspirational.
- Whether a `Done` status still matches the running product.

That gap is deliberate. A requirement nobody can verify is a rule people will route
around.

## Where this is implemented

The scheme was extracted from two repos that arrived at it independently:

| Repo | Requirements | Notes |
|---|---|---|
| Smartimize | `Smartimize-App/docs/requirements/` | The reference long form: tabular requirements with `ACn` lists, IDs `COL`, `CHAT`, `KG`, `MM`, `LIC` |
| Klangkurator | `dj-crate-explorer/docs/requirements/` | Same split, prose-heavy domains, IDs written as `REQ-34-PR` inside headings |

Both follow the folder contract, both carry `90`–`93` and `99`, and both let a plan cite
a requirement by ID. That is the part worth keeping; the ID formatting inside a heading
is a per-repo choice.
