> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Specification System

> How a module goes from an idea to merged code, and how we prove afterwards what was decided, when, why, and by whom. This is the operational manual; the repository README.md carries the short version for anyone arriving for the first time.

The model is borrowed rather than invented: a numbered, versioned proposal with a mandatory
structure, a lifecycle status, and a record of the release that implemented it — the mechanism
Kubernetes uses for KEPs, IETF for RFCs and Python for PEPs. What we add is Backstage's lesson:
if the traceability lives in prose, nobody queries it. Ours lives in validated frontmatter.

***

## 1. Dictionary

| Term            | What it is                                                                                                           | Where it lives                   | Versioned as       | Changes by                            |
| --------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------ | ------------------------------------- |
| **DEC**         | A closed design decision from the debate rounds. The vocabulary every other document cites.                          | `design/decision-registry-v1.md` | registry version   | a new ADR that supersedes it          |
| **ADR**         | Architecture Decision Record. *Why* a cross-cutting technical decision was taken. One series for the whole platform. | `adr/`                           | status field       | a superseding ADR                     |
| **Standard**    | The MUST/NEVER rules for a whole area (data, events, jobs, api…). Applies to every module.                           | `standards/`                     | v1, v2             | in place, with review                 |
| **PRD**         | Product Requirements Document. What a module is, for whom, what success means. One per module.                       | `modules/{m}/prd.md`             | **semver**         | new version + changelog               |
| **Module spec** | The living technical shape of a module: entities, invariants, events, endpoints. Always reflects reality.            | `modules/{m}/spec.md`            | not versioned      | edited freely; it describes what *is* |
| **FS**          | **Feature Spec.** The unit of delivery: one independently shippable increment.                                       | `modules/{m}/features/`          | **semver**         | new version + changelog               |
| **TS**          | Task Spec. The execution plan for an agent. **Optional** — only when a feature needs more than one slice.            | `task-specs/`                    | not versioned      | rewritten per attempt                 |
| **Slice**       | A canonical implementation archetype an agent copies. Two exist: synchronous command, async pipeline.                | `slices/`                        | SPEC → WALKTHROUGH | when the reference code changes       |
| **Runbook**     | What a human does when something breaks.                                                                             | `runbooks/`                      | not versioned      | edited freely                         |

Two vocabulary traps worth naming:

* **Module spec ≠ Feature Spec.** The module spec is a *map of what exists* and is edited freely.
  A Feature Spec is a *contract for a change* and is frozen once approved.
* **FS ≠ TS.** The FS says what to build and how we will know it is done. The TS says how to build
  it. A small feature needs no TS at all — the FS carries an *Execution* section instead.

## 2. The chain

```
DEC-*  ──►  ADR  ──►  PRD  ──►  Module spec  ──►  FS  ──►  TS (optional)  ──►  PR
 the         the       what      what exists      the       how the agent      and the PR
 decision    why       and for   right now        unit of   executes it        links back
             behind it  whom                      delivery                     into the FS
```

Every link is checkable. A FS that cites a DEC that does not exist fails CI; a FS marked
`implemented` with no PR recorded fails CI; a table in the module spec that no FS claims is
reported as uncovered.

## 3. Identifiers

`FS-{MODULE}-{NNNN}` — four digits, so a module can hold up to 9999 feature specs without
renumbering. Module codes:

| Code   | Module        |
| ------ | ------------- |
| `IDN`  | identity      |
| `CORE` | customer-core |
| `LOY`  | loyalty       |
| `MSG`  | messaging     |
| `CRM`  | crm           |

Examples: `FS-LOY-0002`, `FS-CORE-0014`. Numbering is **per module** — modules grow at different
rates and a global sequence would force coordination for nothing. Numbers are never reused, not
even after a spec is withdrawn.

A feature that genuinely spans modules belongs to `core`, or it is two feature specs with a
`depends_on` between them. It is never one FS filed under two modules.

## 4. Versioning

Two different schemes, deliberately:

| What                                | Scheme                         | Why                                                                                                                                                                                                   |
| ----------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Spec documents** (PRD, FS)        | **Semver** `MAJOR.MINOR.PATCH` | A spec describes a contract. Major = the contract changed and consumers must react. Minor = something was added. Patch = wording, no behavioural change. It lines up with the API deprecation policy. |
| **Every project in the repository** | **CalVer** `YYYY.M.PATCH`      | We ship a suite, not a library. Nobody depends on our internal package versions, so a date is more informative than a semver nobody reads. All projects share the repository version.                 |

A spec starts at `0.1.0` in `draft`. It reaches `1.0.0` the moment it is `approved` — that is what
approval *means*: the contract is now stable enough to build against.

## 5. Lifecycle

```
draft ──► review ──► approved ──► in-progress ──► implemented
                         │
                         └──────► superseded  |  withdrawn
```

| Status        | Meaning                                                                | Who moves it            |
| ------------- | ---------------------------------------------------------------------- | ----------------------- |
| `draft`       | Being written. Anything can change.                                    | author (human or agent) |
| `review`      | Complete and awaiting the owner's decision.                            | author                  |
| `approved`    | **Frozen contract.** Version becomes `1.0.0`. Code may now be written. | owner only              |
| `in-progress` | A TS or PR is executing it.                                            | whoever starts the work |
| `implemented` | Merged and verified, with a delivery record.                           | the merging PR          |
| `superseded`  | Replaced. Must name the FS that replaces it.                           | the superseding FS      |
| `withdrawn`   | Decided against. **Kept forever** — the reasoning is the value.        | owner                   |

### The two rules that make this a system rather than a folder

> **R-S1 — No code merges for a feature whose FS is not `approved`.**
> The PR states its FS id; CI reads that spec's status and blocks otherwise.

> **R-S2 — An `approved` FS is immutable in its normative sections.**
> Changing it is not editing it: bump the version, add a changelog row saying what changed and
> why. If the change reverses a DEC or an ADR, that ADR is written *first*.

Normative sections are: Scope, Non-scope, Behaviour, Data, API, Events, Acceptance criteria.
Context, Notes and Open questions can be edited freely at any status.

## 6. Frontmatter — the machine-readable contract

Every FS carries this. The validators read it; nothing here is decoration.

```yaml theme={null}
---
id: FS-LOY-0002
title: Points Ledger
module: loyalty
type: feature-spec
version: 1.2.0
status: implemented
phase: F1a                         # F1a | F1b | F2
archetype: synchronous-command     # or asynchronous-pipeline, or none
owner: daniel
depends_on: [FS-LOY-0001]
supersedes: []
decisions: [DEC-B5, DEC-H3]        # DEC-* this implements
adrs: [ADR-011, ADR-017]           # ADRs it depends on
tables:    [loyalty.ledger_transactions, loyalty.point_lots]
events:    [loyalty.points.earned, loyalty.points.expired]
endpoints: []                      # "METHOD /v1/path — permission"
created: 2026-08-17
updated: 2026-09-15
implementation:
  - version: 1.0.0
    task_spec: TS-004
    pr: 142
    merged: 2026-09-01
    by: daniel + claude-opus-5
    verified_in: cert
---
```

`tables`, `events` and `endpoints` are what the coverage validator uses: every entity in a module
spec must be claimed by exactly one FS. That is how we find out that something was designed and
never assigned to a delivery.

The **Delivery record** section in the body is *generated* from `implementation`. Never hand-write
it — same discipline as a generated data model.

## 7. Writing a module, step by step

1. **PRD.** One document, from [`TEMPLATE-prd.md`](/modules/TEMPLATE-prd). Who it is for, the jobs it
   does, explicit non-goals, how success is measured, pricing implications, phases. Written once,
   reviewed, then rarely touched. Write it before the technical design — if the PRD is hard to
   write, the module is not understood yet.
2. **Module spec.** The technical shape: entities and their invariants, events emitted, endpoints
   with the single permission each declares, extension points. This one stays alive and is edited
   as reality changes.
3. **Decompose into features.** Cut along *independently shippable increments*, not along tables.
   The test: can this ship alone and be worth something? If not, it is a section inside another FS.
   Record the resulting list in the module hub as a roadmap table.
4. **Write each FS** from [`TEMPLATE-feature-spec.md`](/modules/TEMPLATE-feature-spec), in dependency
   order. Status `draft` → `review` → owner approves → `1.0.0`.
5. **Execute.** Small feature: the FS's *Execution* section is enough. Larger: open a TS declaring
   its archetype. The agent works from the approved FS, not from a chat prompt — that is the whole
   point of the system.
6. **Merge and record.** The PR appends an `implementation` entry and moves the status to
   `implemented`. Then the module spec is updated to match what now exists.

## 8. Languages

Module documentation ships in **English and Spanish**, so anyone can read and review a module in
full — not only the people who work in English all day.

* **English is authoritative.** `modules/{m}/` is the normative version: it is what agents read,
  what CI validates, and what wins on any discrepancy.
* **Spanish is a faithful translation**, not a variant. It lives at `es/modules/{m}/` and mirrors
  the tree exactly, file for file. A translation that says something different from the original
  is a defect, not a local decision.
* Both carry the **same front-matter** — same `id`, `version` and `status`. The `translation`
  validator fails when they diverge, which is what stops a version bump from silently leaving the
  Spanish behind.
* Every translated file opens with a line naming its authoritative original.

Standards, ADRs, task specs and slices remain English-only. They are agent and engineer artifacts
governed by DEC-I1, and their audience already works in English. If that changes, the same mirror
mechanism extends to them without any new machinery.

The public site (`docs/public/`) is a different document with a different audience, and neither
side of it is a translation of anything here.

## 9. Validation

```bash theme={null}
pnpm docs:validate          # report mode — prints findings, exits 0
pnpm docs:validate:strict   # CI mode — any finding fails the build
pnpm docs:validate:mint     # what Mintlify itself will say about the build
```

| Validator     | Catches                                                                                                         |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `frontmatter` | Missing or malformed fields, invalid status/phase/archetype, bad semver, `updated` before `created`             |
| `coverage`    | A table, event or endpoint in a module spec that no FS claims — or that two FS claim                            |
| `graph`       | `depends_on`, `supersedes`, `DEC-*` and `ADR-*` references that do not resolve; dependency cycles               |
| `status`      | `implemented` with no delivery record · `superseded` with no target · `approved` still at `0.x`                 |
| `changelog`   | Frontmatter version that does not match the latest changelog row                                                |
| `translation` | A module document with no Spanish mirror, or a mirror whose `id`/`version`/`status` disagrees with the original |
| `links`       | A link that is relative, carries a `.md` suffix, or points at no page                                           |
| `navigation`  | A `docs.json` page with no file, a file in no navigation, a page named `README`                                 |

Report mode is the default while modules are still being decomposed; `strict` is what CI runs.
`docs:validate:mint` runs Mintlify's own CLI. It is deliberately **not** pinned and **not** a CI
gate: it validates against a hosted product that ships several releases a day, so pinning it would
check us against a stale contract. Run it before a documentation push; the rules it enforces that
we already know about are encoded in `links` and `navigation`, which are pinned and deterministic.

## 9b. Writing for Mintlify

Three constraints are not obvious, and each of them silently produced a 404 rather than an error.

> **`.md` is parsed as MDX.** A bare `<` followed by anything that is not a letter reads as the
> start of a JSX tag and kills the page. Latency budgets are the common case: write `p95 &lt;300 ms`,
> not `p95 <300 ms`. A bare `{` opens a JavaScript expression — escape it as `\{`, or put the token
> in backticks. HTML comments fail the same way: use `{/* ... */}`, never `<!-- ... -->`.
> Inside fenced code blocks and inline code spans, none of this applies.

> **A page must never be named `README.md`.** Mintlify skips those files entirely; the navigation
> then points at a page that, as far as the build is concerned, does not exist. Section overviews
> are `overview.md`. The one `README.md` in `docs/internal/` is deliberate — it is the GitHub-facing
> file, and it is the only place relative, `.md`-suffixed links are correct.

> **Links resolve from the docs root, not from the file.** A link whose target is written as
> `data.md` works on GitHub and 404s on the site, and so does a bare `standards/data`. Every
> internal link is absolute and extensionless: `/standards/data`.

The `links` and `navigation` validators enforce all three.

## 10. Module index

| Module                                     | PRD                           | Spec                            | Features                                 | Status                             |
| ------------------------------------------ | ----------------------------- | ------------------------------- | ---------------------------------------- | ---------------------------------- |
| [`identity`](/modules/identity/overview)   | [prd](/modules/identity/prd)  | [spec](/modules/identity/spec)  | [9 written](/modules/identity/overview)  | **complete, EN + ES, all `draft`** |
| [`core`](/modules/core/overview)           | [prd](/modules/core/prd)      | [spec](/modules/core/spec)      | [14 written](/modules/core/overview)     | **complete, EN + ES, all `draft`** |
| [`loyalty`](/modules/loyalty/overview)     | [prd](/modules/loyalty/prd)   | [spec](/modules/loyalty/spec)   | [14 written](/modules/loyalty/overview)  | **complete, EN + ES, all `draft`** |
| [`messaging`](/modules/messaging/overview) | [prd](/modules/messaging/prd) | [spec](/modules/messaging/spec) | [9 written](/modules/messaging/overview) | **complete, EN + ES, all `draft`** |
| [`crm`](/modules/crm/overview)             | [prd](/modules/crm/prd)       | [spec](/modules/crm/spec)       | [5 written](/modules/crm/overview)       | **complete, EN + ES, all `draft`** |

## 11. When to prune this

If in a few months specs are being written that nobody reads, the system has failed and should be
cut back rather than defended. The honest signal: a FS whose acceptance criteria were never
checked against the merged code. The ceremony is only worth its cost because an agent given an
approved spec with real acceptance criteria produces far more consistent work than one given a
prompt — if that stops being true, stop paying for it.
