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>
This commit is contained in:
@@ -69,8 +69,9 @@ App port `8080`; management port `8081`. Backend app id: `org.raddatz.familienar
|
||||
|
||||
## Spec-Driven Development
|
||||
|
||||
Before implementing a feature, read its spec at `.specify/features/<name>/spec.md` and its
|
||||
contract at `.specify/features/<name>/api-contract.yaml` if present. The spec's EARS
|
||||
requirements (`REQ-NNN`) are the contract; each maps to a test. Worked reference:
|
||||
[`.specify/features/_example/`](./features/_example/). Full workflow:
|
||||
A feature's spec is its **Gitea issue body** — there is no committed `spec.md`. The issue's
|
||||
EARS requirements (`REQ-NNN`) and acceptance criteria are the contract; each maps to a test,
|
||||
traced in [`.specify/rtm.md`](./rtm.md) (`REQ-ID → issue # → test`). Read the issue before
|
||||
implementing. The committed [`.specify/features/_example/`](./features/_example/) is a
|
||||
template/reference showing the full artifact set, not a live feature. Full workflow:
|
||||
[SPEC_DRIVEN_DEVELOPMENT.md](../SPEC_DRIVEN_DEVELOPMENT.md).
|
||||
|
||||
@@ -19,12 +19,12 @@ structures or hand-wave the hard integration points.
|
||||
4. Are new error conditions expressed as named `ErrorCode`s, with the four-site update (`ErrorCode.java`, `errors.ts`, `getErrorMessage()`, `messages/{de,en,es}.json`) called out as tasks?
|
||||
5. Does every entity/DTO field the spec adds get `@Schema(requiredMode = REQUIRED)` where always-populated, and is `npm run generate:api` listed as a task after backend changes?
|
||||
6. Are frontend changes inside the correct `$lib/<domain>/` boundary, with any cross-domain import either pre-allowed in `eslint.config.js` or flagged for an explicit allow-entry?
|
||||
7. Does each `REQ-NNN` map to a concrete test at the right level (unit / `@WebMvcTest` slice / Playwright E2E per COLLABORATING.md's table) in `tasks.md`?
|
||||
7. Does each `REQ-NNN` imply a concrete test at the right level (unit / `@WebMvcTest` slice / Playwright E2E per COLLABORATING.md's table) — i.e. is it specified concretely enough to write that test?
|
||||
8. Is lazy-loading handled — does any returned entity with a lazy collection get a view (ADR-036) instead of being serialized raw?
|
||||
9. Does the design avoid premature abstraction (KISS over DRY) — no new base class/util introduced before a third caller exists?
|
||||
10. Are data-model changes expressed as a single forward-only Flyway migration with the next free `V<n>` number verified against disk?
|
||||
11. Does the spec avoid backwards-compat shims for code paths that have no existing callers?
|
||||
12. Is the `tasks.md` decomposition red/green-ordered — a failing test task precedes each implementation task?
|
||||
12. Are the requirements decomposable into a red/green-ordered task list — each behavior small enough that a failing test can precede its implementation?
|
||||
|
||||
## EARS patterns to watch for
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
# Requirements Traceability Matrix (RTM)
|
||||
|
||||
> Living document. One row per `REQ-NNN` across all in-flight and shipped features. It links
|
||||
> a requirement to the design that realises it, the code that implements it, and the
|
||||
> test(s) that prove it — so any requirement can be traced end to end, and any orphan
|
||||
> (a requirement with no test, or a test with no requirement) is visible.
|
||||
> Living document. One row per `REQ-NNN` across all in-flight and shipped features. The spec
|
||||
> itself lives in the **Gitea issue** (issue-only — there is no committed `spec.md`); this
|
||||
> matrix is the part of the spec that *is* committed: it links each requirement to its issue,
|
||||
> the code that implements it, and the test(s) that prove it — so any requirement traces end
|
||||
> to end, and any orphan (a requirement with no test) is visible on `main`.
|
||||
|
||||
## How to update
|
||||
|
||||
1. When a `spec.md` is approved, copy its `## Traceability` rows here with `Status: Planned`.
|
||||
2. As tasks land, fill `Implementation File(s)` and flip `Status` → `In progress` → `Done`.
|
||||
3. `REQ-ID`s are **scoped per feature**, so always qualify with the Feature column — `REQ-001`
|
||||
in *avatar* is not `REQ-001` in another feature.
|
||||
4. The `sdd-gate.yml` CI job (`traceability-check`) warns (non-blocking, for now) when a
|
||||
`spec.md` contains a `REQ-NNN` that does not appear in this file. Keep it in sync to keep
|
||||
the warning quiet; it flips to blocking once adoption settles (see the workflow's TODO).
|
||||
1. When a feature's issue is approved (via `/review-issue`), add one row per `REQ-NNN` with the
|
||||
`Issue` set to the Gitea issue number and `Status: Planned`. Commit these rows on the feature
|
||||
branch (they merge with the feature's PR).
|
||||
2. As tasks land, fill `Implementation File(s)` + `Test(s)` and flip `Status` →
|
||||
`In progress` → `Done`.
|
||||
3. `REQ-ID`s are **scoped per feature**, so always read them together with the `Issue` column —
|
||||
`REQ-001` for issue #142 is not `REQ-001` for issue #150.
|
||||
4. The `sdd-gate.yml` CI job (`rtm-check`) warns (non-blocking, for now) when a row is missing
|
||||
its `Issue` or `Test(s)`. It flips to blocking once adoption settles (see the workflow's TODO).
|
||||
|
||||
## Status legend
|
||||
|
||||
@@ -21,16 +24,16 @@
|
||||
|
||||
## Matrix
|
||||
|
||||
| REQ-ID | Requirement Summary | Feature | Design Artifact | Implementation File(s) | Test(s) | Status |
|
||||
| REQ-ID | Requirement Summary | Issue | Feature | Implementation File(s) | Test(s) | Status |
|
||||
|---|---|---|---|---|---|---|
|
||||
| REQ-001 | Store avatar at `avatars/{userId}`, overwrite | profile-picture-upload (_example) | features/_example/design.md; adr-001 | `UserService` (planned) | `UserServiceAvatarTest#storesUnderUserKey`, `#replaceLeavesNoOrphan` | Planned |
|
||||
| REQ-002 | Upload self avatar → 200 + avatarUrl | profile-picture-upload (_example) | features/_example/design.md; api-contract.yaml | `UserAvatarController`, `UserService` (planned) | `UserAvatarControllerTest#uploadReturnsAvatarUrl` | Planned |
|
||||
| REQ-003 | Delete self avatar → avatarUrl null | profile-picture-upload (_example) | features/_example/api-contract.yaml | `UserAvatarController` (planned) | `UserAvatarControllerTest#deleteClearsAvatar` | Planned |
|
||||
| REQ-004 | No avatar → null + initials placeholder | profile-picture-upload (_example) | features/_example/design.md | `UserProfileView`, avatar component (planned) | `avatar-placeholder.svelte.spec.ts` | Planned |
|
||||
| REQ-005 | ADMIN_USER may delete others' avatar | profile-picture-upload (_example) | features/_example/api-contract.yaml | `UserAvatarController` (planned) | `UserAvatarControllerTest#adminDeletesOthersAvatar` | Planned |
|
||||
| REQ-006 | Unauthenticated → 401, store nothing | profile-picture-upload (_example) | features/_example/threat-model.md | `SecurityConfig`, controller (planned) | `UserAvatarControllerTest#unauthenticatedReturns401` | Planned |
|
||||
| REQ-007 | Non-image → 400 UNSUPPORTED_FILE_TYPE | profile-picture-upload (_example) | features/_example/design.md | `UserService` (planned) | `UserAvatarControllerTest#rejectsNonImage` | Planned |
|
||||
| REQ-008 | Over 2 MB → 400 AVATAR_TOO_LARGE | profile-picture-upload (_example) | features/_example/design.md | `UserService`, `ErrorCode` (planned) | `UserAvatarControllerTest#rejectsOversize` | Planned |
|
||||
| REQ-009 | Non-admin on others → 403 FORBIDDEN | profile-picture-upload (_example) | features/_example/threat-model.md | `UserAvatarController` (planned) | `UserAvatarControllerTest#nonAdminForbiddenOnOthers` | Planned |
|
||||
| REQ-001 | Store avatar at `avatars/{userId}`, overwrite | #example | profile-picture-upload (_example) | `UserService` (planned) | `UserServiceAvatarTest#storesUnderUserKey`, `#replaceLeavesNoOrphan` | Planned |
|
||||
| REQ-002 | Upload self avatar → 200 + avatarUrl | #example | profile-picture-upload (_example) | `UserAvatarController`, `UserService` (planned) | `UserAvatarControllerTest#uploadReturnsAvatarUrl` | Planned |
|
||||
| REQ-003 | Delete self avatar → avatarUrl null | #example | profile-picture-upload (_example) | `UserAvatarController` (planned) | `UserAvatarControllerTest#deleteClearsAvatar` | Planned |
|
||||
| REQ-004 | No avatar → null + initials placeholder | #example | profile-picture-upload (_example) | `UserProfileView`, avatar component (planned) | `avatar-placeholder.svelte.spec.ts` | Planned |
|
||||
| REQ-005 | ADMIN_USER may delete others' avatar | #example | profile-picture-upload (_example) | `UserAvatarController` (planned) | `UserAvatarControllerTest#adminDeletesOthersAvatar` | Planned |
|
||||
| REQ-006 | Unauthenticated → 401, store nothing | #example | profile-picture-upload (_example) | `SecurityConfig`, controller (planned) | `UserAvatarControllerTest#unauthenticatedReturns401` | Planned |
|
||||
| REQ-007 | Non-image → 400 UNSUPPORTED_FILE_TYPE | #example | profile-picture-upload (_example) | `UserService` (planned) | `UserAvatarControllerTest#rejectsNonImage` | Planned |
|
||||
| REQ-008 | Over 2 MB → 400 AVATAR_TOO_LARGE | #example | profile-picture-upload (_example) | `UserService`, `ErrorCode` (planned) | `UserAvatarControllerTest#rejectsOversize` | Planned |
|
||||
| REQ-009 | Non-admin on others → 403 FORBIDDEN | #example | profile-picture-upload (_example) | `UserAvatarController` (planned) | `UserAvatarControllerTest#nonAdminForbiddenOnOthers` | Planned |
|
||||
|
||||
<!-- Append real features below this line. Keep the header row above. -->
|
||||
<!-- Append real features below this line, one row per REQ-NNN, with the real issue number. Keep the header row above. -->
|
||||
|
||||
@@ -6,19 +6,21 @@ TypeScript client from it with `npm run generate:api` (`openapi-typescript` →
|
||||
`frontend/src/lib/generated/api.ts`). There is no GraphQL in this stack.
|
||||
|
||||
> **The live spec is generated from the Java controllers — it is the source of truth.** A
|
||||
> hand-written contract under `.specify/features/<name>/api-contract.yaml` is a *design
|
||||
> artifact*: it pins the intended shape during spec review and is checked against the
|
||||
> generated spec once the endpoint exists. Keep it OpenAPI **3.1**, and keep
|
||||
> `@Schema(requiredMode = REQUIRED)` on the Java side as the real driver of `required`.
|
||||
> hand-written stub is a *design artifact*: it pins the intended shape during spec review.
|
||||
> Issue-only: paste the stub inline into the issue's `## API / Contract Stub` section. Keep it
|
||||
> OpenAPI **3.1**, and keep `@Schema(requiredMode = REQUIRED)` on the Java side as the real
|
||||
> driver of `required`.
|
||||
|
||||
## How to use this stub
|
||||
|
||||
1. Copy the skeleton below to `.specify/features/<name>/api-contract.yaml`.
|
||||
2. Fill in the paths/methods/schemas your feature adds. Every mutating path documents the
|
||||
`403`/`401` responses and the `cookieAuth` security requirement (matching the real
|
||||
`@RequirePermission` gate).
|
||||
3. The `sdd-gate.yml` CI job lints any changed `api-contract.yaml` with Spectral
|
||||
(`npx @stoplight/spectral-cli lint`). Run it locally the same way before pushing.
|
||||
1. Fill in the skeleton below with the paths/methods/schemas your feature adds, and paste it
|
||||
into the issue's `## API / Contract Stub` section.
|
||||
2. Every mutating path documents the `403`/`401` responses and the `cookieAuth` security
|
||||
requirement (matching the real `@RequirePermission` gate).
|
||||
3. If you prefer a standalone, lintable file (e.g. for a large contract), commit it on the
|
||||
**feature branch** as `<feature>.openapi.yaml` — the `sdd-gate.yml` CI job lints any
|
||||
committed OpenAPI contract with Spectral (`npx @stoplight/spectral-cli lint`). It never
|
||||
needs to predate the issue.
|
||||
4. After the endpoint ships, run `npm run generate:api` and diff the generated types against
|
||||
this contract; reconcile any drift (the generated spec wins — update the contract).
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<!--
|
||||
Feature Spec template — copy this into a Gitea issue body (or .specify/features/<name>/spec.md).
|
||||
Replace every <placeholder>. Delete this comment block before submitting.
|
||||
Feature Spec template — paste this into the Gitea issue body (issue-only: this IS the spec;
|
||||
there is no committed spec.md). The .gitea/ISSUE_TEMPLATE/feature.md mirror gives the same
|
||||
structure with the right labels. Replace every <placeholder>. Delete this comment before submitting.
|
||||
EARS = Easy Approach to Requirements Syntax. Every requirement uses one of the five patterns
|
||||
shown in ## Requirements and carries a unique REQ-NNN id (three-digit, scoped to THIS feature).
|
||||
Companion artifacts live beside this file: design.md, api-contract.yaml, threat-model.md,
|
||||
adr-NNN-*.md, tasks.md, checklist-results.md.
|
||||
Use plain code-path references (not relative markdown links) — links don't resolve inside a Gitea issue.
|
||||
-->
|
||||
|
||||
# <Feature title — match the Gitea issue: "As a <role> I want <capability> so <reason>">
|
||||
@@ -13,10 +13,10 @@
|
||||
|
||||
<Business motivation in 2–4 sentences: who needs this and why now.>
|
||||
|
||||
Constitution principles this feature depends on:
|
||||
- [§<n> <principle name>](../../constitution.md#<anchor>) — <why it applies>
|
||||
Constitution principles this feature depends on (see `.specify/constitution.md`):
|
||||
- §<n> <principle name> — <why it applies>
|
||||
|
||||
Related: <links to prior issues / ADRs / specs>.
|
||||
Related: <links to prior issues / ADRs>.
|
||||
|
||||
## User Journey
|
||||
|
||||
@@ -51,7 +51,7 @@ Related: <links to prior issues / ADRs / specs>.
|
||||
|
||||
## API / Contract Stub
|
||||
|
||||
<Inline stub OR link to `./api-contract.yaml`. Name the new/changed paths, methods, request/response shapes, status codes, and `@RequirePermission`.>
|
||||
<Inline OpenAPI stub. Name the new/changed paths, methods, request/response shapes, status codes, and `@RequirePermission`. Use the `.specify/templates/api-contract-stub.md` skeleton as a writing aid.>
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
@@ -59,7 +59,7 @@ Related: <links to prior issues / ADRs / specs>.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
<STRIDE categories touched (Spoofing/Tampering/Repudiation/Information disclosure/DoS/Elevation). For AI-agent/tool features, also ASTRIDE. Link to `./threat-model.md` if the feature has a non-trivial attack surface.>
|
||||
<STRIDE categories touched (Spoofing/Tampering/Repudiation/Information disclosure/DoS/Elevation). For AI-agent/tool features, also ASTRIDE. Include an inline STRIDE table (use `.specify/templates/threat-model.md`) if the feature has a non-trivial attack surface.>
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -75,7 +75,7 @@ Related: <links to prior issues / ADRs / specs>.
|
||||
| REQ-001 | <T-1> | <test name> | Planned |
|
||||
| REQ-002 | <T-2> | <test name> | Planned |
|
||||
|
||||
<Mirror these rows into [.specify/rtm.md](../../rtm.md). Fill Task/Test IDs as work progresses.>
|
||||
<After approval, add one committed row per REQ-NNN to `.specify/rtm.md` with this issue's number. Fill Task/Test IDs as work progresses.>
|
||||
|
||||
## Persona Review Results
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<!--
|
||||
Threat model template — STRIDE + ASTRIDE. Lives at .specify/features/<name>/threat-model.md.
|
||||
Required when a feature adds a new trust boundary, handles uploads, exposes a new mutating
|
||||
endpoint, or invokes an AI agent/tool. The Security persona gates this file. Delete this comment.
|
||||
Threat model template — STRIDE + ASTRIDE. WRITING AID: fill this in and paste the result into
|
||||
the issue's "## Security Considerations" section (issue-only — the threat model lives in the
|
||||
issue body, not a committed file). Required when a feature adds a new trust boundary, handles
|
||||
uploads, exposes a new mutating endpoint, or invokes an AI agent/tool. The Security persona
|
||||
gates it during /review-issue. Delete this comment.
|
||||
-->
|
||||
|
||||
# Threat Model — <Feature name>
|
||||
|
||||
**Feature spec:** [./spec.md](./spec.md)
|
||||
**Feature spec:** Gitea issue #<n>
|
||||
**Date:** <YYYY-MM-DD>
|
||||
**Author:** <name>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user