Files
familienarchiv/SPEC_DRIVEN_DEVELOPMENT.md
Marcel c160ab3223 refactor(sdd): make the feature spec issue-only (no committed spec.md)
The Gitea issue body is the single source of truth for a spec; the only
per-feature artifact in git is the RTM row (REQ-ID -> issue # -> test). Drops
per-feature spec.md/tasks.md/checklist files from the workflow (the _example
stays as a template/reference). Updates the guide, ADR-041, AGENTS.md, CLAUDE.md,
templates, the RTM (adds an Issue column), the implement/review-pr skills, and
replaces the file-spec CI jobs with an rtm-check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:55:26 +02:00

171 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Spec-Driven Development (SDD)
How we turn a feature idea into merged, traceable code in this repo. SDD layers a uniform,
machine-readable front-end onto the workflow we already run (Gitea issues → branch/PR →
multi-persona review → red/green TDD). It does not replace any of that — see
[ADR-041](./docs/adr/041-sdd-adoption.md) for the why.
- **The rules** live in [`.specify/constitution.md`](./.specify/constitution.md) (humans) and
[`.specify/AGENTS.md`](./.specify/AGENTS.md) (AI agents, every invocation).
- **The templates** live in [`.specify/templates/`](./.specify/templates/).
- **The worked example** is [`.specify/features/_example/`](./.specify/features/_example/) — read it first.
---
## 1. The workflow in 8 steps
| # | Step | Who | Artifacts created / touched |
|---|---|---|---|
| 1 | **Idea → Gitea issue** using the Feature template | author | Gitea issue (labels `spec-required`, `needs-review`) from `.gitea/ISSUE_TEMPLATE/feature.md` |
| 2 | **Write the spec _in the issue body_** — Context, User Journey, EARS `REQ-NNN` requirements, measurable acceptance criteria, Out of Scope | author | the Gitea issue body **is** the spec (single source of truth — no committed `spec.md`) |
| 3 | **Capture durable design decisions** as needed | author | a `docs/adr/` ADR for any project-wide/irreversible decision; an OpenAPI contract and a STRIDE threat model inline in the issue (use the `.specify/templates/` as the writing aid) |
| 4 | **Persona spec review** — the six checklists gate the spec | RE, Developer, Security, DevOps, UI/UX, Architect | `/review-issue` posts each persona's checklist verdict as a Gitea comment; findings folded into the issue body |
| 5 | **Resolve Open Questions & blocking FAILs** — spec does not proceed while any remain | author | issue body updated; `Open Questions` emptied |
| 6 | **Seed the RTM** — one row per `REQ-NNN`, pointing at the issue | author | rows added to [`.specify/rtm.md`](./.specify/rtm.md) (`Issue: #n`, `Status: Planned`) — committed with the feature branch |
| 7 | **Implement** in a worktree, TDD per task (failing test → green → refactor → commit); agent reads `AGENTS.md` + the **issue body** (the spec) | implementer (often an AI agent) | code + tests; `npm run generate:api` after backend changes; RTM `Status``Done` |
| 8 | **PR → multi-persona PR review → merge** | reviewers | PR (`Closes #n`); the closed issue is the archived spec, the RTM rows record what shipped |
The personas at step 4 review the **spec (the issue)**; the same personas at step 8 (via the
existing `review-pr` / `deliver-issue` skills) review the **code**. Step 4 catches at spec time
what used to surface only at step 8.
**Skills that drive this:** `/draft-spec` (requirements engineer authors steps 12 → creates
the issue) → `/review-issue` (step 4 gate) → `/implement` (steps 67) → `/review-pr` (step 8).
`/deliver-issue` runs review → discuss → implement → review-loop end-to-end.
> **Why issue-only?** The Gitea issue body is the single source of truth for a spec — there is
> no committed per-feature `spec.md` to drift out of sync with it. The only SDD artifact that
> lives in git per feature is the RTM row (`REQ-ID → issue # → test`). The worked example under
> [`.specify/features/_example/`](./.specify/features/_example/) is a **template/reference**, not
> a live feature — it shows the full artifact set in one place; real features keep the spec in
> the issue.
## 2. How a Gitea issue becomes a spec
**Before (free-form issue):**
> **Title:** Add profile pictures
> Users should be able to upload a picture for their profile. Make sure it's not too big and
> only admins can remove other people's. Show initials if there's no picture.
Ambiguous: how big? which formats? what status code on rejection? what about unauthenticated
callers? No identifiers to trace, no measurable criteria.
**After (SDD-structured issue — excerpt):**
> **Title:** As a user I want to upload a profile picture so other family members recognise me
>
> **## Requirements**
> - **REQ-002** (Event-driven) — When an authenticated user sends `POST /api/users/me/avatar`
> with a valid image, the user service shall store it and return a profile view with a
> non-null `avatarUrl`.
> - **REQ-008** (Unwanted-behavior) — If the uploaded file exceeds 2 MB, then the user service
> shall return `400 ErrorCode.AVATAR_TOO_LARGE` and store nothing.
> - **REQ-009** (Unwanted-behavior) — If a caller without `Permission.ADMIN_USER` targets
> another user's avatar, then the system shall return `403 ErrorCode.FORBIDDEN`.
>
> **## Acceptance Criteria**
> - **REQ-008** — a 2.1 MB PNG returns `400 AVATAR_TOO_LARGE`; bucket object count unchanged.
Every behavior is now a uniquely-identified, testable, EARS-formed requirement with a
measurable acceptance criterion. See the full version in
[`.specify/features/_example/spec.md`](./.specify/features/_example/spec.md).
## 3. How to run a persona review
Each persona reads the spec, walks its checklist in `.specify/personas/<persona>.md`, and
posts a Gitea comment with **PASS / FAIL / QUESTION** per
item and a verdict. A `FAIL` from Security or Architect is a hard block. Concrete example:
> ### Security — Spec Review
>
> | # | Item | Status | Note |
> |---|---|---|---|
> | 1 | All mutating endpoints have authn + authz `If` clauses | PASS | REQ-006 (401), REQ-009 (403) |
> | 3 | Audit fields server-set, forbidden in body | **FAIL** | `avatarObjectKey` is bound from the request body → mass-assignment (CWE-639). Make it server-set in `UserService`. |
> | 6 | Upload type allow-list + size | PASS | REQ-007 / REQ-008 |
> | 9 | threat-model.md present & STRIDE-complete | **QUESTION** | Is the avatar URL public or proxied? If public S3, that's information disclosure. |
>
> **Verdict: CHANGES REQUESTED** — blocking FAIL: #3. Resolve #9 in the threat model.
The author folds the fix into the spec (here: server-set key + authenticated proxy URL),
empties the finding, and the persona re-reviews until `APPROVE`. This mirrors the existing
`review-issue` skill — the persona checklists just make the spec pass/fail explicit.
## 4. How the AI agent uses the spec
Once the spec is `APPROVE`d and tasks are seeded, the implementer points the agent at the
artifacts. Example prompt:
> Implement Gitea issue #142 (profile picture upload). Read `.specify/AGENTS.md` and obey the
> constitution it references. The contract is the issue body — its EARS requirements
> REQ-001…REQ-009 and acceptance criteria. Build a red/green task list from them, write the
> failing test for each REQ first, confirm it fails, then make it pass. After backend model
> changes run `npm run generate:api`. Do not mark a REQ done until its test is green; flip its
> row in `.specify/rtm.md` to Done as you go.
The agent now has: the rules (`AGENTS.md` → constitution) and the exact requirements with ids
from the issue — so its output is bounded and verifiable. (The `/implement` skill fetches the
issue body for you via the Gitea API.)
## 5. Maintenance rules
- **Constitution** ([`.specify/constitution.md`](./.specify/constitution.md)) — change it only
when a project-wide rule genuinely changes. Bump the semantic version (MAJOR = rule
removed/weakened, MINOR = rule added/tightened, PATCH = wording), run the §6 Sync Impact
review, and let the `constitution-diff` CI job list the files to reconcile. Record the bump
in ADR-041's revision log (or a superseding ADR for MAJOR).
- **AGENTS.md** — keep it under 200 lines. It cross-references the constitution; it must never
duplicate or contradict it.
- **ADRs** — project-wide/irreversible decisions go in [`docs/adr/`](./docs/adr/) (next free
`NNN`, verify on disk). Immutable once `Accepted`; supersede, don't edit.
- **Feature specs** — the spec is the Gitea issue body; there is no committed `spec.md`.
"Archiving" is just closing the issue (`Closes #n` on merge). The closed issue + the RTM
rows are the record of what shipped.
- **RTM** ([`.specify/rtm.md`](./.specify/rtm.md)) — append one row per `REQ-NNN` when a spec
is approved, each pointing at its issue (`#n`); flip `Status` as tests go green; never delete
a shipped requirement's row.
- **Personas** — update `.specify/personas/*.md` checklists when a recurring blind spot
appears; keep them aligned with the richer `.claude/personas/`.
## 6. Quick-start cheatsheet
**EARS patterns** (every requirement is one of these + a `REQ-NNN` id):
| Pattern | Shape |
|---|---|
| Ubiquitous | `The <system> shall <behavior>.` |
| Event-driven | `When <trigger>, the <system> shall <behavior>.` |
| State-driven | `While <state>, the <system> shall <behavior>.` |
| Optional-feature | `Where <feature/permission present>, the <system> shall <behavior>.` |
| Unwanted-behavior | `If <undesired condition>, then the <system> shall <response>.` |
**File locations:**
| What | Where |
|---|---|
| Non-negotiable rules | `.specify/constitution.md` |
| Agent rules (read every time) | `.specify/AGENTS.md` |
| Templates (writing aids) | `.specify/templates/{feature-spec,adr,threat-model,api-contract-stub}.md` |
| Persona checklists | `.specify/personas/*.md` |
| In-flight feature spec | the **Gitea issue body** (not a committed file) |
| Worked example (template/reference) | `.specify/features/_example/` |
| Traceability matrix | `.specify/rtm.md` (`REQ-ID → issue # → test`) |
| ADR archive | `docs/adr/NNN-*.md` |
| Issue templates | `.gitea/ISSUE_TEMPLATE/{feature,bug}.md` |
| CI gate | `.gitea/workflows/sdd-gate.yml` |
**Before you mark a feature done:** every `REQ-NNN` has a green test, the RTM Status is
`Done`, all six personas APPROVE, `npm run lint` and the targeted tests pass, and
`npm run generate:api` has been run if the backend model changed.
**Commands:**
```bash
# validate an OpenAPI contract locally (if you drafted one — same as CI)
npx @stoplight/spectral-cli lint <your-contract>.yaml
# regenerate the TS client after a backend model/endpoint change
cd frontend && npm run generate:api # backend must run with --spring.profiles.active=dev
```