feat(sdd): add .specify scaffold — constitution, AGENTS, personas, templates, example, RTM
Introduces the SDD root: a v1.0.0 constitution and machine-readable AGENTS.md grounded in the project's real conventions; six EARS-aware persona spec-review checklists that cross-reference .claude/personas/; feature-spec/ADR/threat-model/ api-contract templates; a fully worked _example feature; a living RTM; and an adrs/ pointer that reuses the existing docs/adr/ archive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
42
.specify/templates/adr.md
Normal file
42
.specify/templates/adr.md
Normal file
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
ADR template. ADRs live in the existing archive: docs/adr/NNN-kebab-title.md.
|
||||
Verify the next free NNN against `ls docs/adr/` on disk (parallel worktrees make
|
||||
issue-body numbers stale). An ADR is IMMUTABLE once Status = Accepted — to change a
|
||||
decision, write a NEW higher-numbered ADR and set this one's Status to Superseded.
|
||||
This header mirrors the existing archive style (see docs/adr/040-*.md). Delete this comment.
|
||||
-->
|
||||
|
||||
# ADR-NNN — <Short decision title>
|
||||
|
||||
**Status:** Proposed <!-- Proposed | Accepted | Deprecated | Superseded by ADR-MMM -->
|
||||
**Date:** <YYYY-MM-DD>
|
||||
**Issue:** #<n> <!-- the Gitea issue / feature this decision serves -->
|
||||
|
||||
## Context
|
||||
|
||||
<The forces at play: what problem demands a decision now, the constraints from the
|
||||
constitution and existing ADRs, and why the status quo is insufficient. State facts, not
|
||||
the chosen answer.>
|
||||
|
||||
## Decision
|
||||
|
||||
<The decision, stated in active voice as something the project now does. Number sub-decisions
|
||||
(### 1, ### 2, …) if the ADR commits several related choices, matching the existing archive.>
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Option | Pros | Cons | Reason rejected |
|
||||
|---|---|---|---|
|
||||
| <chosen — name it> | <pros> | <cons> | **Chosen** |
|
||||
| <alternative A> | <pros> | <cons> | <why not> |
|
||||
| <alternative B> | <pros> | <cons> | <why not> |
|
||||
|
||||
## Consequences
|
||||
|
||||
<What becomes easier and what becomes harder. Include the obligations this decision places
|
||||
on future work (migrations forward-only, tests that must exist, guards that must hold), and
|
||||
any new coupling introduced.>
|
||||
|
||||
## References
|
||||
|
||||
- <constitution §, related ADRs, issue links, external docs>
|
||||
94
.specify/templates/api-contract-stub.md
Normal file
94
.specify/templates/api-contract-stub.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# API Contract Stub
|
||||
|
||||
This project is **REST + OpenAPI**. The backend serves the live spec via springdoc at
|
||||
`http://localhost:8080/v3/api-docs` (dev profile only), and the frontend generates its
|
||||
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`.
|
||||
|
||||
## 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.
|
||||
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).
|
||||
|
||||
## OpenAPI 3.1 skeleton
|
||||
|
||||
```yaml
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Familienarchiv API — <feature name>
|
||||
version: 0.0.1-SNAPSHOT
|
||||
description: Design-time contract for <feature>. Source of truth is the generated /v3/api-docs.
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
description: Local backend (dev profile)
|
||||
- url: https://archiv.raddatz.cloud
|
||||
description: Production (behind Caddy)
|
||||
components:
|
||||
securitySchemes:
|
||||
cookieAuth: # Spring Session JDBC — opaque session id in the SESSION cookie
|
||||
type: apiKey
|
||||
in: cookie
|
||||
name: SESSION
|
||||
schemas:
|
||||
ErrorResponse: # shape produced by GlobalExceptionHandler
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: Machine-readable ErrorCode (see ErrorCode.java / errors.ts).
|
||||
example: FORBIDDEN
|
||||
message:
|
||||
type: string
|
||||
# <YourResponseView>: # always a view, never a lazy-collection entity (ADR-036)
|
||||
# type: object
|
||||
# required: [id]
|
||||
# properties:
|
||||
# id: { type: string, format: uuid }
|
||||
security:
|
||||
- cookieAuth: [] # default: every path requires a session unless overridden to []
|
||||
paths:
|
||||
/api/<resource>:
|
||||
post:
|
||||
summary: <create …>
|
||||
operationId: <createResource>
|
||||
security:
|
||||
- cookieAuth: [] # plus @RequirePermission(Permission.X) on the controller
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/<CreateDTO>' }
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/<YourResponseView>' }
|
||||
'400': { description: Validation failed, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
|
||||
'401': { description: Unauthenticated, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
|
||||
'403': { description: Missing permission, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
|
||||
```
|
||||
|
||||
## Validating the contract in CI
|
||||
|
||||
The `sdd-gate.yml` workflow runs, on PRs that touch a `api-contract.yaml`:
|
||||
|
||||
```bash
|
||||
npx @stoplight/spectral-cli lint .specify/features/**/api-contract.yaml
|
||||
```
|
||||
|
||||
Spectral's default OpenAPI ruleset catches malformed specs, missing `operationId`s, and
|
||||
undefined `$ref`s. Add a `.spectral.yaml` at the repo root to tune rules if needed.
|
||||
89
.specify/templates/feature-spec.md
Normal file
89
.specify/templates/feature-spec.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<!--
|
||||
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.
|
||||
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.
|
||||
-->
|
||||
|
||||
# <Feature title — match the Gitea issue: "As a <role> I want <capability> so <reason>">
|
||||
|
||||
## Context & Why
|
||||
|
||||
<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>
|
||||
|
||||
Related: <links to prior issues / ADRs / specs>.
|
||||
|
||||
## User Journey
|
||||
|
||||
<Plain-prose steps the user takes to get value, from the user's perspective — per COLLABORATING.md. Anything not in this journey is out of scope.>
|
||||
|
||||
## Requirements
|
||||
|
||||
> One requirement per line, each with a `REQ-NNN` id and one EARS pattern. Include the
|
||||
> patterns the feature actually needs — do not force all five, but a mutating feature almost
|
||||
> always needs at least one Event-driven and one Unwanted-behavior requirement.
|
||||
|
||||
- **REQ-001** (Ubiquitous) — The `<system component>` shall `<always-true behavior>`.
|
||||
- **REQ-002** (Event-driven) — When `<trigger / endpoint receives X>`, the `<system component>` shall `<response>`.
|
||||
- **REQ-003** (State-driven) — While `<system is in state X>`, the `<system component>` shall `<behavior>`.
|
||||
- **REQ-004** (Optional-feature) — Where `<the caller has Permission.X / a feature flag is set>`, the `<system component>` shall `<behavior>`.
|
||||
- **REQ-005** (Unwanted-behavior) — If `<undesired condition, e.g. caller is unauthenticated / input invalid>`, then the `<system component>` shall `<safe response, e.g. return 401 / ErrorCode.X>`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
> One measurable criterion per REQ-NNN. Numbers, limits, status codes — never adjectives.
|
||||
|
||||
- **REQ-001** — <measurable, e.g. "the response always includes a non-null `id` (UUID)">.
|
||||
- **REQ-002** — <measurable, e.g. "POST returns 201 and the persisted row within the same request">.
|
||||
- **REQ-003** — <measurable>.
|
||||
- **REQ-004** — <measurable, e.g. "a caller without Permission.X receives 403 with ErrorCode.FORBIDDEN">.
|
||||
- **REQ-005** — <measurable, e.g. "an unauthenticated request receives 401 and nothing is persisted">.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- <Explicit boundary statement — the nearest tempting scope creep, named and excluded.>
|
||||
- <…>
|
||||
|
||||
## API / Contract Stub
|
||||
|
||||
<Inline stub OR link to `./api-contract.yaml`. Name the new/changed paths, methods, request/response shapes, status codes, and `@RequirePermission`.>
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
<Entity/schema delta: new tables/columns, constraints, the next free Flyway `V<n>`, and the rollback note. Write "none" if not applicable.>
|
||||
|
||||
## 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.>
|
||||
|
||||
## Open Questions
|
||||
|
||||
> Each item is a BLOCKER until resolved. Empty this list before implementation starts.
|
||||
|
||||
- [ ] <question> — owner: <name>
|
||||
- [ ] <question> — owner: <name>
|
||||
|
||||
## Traceability
|
||||
|
||||
| REQ-ID | Task ID(s) | Test ID(s) | Status |
|
||||
|---|---|---|---|
|
||||
| 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.>
|
||||
|
||||
## Persona Review Results
|
||||
|
||||
| Persona | Status | Key Findings | Resolved |
|
||||
|---|---|---|---|
|
||||
| Requirements Engineer | PENDING | | |
|
||||
| Developer | PENDING | | |
|
||||
| Security | PENDING | | |
|
||||
| DevOps | PENDING | | |
|
||||
| UI/UX | PENDING | | |
|
||||
| Architect | PENDING | | |
|
||||
51
.specify/templates/threat-model.md
Normal file
51
.specify/templates/threat-model.md
Normal file
@@ -0,0 +1,51 @@
|
||||
<!--
|
||||
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 — <Feature name>
|
||||
|
||||
**Feature spec:** [./spec.md](./spec.md)
|
||||
**Date:** <YYYY-MM-DD>
|
||||
**Author:** <name>
|
||||
|
||||
## Data Flow Diagram (text)
|
||||
|
||||
**Actors**
|
||||
- <e.g. Anonymous visitor, Authenticated reader, Authenticated transcriber, Admin, OCR sidecar>
|
||||
|
||||
**Trust boundaries**
|
||||
- TB-1: Browser ⇄ Caddy (public internet ⇄ DMZ)
|
||||
- TB-2: Caddy ⇄ Backend (`:8080`) (DMZ ⇄ app)
|
||||
- TB-3: Backend ⇄ PostgreSQL / MinIO / sidecars (app ⇄ data plane)
|
||||
- <add feature-specific boundaries>
|
||||
|
||||
**Data flows** (source → [boundary] → sink : data)
|
||||
- F-1: Browser → [TB-1,TB-2] → Backend : <request payload>
|
||||
- F-2: Backend → [TB-3] → MinIO : <stored object>
|
||||
- <…>
|
||||
|
||||
## STRIDE
|
||||
|
||||
| Threat Category | Asset / Flow | Threat Description | Mitigation | Likelihood × Impact | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| **S**poofing | <asset> | <e.g. unauthenticated caller forges a request> | <session auth + @RequirePermission> | Low × High | <Open/Mitigated/Accepted> |
|
||||
| **T**ampering | <asset> | <e.g. mass-assignment of createdBy> | <server-set audit fields, no body binding> | Med × High | |
|
||||
| **R**epudiation | <asset> | <e.g. no record of who changed what> | <NOT NULL createdBy/updatedBy audit trail> | Low × Med | |
|
||||
| **I**nformation disclosure | <asset> | <e.g. entity leaks email/hash; raw 500 leaks Hibernate internals> | <view not entity; DomainException.conflict> | Med × High | |
|
||||
| **D**enial of service | <asset> | <e.g. oversized upload / unbounded list> | <size limit, batch cap, pagination> | Med × Med | |
|
||||
| **E**levation of privilege | <asset> | <e.g. reader reaches a write endpoint / IDOR> | <least-privilege Permission, ownership check> | Low × High | |
|
||||
|
||||
## ASTRIDE (only if the feature invokes an AI agent / tool — OCR, NLP, LLM)
|
||||
|
||||
| Threat | Asset / Flow | Threat Description | Mitigation | Likelihood × Impact | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| Prompt Injection | <input to the model> | <untrusted document text steers the model> | <treat model output as untrusted; no auto-exec> | | |
|
||||
| Context Poisoning | <retrieved/shared context> | <attacker plants data that biases later runs> | <scope/provenance of context; validation> | | |
|
||||
| Unsafe Tool Invocation | <tool the agent can call> | <model triggers a privileged action> | <allow-list tools; human-in-loop on mutations> | | |
|
||||
| Reasoning Subversion | <decision the model makes> | <crafted input flips a classification/decision> | <confidence threshold; deterministic guardrail> | | |
|
||||
|
||||
## Residual Risk
|
||||
|
||||
<Threats marked Accepted, who accepted them, and why the residual risk is tolerable.>
|
||||
Reference in New Issue
Block a user