From fdc3e4ffa9aedf6708f42a72ffd85ef211b4d193 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 11:56:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(sdd):=20add=20.specify=20scaffold=20?= =?UTF-8?q?=E2=80=94=20constitution,=20AGENTS,=20personas,=20templates,=20?= =?UTF-8?q?example,=20RTM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .specify/AGENTS.md | 76 ++++++++++ .specify/adrs/README.md | 25 ++++ .specify/constitution.md | 80 +++++++++++ .../adr-001-avatars-reuse-archive-bucket.md | 46 ++++++ .specify/features/_example/api-contract.yaml | 136 ++++++++++++++++++ .../features/_example/checklist-results.md | 76 ++++++++++ .specify/features/_example/design.md | 63 ++++++++ .specify/features/_example/spec.md | 118 +++++++++++++++ .specify/features/_example/tasks.md | 47 ++++++ .specify/features/_example/threat-model.md | 45 ++++++ .specify/personas/architect.md | 40 ++++++ .specify/personas/developer.md | 39 +++++ .specify/personas/devops.md | 39 +++++ .specify/personas/requirements-engineer.md | 43 ++++++ .specify/personas/security.md | 42 ++++++ .specify/personas/ui-ux.md | 39 +++++ .specify/rtm.md | 36 +++++ .specify/templates/adr.md | 42 ++++++ .specify/templates/api-contract-stub.md | 94 ++++++++++++ .specify/templates/feature-spec.md | 89 ++++++++++++ .specify/templates/threat-model.md | 51 +++++++ 21 files changed, 1266 insertions(+) create mode 100644 .specify/AGENTS.md create mode 100644 .specify/adrs/README.md create mode 100644 .specify/constitution.md create mode 100644 .specify/features/_example/adr-001-avatars-reuse-archive-bucket.md create mode 100644 .specify/features/_example/api-contract.yaml create mode 100644 .specify/features/_example/checklist-results.md create mode 100644 .specify/features/_example/design.md create mode 100644 .specify/features/_example/spec.md create mode 100644 .specify/features/_example/tasks.md create mode 100644 .specify/features/_example/threat-model.md create mode 100644 .specify/personas/architect.md create mode 100644 .specify/personas/developer.md create mode 100644 .specify/personas/devops.md create mode 100644 .specify/personas/requirements-engineer.md create mode 100644 .specify/personas/security.md create mode 100644 .specify/personas/ui-ux.md create mode 100644 .specify/rtm.md create mode 100644 .specify/templates/adr.md create mode 100644 .specify/templates/api-contract-stub.md create mode 100644 .specify/templates/feature-spec.md create mode 100644 .specify/templates/threat-model.md diff --git a/.specify/AGENTS.md b/.specify/AGENTS.md new file mode 100644 index 00000000..0f52319a --- /dev/null +++ b/.specify/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md + +Machine-readable rules for AI coding agents (Claude Code, Copilot, Cursor, …) working in +this repository. Read this on every invocation. These are **executable constraints**, not +aspirations. The full rationale lives in [constitution.md](./constitution.md) and the docs +it links — this file does not duplicate it, it points to it. + +If anything here conflicts with the user's explicit instruction, the user wins. Otherwise, +constitution > this file > convenience. + +--- + +## Stack & Versions + +| Layer | Tech | Version | +|---|---|---| +| Backend | Spring Boot (Java, Maven, Jetty, JPA/Hibernate, Flyway, Spring Security, Session JDBC) | Boot 4.0.6 / Java 21 | +| API docs | springdoc-openapi (webmvc-ui), served at `/v3/api-docs` (dev profile only) | — | +| Frontend | SvelteKit / Svelte | 2.60 / 5.43 | +| Frontend lang/style | TypeScript / Tailwind CSS / Paraglide i18n (de/en/es) | TS 5.9 / TW 4.1 | +| API client | `openapi-fetch` + `openapi-typescript` (types generated from the live spec) | — | +| DB | PostgreSQL | 16 | +| Object storage | MinIO (S3-compatible) | — | +| Sidecars | `ocr-service`, `nlp-service` (Python / FastAPI) | Python 3.11 | +| Tests | JUnit + Mockito + `@WebMvcTest` + Testcontainers (backend); Vitest + `vitest-browser-svelte` + Playwright (frontend); Pytest (services) | — | +| Lint/format | ESLint 9 (+ `eslint-plugin-boundaries`) + Prettier; Semgrep (backend) | — | +| CI | Gitea Actions (`.gitea/workflows/`) | — | + +App port `8080`; management port `8081`. Backend app id: `org.raddatz.familienarchiv` / `0.0.1-SNAPSHOT`. + +## Architectural Constraints + +- Controllers call services only — never a repository. (constitution §1.2) +- A service uses only its own domain's repository; reach other domains via their service. (constitution §1.3) +- A new backend domain goes in its own package AND is added to `ArchitectureTest`'s allow-lists in the same change. (constitution §1.7) +- Frontend cross-domain imports are allowed only where `frontend/eslint.config.js` permits; otherwise move shared code to `$lib/shared/`. (constitution §1.4) +- Never serialize a lazy-collection entity across the controller boundary — assemble a view in-transaction. (constitution §1.6 / ADR-036) +- `Person` ≠ `AppUser`; do not add account guards to Person-domain operations. (constitution §1.5) +- Every `POST/PUT/PATCH/DELETE` endpoint has `@RequirePermission(Permission.X)`. Use the enum, never `@PreAuthorize`. (constitution §2.1–2.2) +- Throw only `DomainException.notFound/forbidden/conflict/internal()` from services, each with an `ErrorCode`. (CONTRIBUTING §Error handling) +- Set `createdBy`/`updatedBy` from the session principal in the service — never bind them from a request body. (constitution §2.4) +- Add an `@Schema(requiredMode = REQUIRED)` to every always-populated field. (constitution §3.5) +- Never introduce a new runtime dependency without an ADR in `Accepted` status. (constitution §5.1) +- Render untrusted text with `{...}`; never `{@html}` on user/import data. (constitution §2.5) +- Build dates from ISO strings with a `T12:00:00` suffix. (constitution §3.7) + +## Workflow Rules + +- Always write a failing test before implementation code; confirm it fails, then make it pass, then refactor. (constitution §3.1) +- Run only the specific test file/class locally — never the full suite (it crashes the machine); leave the full sweep to CI. +- Run `npm run generate:api` (in `frontend/`) after ANY backend model or endpoint change — most common cause of TS errors. +- Run `npm run lint` before every commit; a fresh frontend worktree needs `npm install` first or the pre-commit hook fails. +- When adding a new `ErrorCode`, update all four sites at once (constitution §3.6). +- One logical change per commit; reference the Gitea issue (`Closes #n` / `Refs #n`) on the last line. +- Create a git worktree for new issue work — never `git checkout -b` in the main repo while another branch has in-flight work. Avoid `+` in worktree/branch names (breaks vitest browser mode). +- Pull `main` as a separate explicit step before creating a branch. +- Track work as Gitea issues (`http://192.168.178.71:3005`, repo `marcel/familienarchiv`), not todo files. +- Verify ADR and Flyway migration numbers against disk before using one — parallel worktrees make issue-body numbers go stale. + +## Do Not Touch + +- Generated: `frontend/src/lib/generated/api.ts`, `frontend/src/lib/paraglide/`, `frontend/.svelte-kit/`, `frontend/build/`, `backend/target/`. +- Shipped Flyway migrations — add a new forward-only migration instead. +- An `Accepted` ADR — supersede it with a new one. +- `actions/(upload|download)-artifact` version — stays at `@v3` (ADR-014). +- CI guard steps — do not remove/weaken without an ADR. +- `main` — never commit directly; branch + PR only. +- Worktree copies (`familienarchiv-*`, `.worktrees/`) and `data/` — never commit. + +## Spec-Driven Development + +Before implementing a feature, read its spec at `.specify/features//spec.md` and its +contract at `.specify/features//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: +[SPEC_DRIVEN_DEVELOPMENT.md](../SPEC_DRIVEN_DEVELOPMENT.md). diff --git a/.specify/adrs/README.md b/.specify/adrs/README.md new file mode 100644 index 00000000..190052f7 --- /dev/null +++ b/.specify/adrs/README.md @@ -0,0 +1,25 @@ +# ADR archive — see `docs/adr/` + +This project already keeps a mature, permanent ADR archive at +[`../../docs/adr/`](../../docs/adr/) (40+ records, format `NNN-kebab-title.md`). SDD does +**not** introduce a second archive — that would split the project's decision history in two. + +## Where ADRs live + +- **Project-wide decisions** → [`docs/adr/NNN-kebab-title.md`](../../docs/adr/). Use the + next free `NNN` (verify against the directory on disk — parallel worktrees make + issue-body numbers stale). Template: [`../templates/adr.md`](../templates/adr.md). +- **The decision to adopt SDD itself** → + [`docs/adr/041-sdd-adoption.md`](../../docs/adr/041-sdd-adoption.md) (this is the + "ADR-000" the SDD scaffold calls for, numbered to fit the existing sequence). +- **Feature-local decisions** that are only meaningful within one in-flight feature → + beside that feature's spec, e.g. + [`../features/_example/adr-001-avatars-reuse-archive-bucket.md`](../features/_example/adr-001-avatars-reuse-archive-bucket.md). + Promote one to `docs/adr/` if its reach turns out to be project-wide. + +## Rules (unchanged from the existing convention) + +- An ADR is **immutable once `Accepted`** — supersede it with a new, higher-numbered ADR; + set the old one's status to `Superseded by ADR-MMM`. +- Header style matches the existing archive: `# ADR-NNN — Title`, then + `**Status:** / **Date:** / **Issue:**`. diff --git a/.specify/constitution.md b/.specify/constitution.md new file mode 100644 index 00000000..e1dfcdb9 --- /dev/null +++ b/.specify/constitution.md @@ -0,0 +1,80 @@ +# Familienarchiv Constitution + +**Version:** v1.0.0 +**Status:** Ratified +**Date:** 2026-06-13 +**Adoption ADR:** [docs/adr/041-sdd-adoption.md](../docs/adr/041-sdd-adoption.md) + +> The non-negotiable rules of this project. Every spec, every PR, and every AI agent is +> bound by this document. Rules here are deliberately few and absolute — guidance and +> rationale live in [CLAUDE.md](../CLAUDE.md), [COLLABORATING.md](../COLLABORATING.md), +> [CODESTYLE.md](../CODESTYLE.md), [CONTRIBUTING.md](../CONTRIBUTING.md), and the ADR +> archive ([docs/adr/](../docs/adr/)). When this file conflicts with any of those, **this +> file wins** — open an ADR to change it. +> +> Versioning is semantic: **MAJOR** = a rule removed or weakened (existing code may now +> violate the constitution), **MINOR** = a rule added or tightened, **PATCH** = wording +> only. Any change requires the Sync Impact review in the last section. + +--- + +## 1. Architecture Principles + +1. The backend is organised package-by-domain under `org.raddatz.familienarchiv`; a new domain lives in its own package, never spread across layer packages. +2. Controllers never call repositories directly — a controller calls only services. +3. A service accesses only its own domain's repository; cross-domain data is fetched through the other domain's service, never its repository. +4. The frontend mirrors the backend domain split under `frontend/src/lib//`, and cross-domain imports are allowed only where `frontend/eslint.config.js` (`boundaries/dependencies`) permits them. +5. A `Person` (historical subject) and an `AppUser` (login account) are distinct domains and never share an identity or an account guard. +6. Lazy-collection-bearing entities are never serialized across the controller boundary; the owning service assembles an explicit view inside the transaction (see [ADR-036](../docs/adr/036-geschichte-responses-are-views-not-entities.md)). +7. A new backend domain package is added to `ArchitectureTest`'s package allow-lists in the same change that introduces it. +8. Synchronous cross-domain side effects use in-transaction domain events, not direct service-to-service write calls (see [ADR-006](../docs/adr/006-synchronous-domain-events-in-transaction.md)). + +## 2. Security Defaults + +1. Every `POST`, `PUT`, `PATCH`, and `DELETE` endpoint carries `@RequirePermission(Permission.X)` — there is no unguarded mutating endpoint. +2. Authorization uses the typed `Permission` enum and `@RequirePermission`, never magic-string `@PreAuthorize`. +3. All user input is validated at the system boundary (controller / form action), and validation failures return a typed `ErrorCode`, never a raw exception. +4. Audit fields (`createdBy`/`updatedBy`) are set from the session principal inside the service and are never bound from a request body. +5. Untrusted text is rendered through Svelte's default `{...}` escaping; `{@html}` is never used on user- or import-derived strings. +6. Secrets are read only from environment variables (see `.env.example`); no secret, token, password, or DSN is ever committed to the repository or written to a log. +7. Logs never contain PII beyond a stable user/entity UUID — no names, email addresses, document contents, or transcription text. +8. Every state-mutating endpoint is covered by an Unwanted-behavior requirement (EARS `If`) describing the unauthenticated/unauthorized response. +9. A dependency security audit runs on every CI run (`npm audit --audit-level=high` frontend, Semgrep `.semgrep/security.yml` backend) and nightly; a `high` finding blocks merge. + +## 3. Code Quality Rules + +1. All new behavior is driven by a failing test written before the implementation (Red → Green → Refactor); a passing-on-first-run test proves nothing and is rejected. +2. KISS beats DRY — no premature abstraction; an abstraction is introduced only on the third real caller. +3. Each commit does exactly one logical thing and references its Gitea issue (`Closes #n` / `Refs #n`) on the last line of the body. +4. No backwards-compatibility shims are added for code that has no callers. +5. Every entity/DTO field the backend always populates carries `@Schema(requiredMode = REQUIRED)`, and `npm run generate:api` is run after any backend model or endpoint change. +6. A new `ErrorCode` is added in all four places at once: `ErrorCode.java`, `frontend/src/lib/shared/errors.ts`, `getErrorMessage()`, and `messages/{de,en,es}.json`. +7. Dates built from an ISO date string append `T12:00:00` to avoid UTC off-by-one. +8. `npm run lint` (Prettier + ESLint, including the domain boundary rule) passes before every commit. + +## 4. Do-Not-Touch List + +1. Do not edit generated artifacts: `frontend/src/lib/generated/api.ts`, `frontend/src/lib/paraglide/`, `frontend/.svelte-kit/`, `frontend/build/`, `backend/target/`. +2. Do not edit an `Accepted` ADR — supersede it with a new, higher-numbered ADR. +3. Do not upgrade `actions/upload-artifact` / `download-artifact` past `@v3` (Gitea act_runner lacks the v4 protocol — [ADR-014](../docs/adr/014-upload-artifact-v3-pin.md)). +4. Do not remove or weaken a CI guard step (banned-pattern greps, self-tested regexes) without an ADR recording why. +5. Do not commit to `main` directly — all work flows through a branch and a PR. +6. Do not edit a Flyway migration that has shipped; add a new forward-only migration instead. +7. Do not commit the worktree copy directories (`familienarchiv-*`, `.worktrees/`) or `data/`. + +## 5. Dependency Policy + +1. A new runtime dependency (backend `pom.xml` or frontend `dependencies`) requires an ADR in `Accepted` status before it is merged. +2. A new dependency must be version-pinned in the manifest, and any exact pin (no caret) carries a comment stating why it cannot float (see the `@vitest/browser-playwright` pin). +3. Renovate manages dependency-update PRs; a major-version bump is treated as a feature requiring its own spec and review, not an auto-merge. +4. A dependency with an unresolved `high`+ advisory is not merged; it is pinned to a safe version or replaced. + +## 6. Sync Impact + +When this constitution changes, the author MUST, in the same PR: + +1. Bump the **Version** header per the semantic rule above and record the change in [docs/adr/041-sdd-adoption.md](../docs/adr/041-sdd-adoption.md)'s revision log (or a superseding ADR for a MAJOR change). +2. Re-read and reconcile every file that restates a rule changed here: `CLAUDE.md`, `COLLABORATING.md`, `CODESTYLE.md`, `CONTRIBUTING.md`, `.specify/AGENTS.md`, and the affected `.specify/personas/*.md` checklists. +3. Update any `.specify/templates/*` section that quotes a changed rule. +4. Run the `constitution-diff` CI job locally (or read its PR comment) and resolve every file it lists. +5. Announce the version bump in the PR description so reviewers re-read the constitution before approving. diff --git a/.specify/features/_example/adr-001-avatars-reuse-archive-bucket.md b/.specify/features/_example/adr-001-avatars-reuse-archive-bucket.md new file mode 100644 index 00000000..edff1872 --- /dev/null +++ b/.specify/features/_example/adr-001-avatars-reuse-archive-bucket.md @@ -0,0 +1,46 @@ +# ADR-001 (feature-local) — Avatars reuse the archive bucket under an `avatars/` prefix + +**Status:** Accepted +**Date:** 2026-06-13 +**Issue:** # (profile picture upload) + +> **Feature-local ADR.** This decision is scoped to the avatar feature and lives with its +> spec. A decision with project-wide reach is promoted to the permanent archive at +> `docs/adr/` with the next free number. (For the worked example, it stays local.) + +## Context + +Avatars are small binary objects keyed per user. The project already runs MinIO with a +single archive bucket and a `FileService` abstraction used by document uploads. We must +decide where avatar bytes live without adding operational surface that the self-hosted +Compose deployment has to learn about. + +## Decision + +Store each avatar in the **existing archive bucket** under the deterministic key +`avatars/{userId}`, written and read through the existing `FileService`. No new bucket, no +new env var, no new Compose service or bucket-bootstrap step. + +## Alternatives Considered + +| Option | Pros | Cons | Reason rejected | +|---|---|---|---| +| Reuse archive bucket, `avatars/` prefix | No infra change; reuses `FileService`; idempotent overwrite | Mixes avatars with documents in one bucket | **Chosen** — least operational cost; prefix keeps them logically separate | +| Dedicated `avatars` bucket | Clean separation; independent lifecycle/policy | New bucket + bootstrap step + env var + Compose idempotency test | Operational overhead not justified for small, low-value objects | +| Store bytes in PostgreSQL (`bytea`) | One datastore; transactional with the row | Bloats the DB and backups; streaming images via JPA is awkward | Wrong tool; MinIO already exists for blobs | +| External CDN / object store | Offloads bandwidth | New third-party dependency + secret + ADR; conflicts with self-hosted goal | Contradicts the self-hosted infrastructure stance | + +## Consequences + +- No deployment change ships with this feature — only a Flyway column and code. +- Avatars and documents share a bucket; any future per-object lifecycle policy must filter + by the `avatars/` prefix. +- The deterministic key (`avatars/{userId}`, no random suffix) makes replace an overwrite, + so there is no orphan-cleanup obligation (REQ-001). +- If avatars later need independent retention or a public CDN, this ADR is superseded by a + project-wide ADR in `docs/adr/`. + +## References + +- [`./spec.md`](./spec.md), [`./design.md`](./design.md) +- [constitution §5 Dependency Policy](../../constitution.md#5-dependency-policy) diff --git a/.specify/features/_example/api-contract.yaml b/.specify/features/_example/api-contract.yaml new file mode 100644 index 00000000..19685e1d --- /dev/null +++ b/.specify/features/_example/api-contract.yaml @@ -0,0 +1,136 @@ +openapi: 3.1.0 +info: + title: Familienarchiv API — Profile picture upload + version: 0.0.1-SNAPSHOT + description: > + Design-time contract for the avatar feature (.specify/features/_example). + Source of truth once shipped 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: + type: apiKey + in: cookie + name: SESSION + schemas: + ErrorResponse: + type: object + required: [code, message] + properties: + code: + type: string + example: AVATAR_TOO_LARGE + message: + type: string + UserProfileView: + type: object + required: [id, displayName] + properties: + id: + type: string + format: uuid + displayName: + type: string + avatarUrl: + type: [string, "null"] + description: Authenticated proxy path (/api/users/{id}/avatar) when an avatar exists, else null. + example: /api/users/3f1c.../avatar +security: + - cookieAuth: [] +paths: + /api/users/me/avatar: + post: + summary: Upload or replace the current user's avatar + operationId: uploadMyAvatar + security: + - cookieAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: [file] + properties: + file: + type: string + format: binary + description: PNG or JPEG, max 2 MB. + responses: + '200': + description: Avatar stored; updated profile returned. + content: + application/json: + schema: { $ref: '#/components/schemas/UserProfileView' } + '400': + description: Unsupported type (UNSUPPORTED_FILE_TYPE) or too large (AVATAR_TOO_LARGE). + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + '401': + description: Unauthenticated (UNAUTHORIZED). + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + delete: + summary: Remove the current user's avatar + operationId: deleteMyAvatar + security: + - cookieAuth: [] + responses: + '200': + description: Avatar removed; profile returned with avatarUrl null. + content: + application/json: + schema: { $ref: '#/components/schemas/UserProfileView' } + '401': + description: Unauthenticated (UNAUTHORIZED). + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + /api/users/{id}/avatar: + get: + summary: Stream a user's avatar image (authenticated proxy) + operationId: getUserAvatar + security: + - cookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: { type: string, format: uuid } + responses: + '200': + description: Image bytes. + content: + image/png: { schema: { type: string, format: binary } } + image/jpeg: { schema: { type: string, format: binary } } + '401': { description: Unauthenticated, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } } + '404': { description: User has no avatar, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } } + delete: + summary: Remove another user's avatar (admin only) + operationId: deleteUserAvatar + description: Requires Permission.ADMIN_USER (enforced by @RequirePermission on the controller). + security: + - cookieAuth: [] + parameters: + - name: id + in: path + required: true + schema: { type: string, format: uuid } + responses: + '200': + description: Avatar removed. + content: + application/json: + schema: { $ref: '#/components/schemas/UserProfileView' } + '401': { description: Unauthenticated, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } } + '403': + description: Caller lacks ADMIN_USER (FORBIDDEN). + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } diff --git a/.specify/features/_example/checklist-results.md b/.specify/features/_example/checklist-results.md new file mode 100644 index 00000000..d97fe1a8 --- /dev/null +++ b/.specify/features/_example/checklist-results.md @@ -0,0 +1,76 @@ +# Persona Review Results — Profile picture upload + +> Captured from the six persona spec reviews (the comments that, in a real feature, are +> posted on the Gitea issue). This is the worked example of what a completed review round +> looks like. All personas APPROVE; the two findings raised were folded into the spec +> before approval. + +## Summary + +| Persona | Verdict | Blocking FAILs | Notes | +|---|---|---|---| +| Requirements Engineer | APPROVE | none | — | +| Developer | APPROVE | none | — | +| Security | APPROVE | none (2 resolved) | See F-SEC-1, F-SEC-2 | +| DevOps | APPROVE | none | — | +| UI/UX | APPROVE | none (1 resolved) | See F-UX-1 | +| Architect | APPROVE | none (1 resolved) | See F-ARCH-1 | + +--- + +## ### Security — Spec Review + +| # | Item | Status | Note | +|---|---|---|---| +| 1 | All mutating endpoints have authn + authz `If` clauses | PASS | REQ-006 (401), REQ-009 (403) | +| 2 | Each mutating endpoint names least-privilege `Permission` | PASS | `me` = authenticated; `{id}` = ADMIN_USER | +| 3 | Audit fields server-set, forbidden in body | PASS | `avatarObjectKey` server-set (design.md) | +| 4 | IDOR surfaces addressed | PASS | `/{id}` gated by ADMIN_USER + ownership | +| 5 | Untrusted content rendered safely | PASS | image bytes via proxy + `nosniff` | +| 6 | Upload: type allow-list + size + bytes | PASS | REQ-007 (PNG/JPEG), REQ-008 (2 MB) | +| 7 | No entity internals leaked | PASS | `UserProfileView`, not `AppUser` | +| 8 | Conflicts → 409 not raw 500 | N/A | no optimistic-lock surface here | +| 9 | threat-model.md present & STRIDE-complete | PASS | [threat-model.md](./threat-model.md) | +| 10 | ASTRIDE if AI tool used | N/A | no AI agent | +| 11 | Secrets from env only | PASS | none introduced | +| 12 | Logs PII-free | PASS | user UUID only | +| 13 | New dependency has ADR + clean audit | N/A | no new dependency | + +**F-SEC-1 (resolved):** initial draft exposed a public S3 URL for `avatarUrl` → +information disclosure. Resolved: authenticated proxy `GET /api/users/{id}/avatar`. +**F-SEC-2 (resolved):** initial draft bound `avatarObjectKey` from the request body → +mass-assignment. Resolved: server-set only. +**Verdict: APPROVE.** + +## ### UI/UX — Spec Review + +| # | Item | Status | Note | +|---|---|---|---| +| 1 | Every interaction state described | PASS | idle/preview/uploading/error/done (T-10) | +| 2 | Strings via Paraglide i18n | PASS | T-8 | +| 3 | Reuses design tokens/components | PASS | placeholder uses existing initials pattern | +| 4 | Responsive per device split | PASS | control usable on phone + laptop | +| 5 | Errors via `getErrorMessage(code)` | PASS | UNSUPPORTED_FILE_TYPE / AVATAR_TOO_LARGE | +| 6 | Keyboard + screen-reader | PASS | labelled file input, alt text on image | +| 7 | Acceptance criteria measurable | PASS | sizes, status codes | +| 8 | E2E scenario per journey | PASS | T-12 | +| 9 | Confirmation for destructive action | PASS | remove asks to confirm | +| 10 | Safe rendering + image dims | PASS | fixed dims avoid layout shift | +| 11 | Live routes verified | PASS | `/profile`, `/users/[id]` exist | +| 12 | Token theming respected | PASS | semantic tokens | + +**F-UX-1 (resolved):** no loading state in first draft → spinner during upload added (REQ-... covered by state set in T-10). +**Verdict: APPROVE.** + +## ### Architect — Spec Review + +Key items PASS. **F-ARCH-1 (resolved):** bucket choice was undocumented → captured in +[adr-001-avatars-reuse-archive-bucket.md](./adr-001-avatars-reuse-archive-bucket.md). No new +domain, no boundary crossing, Person/AppUser separation intact. **Verdict: APPROVE.** + +## ### Requirements Engineer / Developer / DevOps — Spec Review + +All checklist items PASS (see each persona's checklist in `.specify/personas/`). RE: 9 REQ +ids, all EARS-formed, every limit has an `If`. Developer: reuses `FileService`/`UserService`, +`AVATAR_TOO_LARGE` four-site update is T-1. DevOps: V78 forward-only + rollback note, no new +bucket/env var, idempotent overwrite. **All three: APPROVE.** diff --git a/.specify/features/_example/design.md b/.specify/features/_example/design.md new file mode 100644 index 00000000..08816b48 --- /dev/null +++ b/.specify/features/_example/design.md @@ -0,0 +1,63 @@ +# Design — Profile picture upload + +> Companion to [`./spec.md`](./spec.md). The spec says *what*; this says *how*, and records +> the alternatives weighed for the non-obvious choices. + +## Component overview + +``` +ProfileSettings.svelte ──► +page.server.ts (form action) + (preview, validate) │ POST /api/users/me/avatar (multipart) + ▼ + UserAvatarController ── @RequirePermission(authenticated) + │ ownership/admin check for /{id} + ▼ + UserService.setAvatar(userId, MultipartFile) + │ validate type+size → ErrorCode + ├──► FileService.put("avatars/{userId}", bytes) (MinIO) + └──► userRepository.save(user.avatarObjectKey=key) + ▼ + UserProfileView { …, avatarUrl } +``` + +Reads: `GET /api/users/{id}/avatar` streams the object through the authenticated API +(`FileService.get`), so no public S3 URL is ever exposed. `avatarUrl` in the view is simply +`/api/users/{id}/avatar` when a key exists, else `null`. + +## Key decisions + +| Decision | Choice | Why | +|---|---|---| +| Where avatars live | Existing archive bucket, `avatars/{userId}` prefix | No new bucket/env var/Compose change — see [ADR-001](./adr-001-avatars-reuse-archive-bucket.md). | +| URL exposure | Authenticated proxy endpoint, not a signed/public URL | Same auth surface as the rest of the API; no key leakage (Information disclosure). | +| Object key | Deterministic `avatars/{userId}` (no random suffix) | A new upload overwrites the old object — no orphan-cleanup job needed (REQ-001). | +| `avatarObjectKey` binding | Server-set in `UserService` only | Never bound from request body — prevents pointing a user's avatar at an arbitrary object (Tampering / CWE-639). | +| Validation site | `UserService`, boundary-only | Type + size checked once, at the service boundary, mapped to `ErrorCode` (constitution §2.3). | + +## Layering & conventions + +- Controller → `UserService` only; `UserService` owns `userRepository` and calls + `FileService` (its public API), never another domain's repository. (constitution §1.2–1.3) +- New `ErrorCode.AVATAR_TOO_LARGE` requires the four-site update (see `tasks.md` T-1). +- `UserProfileView.avatarUrl` is `String` (nullable) with `@Schema` describing the proxy + path; not marked `requiredMode = REQUIRED` because it is legitimately null (REQ-004). +- After backend changes: `npm run generate:api` regenerates `avatarUrl` into the TS types. + +## Non-functional notes + +- Size cap (2 MB, REQ-008) is enforced **before** the object touches MinIO — the multipart + is read into a bounded buffer; Spring's `spring.servlet.multipart.max-file-size` is set to + a matching ceiling so an oversized body is rejected at the container edge too. +- No N+1 risk: the profile view derives `avatarUrl` from the already-loaded `avatarObjectKey` + column; no extra query, no S3 round-trip on list/read paths. +- The proxy `GET` streams bytes (no full-buffer) and sets a short `Cache-Control` so an + updated avatar propagates quickly. + +## Test strategy (maps to tasks.md) + +| Level | What | Tooling | +|---|---|---| +| Unit | `UserService.setAvatar` validation + storage interactions | JUnit + Mockito (mock `FileService`) | +| Slice | controller auth, status codes, error codes | `@WebMvcTest` | +| E2E | upload → preview → confirm → avatar visible; remove → initials | Playwright | +| Component | initials placeholder when `avatarUrl` is null | `vitest-browser-svelte` (`*.svelte.spec.ts`) | diff --git a/.specify/features/_example/spec.md b/.specify/features/_example/spec.md new file mode 100644 index 00000000..e1acce3c --- /dev/null +++ b/.specify/features/_example/spec.md @@ -0,0 +1,118 @@ +# As a user I want to upload a profile picture so other family members recognise me + +> **This is the canonical worked example for SDD in this repo.** It is fictional but +> realistic, chosen because no real avatar feature exists in the codebase. Use it as the +> reference shape for a real `spec.md`. Every section is filled — no placeholders. + +## Context & Why + +Readers and transcribers collaborate in threads and on document comments, but every user is +currently represented by initials only. Letting a user upload a small profile picture makes +the activity feed, comments, and the public user profile page (`/users/[id]`) more personal +and easier to scan — directly serving the family-archive product goal of feeling like a +shared family space, not a database. + +Constitution principles this feature depends on: +- [§2 Security Defaults](../../constitution.md#2-security-defaults) — upload validation, permission gating, no PII in logs. +- [§1.3 services own their repository](../../constitution.md#1-architecture-principles) — avatar storage goes through `UserService` + `FileService`, not a controller. +- [§3.6 ErrorCode four-site rule](../../constitution.md#3-code-quality-rules) — introduces `AVATAR_TOO_LARGE`. + +Related: builds on the existing `FileService` (MinIO) used by `Document` uploads. + +## User Journey + +A logged-in user opens their profile settings (`/profile`), clicks "Profilbild ändern", +selects a PNG or JPEG from their device, sees an instant preview, and confirms. The picture +replaces their initials everywhere their name appears. They can later remove it and fall +back to initials. An admin (with `ADMIN_USER`) can remove an inappropriate picture from +another user's account from the admin user view. + +## Requirements + +- **REQ-001** (Ubiquitous) — The user service shall store each profile picture as a single object in the existing archive bucket under the key `avatars/{userId}`, overwriting any previous object for that user. +- **REQ-002** (Event-driven) — When an authenticated user sends `POST /api/users/me/avatar` with a valid image, the user service shall store the image, set the user's `avatarObjectKey`, and return the updated profile view including a non-null `avatarUrl`. +- **REQ-003** (Event-driven) — When an authenticated user sends `DELETE /api/users/me/avatar`, the user service shall delete the stored object, clear `avatarObjectKey`, and return the profile view with `avatarUrl = null`. +- **REQ-004** (State-driven) — While a user has no stored avatar, the profile view for that user shall return `avatarUrl = null` and the frontend shall render the initials placeholder. +- **REQ-005** (Optional-feature) — Where the caller holds `Permission.ADMIN_USER`, the user service shall allow `DELETE /api/users/{id}/avatar` to remove another user's avatar. +- **REQ-006** (Unwanted-behavior) — If the request to any avatar endpoint is unauthenticated, then the system shall return `401` with `ErrorCode.UNAUTHORIZED` and store or delete nothing. +- **REQ-007** (Unwanted-behavior) — If the uploaded file's content type is not `image/png` or `image/jpeg`, then the user service shall return `400 ErrorCode.UNSUPPORTED_FILE_TYPE` and store nothing. +- **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 via `/api/users/{id}/avatar`, then the system shall return `403 ErrorCode.FORBIDDEN` and modify nothing. + +## Acceptance Criteria + +- **REQ-001** — After a successful upload, exactly one object exists at `avatars/{userId}`; a second upload leaves exactly one object (no orphan), verified by a `FileService` interaction test. +- **REQ-002** — `POST /api/users/me/avatar` with a 100 KB PNG returns `200` and a body whose `avatarUrl` is a non-null string; the persisted `app_users.avatar_object_key` equals `avatars/{userId}`. +- **REQ-003** — `DELETE /api/users/me/avatar` returns `200`, the object is gone, and the response `avatarUrl` is `null`. +- **REQ-004** — `GET` profile view for a user with `avatar_object_key IS NULL` returns `avatarUrl: null`; the rendered component shows a 2-letter initials placeholder (Playwright). +- **REQ-005** — An `ADMIN_USER` caller deleting another user's avatar returns `200`; the target's `avatar_object_key` becomes `NULL`. +- **REQ-006** — An unauthenticated `POST`/`DELETE` returns `401`; bucket object count is unchanged. +- **REQ-007** — A `text/plain` or `application/pdf` upload returns `400 UNSUPPORTED_FILE_TYPE`; bucket object count is unchanged. +- **REQ-008** — A 2.1 MB PNG returns `400 AVATAR_TOO_LARGE`; bucket object count is unchanged. +- **REQ-009** — A non-admin caller targeting another user's id returns `403 FORBIDDEN`; the target's `avatar_object_key` is unchanged. + +## Out of Scope + +- Image cropping, resizing, or transformation — the client sends a final image; the server stores it verbatim within the size limit. +- Avatars for historical `Person` entities — this feature is for `AppUser` accounts only (Person ≠ AppUser). +- Gravatar / external avatar providers. +- Animated formats (GIF/WebP) — PNG and JPEG only in v1. + +## API / Contract Stub + +See [`./api-contract.yaml`](./api-contract.yaml). Endpoints: +`POST /api/users/me/avatar` (multipart), `DELETE /api/users/me/avatar`, +`DELETE /api/users/{id}/avatar` (ADMIN_USER). The profile view gains an optional +`avatarUrl: string | null`. All mutating endpoints carry `@RequirePermission` — `me` +endpoints require an authenticated session; the `{id}` delete requires `ADMIN_USER`. + +## Data Model Changes + +- Add nullable `avatar_object_key VARCHAR(512)` to `app_users`. +- Flyway `V78__add_app_user_avatar_object_key.sql` (next free number — verify against + `backend/src/main/resources/db/migration/` on disk before committing). +- **Rollback:** forward-only. Reverse manually with `ALTER TABLE app_users DROP COLUMN avatar_object_key;`. The MinIO `avatars/` objects are orphaned but harmless on rollback and can be pruned with `mc rm --recursive`. + +## Security Considerations + +STRIDE categories touched: **Tampering** (mass-assignment of `avatarObjectKey` if bound from +body), **Elevation of privilege** (a non-admin modifying another user's avatar — REQ-009), +**Denial of service** (oversized upload — REQ-008), **Information disclosure** (avatar URL +must not expose a signed key that bypasses auth). No AI agent involved, so ASTRIDE does not +apply. Full analysis: [`./threat-model.md`](./threat-model.md). + +## Open Questions + +> All resolved before implementation. + +- [x] Public or signed avatar URL? — **Resolved:** served through an authenticated + `GET /api/users/{id}/avatar` proxy (same auth as the rest of the API), not a public S3 URL. +- [x] New bucket or reuse archive bucket? — **Resolved:** reuse the archive bucket under an + `avatars/` prefix; see [`./adr-001-avatars-reuse-archive-bucket.md`](./adr-001-avatars-reuse-archive-bucket.md). + +## Traceability + +| REQ-ID | Task ID(s) | Test ID(s) | Status | +|---|---|---|---| +| REQ-001 | T-3 | `UserServiceAvatarTest#storesUnderUserKey`, `…#replaceLeavesNoOrphan` | Planned | +| REQ-002 | T-4 | `UserAvatarControllerTest#uploadReturnsAvatarUrl` | Planned | +| REQ-003 | T-5 | `UserAvatarControllerTest#deleteClearsAvatar` | Planned | +| REQ-004 | T-7 | `avatar-placeholder.svelte.spec.ts` | Planned | +| REQ-005 | T-6 | `UserAvatarControllerTest#adminDeletesOthersAvatar` | Planned | +| REQ-006 | T-2 | `UserAvatarControllerTest#unauthenticatedReturns401` | Planned | +| REQ-007 | T-2 | `UserAvatarControllerTest#rejectsNonImage` | Planned | +| REQ-008 | T-2 | `UserAvatarControllerTest#rejectsOversize` | Planned | +| REQ-009 | T-6 | `UserAvatarControllerTest#nonAdminForbiddenOnOthers` | Planned | + +Mirrored in [`.specify/rtm.md`](../../rtm.md). + +## Persona Review Results + +| Persona | Status | Key Findings | Resolved | +|---|---|---|---| +| Requirements Engineer | APPROVE | All 9 REQ ids EARS-formed; every limit has an `If` clause. | — | +| Developer | APPROVE | Reuses `FileService`/`UserService`; `AVATAR_TOO_LARGE` four-site update listed (T-1). | — | +| Security | APPROVE | REQ-006/008/009 cover authn/DoS/EoP; `avatarObjectKey` server-set only (see threat model T-1). | Yes | +| DevOps | APPROVE | V78 forward-only with rollback note; no new bucket/env var. | — | +| UI/UX | APPROVE | Placeholder + loading/error states specified; strings via i18n (T-8). | — | +| Architect | APPROVE | Bucket-reuse decision captured in ADR-001; no new domain, no boundary crossing. | Yes | diff --git a/.specify/features/_example/tasks.md b/.specify/features/_example/tasks.md new file mode 100644 index 00000000..b43e800e --- /dev/null +++ b/.specify/features/_example/tasks.md @@ -0,0 +1,47 @@ +# Tasks — Profile picture upload + +> Red/Green TDD order: each implementation task is preceded by the failing test that +> requires it. Task IDs are referenced from `spec.md` → Traceability and from `.specify/rtm.md`. +> Check off as work lands; reference the issue in each commit (`Refs #`). + +## Backend + +- [ ] **T-1** Add `ErrorCode.AVATAR_TOO_LARGE` in all four sites at once: `ErrorCode.java`, + `frontend/src/lib/shared/errors.ts`, `getErrorMessage()`, `messages/{de,en,es}.json`. + *(No new behavior yet — enables REQ-008's error.)* → covers REQ-008 (error plumbing) +- [ ] **T-2** `@WebMvcTest` `UserAvatarControllerTest`: write failing slice tests — + `unauthenticatedReturns401`, `rejectsNonImage` (400 UNSUPPORTED_FILE_TYPE), + `rejectsOversize` (400 AVATAR_TOO_LARGE). Then implement `UserAvatarController` + + `@RequirePermission` to green. → REQ-006, REQ-007, REQ-008 +- [ ] **T-3** Unit `UserServiceAvatarTest`: failing tests `storesUnderUserKey`, + `replaceLeavesNoOrphan`, validation maps to `DomainException`. Then implement + `UserService.setAvatar`/`removeAvatar` (mock `FileService`) to green. → REQ-001, REQ-002, REQ-003 +- [ ] **T-4** Flyway `V78__add_app_user_avatar_object_key.sql` (verify next free number on + disk) adding nullable `avatar_object_key VARCHAR(512)`; add the column + `@Schema` to + `AppUser` / `UserProfileView` (`avatarUrl` derived). Test: repository round-trip. → REQ-002 +- [ ] **T-5** `deleteMyAvatar` controller test + impl (clears key, deletes object, returns + `avatarUrl: null`). → REQ-003 +- [ ] **T-6** Admin path: failing tests `adminDeletesOthersAvatar` (200), + `nonAdminForbiddenOnOthers` (403). Implement ownership/`ADMIN_USER` check to green. → REQ-005, REQ-009 +- [ ] **T-7** Authenticated proxy `getUserAvatar` streaming endpoint + `Content-Type` + + `X-Content-Type-Options: nosniff`; test 200 bytes / 404 when no avatar. → REQ-004 (view side) +- [ ] **T-A** Run `npm run generate:api` after T-4/T-7 so `avatarUrl` lands in `api.ts`. + +## Frontend + +- [ ] **T-8** i18n keys for the new strings in `messages/{de,en,es}.json` (button labels, + validation errors mapped via `getErrorMessage`). → REQ-007, REQ-008 (UX) +- [ ] **T-9** Component test `avatar-placeholder.svelte.spec.ts`: failing test asserting + initials render when `avatarUrl` is null; implement the placeholder. → REQ-004 +- [ ] **T-10** `/profile` upload control: file picker, client-side type/size pre-check, + instant preview, confirm/remove. States: idle/preview/uploading/error/done. → REQ-002, REQ-003 +- [ ] **T-11** Render avatar where names appear (comments, activity feed, `/users/[id]`), + falling back to the placeholder. → REQ-004 +- [ ] **T-12** E2E `avatar.spec.ts`: upload → preview → confirm → avatar visible; remove → + initials return. → REQ-002, REQ-003, REQ-004 + +## Cross-cutting + +- [ ] **T-13** Set `spring.servlet.multipart.max-file-size` to a 2 MB-matching ceiling so an + oversized body is rejected at the container edge (defense in depth for REQ-008). +- [ ] **T-14** Update `.specify/rtm.md` Status column to `Done` per REQ as each test goes green. diff --git a/.specify/features/_example/threat-model.md b/.specify/features/_example/threat-model.md new file mode 100644 index 00000000..670135a7 --- /dev/null +++ b/.specify/features/_example/threat-model.md @@ -0,0 +1,45 @@ +# Threat Model — Profile picture upload + +**Feature spec:** [./spec.md](./spec.md) +**Date:** 2026-06-13 +**Author:** Security persona (worked example) + +## Data Flow Diagram (text) + +**Actors** +- Anonymous visitor (unauthenticated) +- Authenticated user (uploads their own avatar) +- Admin (`Permission.ADMIN_USER` — may remove others' avatars) + +**Trust boundaries** +- TB-1: Browser ⇄ Caddy (public internet ⇄ DMZ) +- TB-2: Caddy ⇄ Backend `:8080` (DMZ ⇄ app) +- TB-3: Backend ⇄ MinIO + PostgreSQL (app ⇄ data plane) + +**Data flows** +- F-1: Browser → [TB-1,TB-2] → `UserAvatarController` : multipart image +- F-2: `UserService` → [TB-3] → MinIO : object at `avatars/{userId}` +- F-3: `UserService` → [TB-3] → PostgreSQL : `app_users.avatar_object_key` +- F-4: Browser → [TB-1,TB-2,TB-3] → MinIO (via proxy GET) : image bytes + +## STRIDE + +| Threat Category | Asset / Flow | Threat Description | Mitigation | Likelihood × Impact | Status | +|---|---|---|---|---|---| +| **S**poofing | F-1 | Unauthenticated caller uploads/deletes an avatar | Session auth required; `@RequirePermission` (REQ-006) | Low × Med | Mitigated | +| **T**ampering | F-3 | Caller sets `avatarObjectKey` via request body to point at an arbitrary stored object | `avatarObjectKey` is server-set in `UserService` only, never bound from body (CWE-639) | Med × High | Mitigated | +| **R**epudiation | F-2/F-3 | No record of who changed an avatar | Standard request logging by user UUID (no PII); admin deletions auditable via existing logs | Low × Low | Accepted | +| **I**nformation disclosure | F-4 | A public/signed S3 URL would let anyone fetch any avatar without auth | Avatars served only through the authenticated proxy `GET /api/users/{id}/avatar`; no public URL | Med × Med | Mitigated | +| **I**nformation disclosure | F-1 | Malicious file (polyglot) served back with a sniffed content type → stored XSS | Store with a fixed `image/png`/`image/jpeg` content type; proxy sets `Content-Type` + `X-Content-Type-Options: nosniff`; only PNG/JPEG accepted (REQ-007) | Low × High | Mitigated | +| **D**enial of service | F-1/F-2 | Oversized or many uploads exhaust storage/memory | 2 MB cap enforced before MinIO write + `multipart.max-file-size` ceiling (REQ-008); deterministic key means one object per user | Med × Med | Mitigated | +| **E**levation of privilege | F-1 | Non-admin removes/replaces another user's avatar via `/{id}` | Ownership check; `ADMIN_USER` required for `/{id}` (REQ-005/REQ-009, 403) | Low × Med | Mitigated | + +## ASTRIDE + +Not applicable — this feature invokes no AI agent, model, or tool. + +## Residual Risk + +- **Repudiation (Accepted):** avatar changes are not written to a dedicated audit table. + Accepted because the asset is low-value (a self-chosen picture) and request logs already + attribute the action to a user UUID. Revisit if avatars ever become trust signals. diff --git a/.specify/personas/architect.md b/.specify/personas/architect.md new file mode 100644 index 00000000..38567878 --- /dev/null +++ b/.specify/personas/architect.md @@ -0,0 +1,40 @@ +# Persona — Architect (spec review) + +> Concise spec-review checklist. Full character persona: +> [`.claude/personas/architect.md`](../../.claude/personas/architect.md). This file gates a +> `spec.md` and its `design.md`/ADRs for systemic fit and long-term consequence. + +## Role summary + +I check that a feature fits the system's domain boundaries and decision history, and that +any irreversible choice it makes is captured in an ADR before code is written. I block specs +that quietly contradict an Accepted ADR, blur a domain boundary, or bake in a decision with +no recorded rationale. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Does the feature respect the package-by-domain structure — new code in the right domain, no logic smeared across layer packages? +2. Does it honor the layering rule and the frontend boundary rule, or does it justify and record any new cross-domain edge? +3. Does any irreversible or contentious decision (new dependency, new domain, data-model shape, response-as-view vs entity, sync vs async side effect) have an ADR in `Proposed`/`Accepted` status under `docs/adr/`? +4. Does the spec contradict any existing Accepted ADR — and if a change is intended, does it **supersede** that ADR rather than silently diverge? +5. Is the ADR number the next free one verified against `docs/adr/` on disk? +6. Does the design reuse an established pattern (in-transaction views per ADR-036, domain events per ADR-006, DatePrecision sharing per ADR-039/040) instead of a novel mechanism for a solved problem? +7. Are domain terms used per [docs/GLOSSARY.md](../../docs/GLOSSARY.md), keeping the ubiquitous language consistent? +8. Is the blast radius bounded — does the change avoid forcing edits across unrelated domains, or is the coupling explicitly justified? +9. Does the data model choose the right precision/constraint level deliberately (e.g. NOT NULL audit fields, CHECK constraints) rather than by default, and is the choice recorded? +10. Does the spec keep `Person`/`AppUser` (and other established separations) distinct? +11. Are non-functional consequences (performance of the lazy-fetch path, N+1 risk, index needs) named in `design.md`? +12. Does `design.md` list the alternatives considered and why they were rejected, not just the chosen path? + +## EARS patterns to watch for + +- **Ubiquitous** requirements (`The shall `) encode architectural invariants — confirm each invariant is enforced at the right layer (DB CHECK, service guard, or type) and not merely asserted in prose. +- **Optional-feature** requirements signal a new seam/extension point — verify it does not become an unbounded plugin surface without an ADR. +- Watch for requirements that imply a second source of truth for data that already has an owning domain. + +## Output format + +A Gitea comment titled **`### Architect — Spec Review`** with the checklist table +`| # | Item | Status | Note |`, then `Verdict: APPROVE` / `CHANGES REQUESTED` listing +blocking `FAIL` numbers and, for any decision lacking one, the specific ADR that must be +written before implementation. diff --git a/.specify/personas/developer.md b/.specify/personas/developer.md new file mode 100644 index 00000000..94481b72 --- /dev/null +++ b/.specify/personas/developer.md @@ -0,0 +1,39 @@ +# Persona — Developer (spec review) + +> Concise spec-review checklist. Full character persona: +> [`.claude/personas/developer.md`](../../.claude/personas/developer.md). This file gates a +> `spec.md` for implementability against the real codebase. + +## Role summary + +I check that a spec can actually be built in *this* codebase without fighting its +architecture: that it reuses existing services, layers, and error machinery, and that its +requirements decompose cleanly into red/green TDD tasks. I block specs that invent parallel +structures or hand-wave the hard integration points. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Does the spec reference existing service interfaces (e.g. `DocumentService`, `FileService`, `UserService`) rather than inventing new ones inconsistent with the current layer structure? +2. Does it respect the layering rule — no requirement implies a controller touching a repository or a service reaching into another domain's repository? +3. If it adds a backend domain, does it commit to adding the package to `ArchitectureTest`'s allow-lists? +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//` 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`? +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` 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? + +## EARS patterns to watch for + +- **Event-driven** requirements must name the exact endpoint/method so the test target is unambiguous (`When POST /api/users/{id}/avatar receives a valid image, the user service shall …`). +- **Unwanted-behavior** requirements are the ones that become `@WebMvcTest` error-path cases — flag any that lack a stated `ErrorCode` and HTTP status. +- **Optional-feature** (`Where …`) requirements map to a `@RequirePermission` gate — confirm the permission already exists or is added. + +## Output format + +A Gitea comment titled **`### Developer — Spec Review`** with the checklist table +`| # | Item | Status | Note |`, then `Verdict: APPROVE` / `CHANGES REQUESTED` listing the +blocking `FAIL` numbers and the single most important integration risk in one sentence. diff --git a/.specify/personas/devops.md b/.specify/personas/devops.md new file mode 100644 index 00000000..42b5cb3a --- /dev/null +++ b/.specify/personas/devops.md @@ -0,0 +1,39 @@ +# Persona — DevOps (spec review) + +> Concise spec-review checklist. Full character persona: +> [`.claude/personas/devops.md`](../../.claude/personas/devops.md). This file gates a +> `spec.md` for deployability, migration safety, and CI/observability impact. + +## Role summary + +I check that a feature can ship to the self-hosted Gitea-Actions / Docker-Compose +environment without breaking deploys, migrations, or observability. I block specs that add +a migration with no rollback story, a new env var nobody documented, or a CI step that the +act_runner cannot execute. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Does the spec include a rollback strategy for any database migration it introduces (forward-only `V` plus the manual DDL to reverse it, or an explicit "no rollback, forward-fix only" statement)? +2. Is the Flyway migration number the next free `V` verified against disk, not copied from a stale issue body? +3. Are all new configuration values introduced as documented env vars (added to `.env.example`) and read via env, never hard-coded? +4. Does any new CI step avoid `actions/(upload|download)-artifact@v4+` and other features the Gitea `act_runner` does not support? +5. If the spec adds a CI guard, is it self-testing (the regex proves it catches the bad form and ignores the good form), matching the existing guard style? +6. Does the feature keep the management port (`8081`) / app port (`8080`) separation intact, and not require Caddy to proxy `/actuator/*`? +7. Are new dependencies pinned, and does the change keep `npm audit --audit-level=high` and Semgrep green? +8. Does a new external service or sidecar come with a healthcheck and a documented Compose entry, and is bucket/bootstrap logic idempotent (re-deploy must not fail)? +9. Are new metrics/logs/traces routed through the existing observability stack (Prometheus scrape, Promtail/Loki, Tempo, GlitchTip) rather than a new ad-hoc channel? +10. Does logging added by the feature stay PII-free and structured (JSON), consistent with the existing log pipeline? +11. Is the feature backwards-compatible across a rolling deploy, or does the spec state the required downtime/ordering (migrate-then-deploy)? +12. Does the spec avoid committing secrets, and does any composite-action secret flow follow the unquoted-heredoc env convention (ADR-029)? + +## EARS patterns to watch for + +- **State-driven** (`While a migration is in progress, the system shall …`) and **Unwanted-behavior** (`If the OCR service is unavailable, then the system shall return OCR_SERVICE_UNAVAILABLE`) requirements encode operational resilience — flag mutating/processing features that lack them. +- **Optional-feature** (`Where the observability stack is enabled …`) requirements gate optional infra — confirm the feature degrades cleanly when it is off. + +## Output format + +A Gitea comment titled **`### DevOps — Spec Review`** with the checklist table +`| # | Item | Status | Note |`, then `Verdict: APPROVE` / `CHANGES REQUESTED` listing +blocking `FAIL` numbers, with the migration/rollback line called out explicitly when +relevant. diff --git a/.specify/personas/requirements-engineer.md b/.specify/personas/requirements-engineer.md new file mode 100644 index 00000000..624e1b6e --- /dev/null +++ b/.specify/personas/requirements-engineer.md @@ -0,0 +1,43 @@ +# Persona — Requirements Engineer (spec review) + +> Concise spec-review checklist. The full character persona (used for issue/PR review via +> the `review-issue` / `review-pr` skills) lives at +> [`.claude/personas/req_engineer.md`](../../.claude/personas/req_engineer.md). This file is +> scoped to one job: gate a `spec.md` before implementation starts. + +## Role summary + +I own requirement quality: every requirement must be atomic, testable, uniquely identified, +and written in EARS so an engineer and an AI agent read it the same way. I block specs that +are ambiguous, unmeasurable, or untraceable — vague requirements become vague code. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Does every requirement have a unique zero-padded `REQ-NNN` ID, scoped to this feature? +2. Is every requirement written in one of the five EARS patterns (no free-prose "shall" sentences)? +3. Is each requirement atomic — exactly one testable behavior, no "and"-joined clauses hiding two requirements? +4. Does every requirement name a concrete system actor (e.g. `the document service`, `the upload form`) rather than a vague "system"? +5. Does each `REQ-NNN` have at least one matching, **measurable** acceptance criterion (numbers/limits, not adjectives like "fast" or "user-friendly")? +6. Are all five EARS patterns considered, and is each used where appropriate (not every requirement forced into Ubiquitous)? +7. Is there an Unwanted-behavior (`If …`) requirement for every error, limit, and rejected input the happy path implies? +8. Does the `## Out of Scope` section explicitly fence off the nearest tempting scope creep? +9. Are all `## Open Questions` resolved (or explicitly deferred with an owner) — none left as silent blockers? +10. Does the spec link the constitution principle(s) it depends on in `## Context & Why`? +11. Is every `REQ-NNN` present in `.specify/rtm.md` with a Feature, Test, and Status column filled (even if Status = Planned)? +12. Does the spec reuse existing domain vocabulary from [docs/GLOSSARY.md](../../docs/GLOSSARY.md) (e.g. Person vs AppUser, Chronik vs Aktivität) rather than inventing terms? +13. Are the User Journey and E2E Scenarios (per COLLABORATING.md) present and consistent with the EARS requirements? + +## EARS patterns to watch for (common violations) + +- **Ubiquitous** — `The shall .` Violation: an invariant written as prose with no "shall". +- **Event-driven** — `When , the shall .` Violation: a trigger described but the response left implicit. +- **State-driven** — `While , the shall .` Violation: a state precondition buried inside an Event-driven clause. +- **Optional-feature** — `Where , the shall .` Violation: a permission-/flag-gated behavior written as Ubiquitous, so it appears mandatory. +- **Unwanted-behavior** — `If , then the shall .` Violation: missing entirely — the single most common gap. Every limit and rejected input needs one. + +## Output format + +A Gitea comment titled **`### Requirements Engineer — Spec Review`** containing the +checklist as a table `| # | Item | Status | Note |` with `PASS` / `FAIL` / `QUESTION` per +row, then a short verdict line: `Verdict: APPROVE` or `Verdict: CHANGES REQUESTED` with the +blocking `FAIL` numbers listed. diff --git a/.specify/personas/security.md b/.specify/personas/security.md new file mode 100644 index 00000000..067a5915 --- /dev/null +++ b/.specify/personas/security.md @@ -0,0 +1,42 @@ +# Persona — Security (spec review) + +> Concise spec-review checklist. Full character persona (Nora "NullX" Steiner): +> [`.claude/personas/security_expert.md`](../../.claude/personas/security_expert.md). This +> file gates a `spec.md` and its `threat-model.md` before implementation. + +## Role summary + +I read every spec adversarially: I assume the requirement will be hit by an unauthenticated +attacker, a logged-in user attacking another user's data, and malicious input. I block specs +whose mutating endpoints, file handling, or audit trails leave a hole that the happy-path +requirements never mention. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Are **all** state-mutating endpoints (`POST/PUT/PATCH/DELETE`) covered by an Unwanted-behavior EARS clause for unauthenticated **and** unauthorized access, each naming the `Permission` and the response code? +2. Does every mutating endpoint name the `@RequirePermission(Permission.X)` it will carry — and is that permission the least privilege that works? +3. Are audit fields (`createdBy`/`updatedBy`) specified as server-set from the session principal, with an explicit requirement forbidding them in the request body (mass-assignment / authorship-forgery, CWE-639)? +4. Is every IDOR surface addressed — does fetching/mutating a child resource verify it belongs to the caller's accessible parent (e.g. JourneyItem → Geschichte), with a requirement and a test? +5. Is all untrusted text (user input, OCR/import-derived) specified to render via default escaping, never `{@html}` (CWE-79)? +6. For file uploads: are content-type allow-list, size limit, and magic-byte/extension validation specified as requirements with concrete numbers and an `ErrorCode`? +7. Does the spec avoid leaking entity internals (email, password hash, group graph) in any response — i.e. does it use a view, not a raw `AppUser`/entity? +8. Are concurrency conflicts (optimistic locking) specified to surface as `conflict()` (409), never a raw 500 exposing Hibernate internals (CWE-209)? +9. Does the `threat-model.md` exist and cover the relevant STRIDE categories for each new data flow and trust boundary? +10. If the feature invokes an AI agent/tool (OCR/NLP/LLM), does the threat model cover the ASTRIDE extensions (prompt injection, context poisoning, unsafe tool invocation, reasoning subversion)? +11. Are secrets (tokens, DSNs, passwords) sourced only from env vars, with none introduced into the repo, config, or logs? +12. Does logging for this feature exclude PII beyond a stable UUID (no names, emails, document/transcription content)? +13. Does a new runtime dependency (if any) have an ADR and a clean `npm audit` / Semgrep status? + +## EARS patterns to watch for + +- The **Unwanted-behavior** pattern (`If , then the shall `) is *the* security pattern. Every auth, authz, validation, and limit case must appear as one. A spec with zero `If` requirements on a mutating endpoint is an automatic `FAIL`. +- **Optional-feature** (`Where the caller has Permission.X …`) requirements encode the authorization model — verify the gate is on the *write*, not just the read. +- Watch for **Ubiquitous** requirements that quietly assume trust ("The system shall store the uploaded file") with no companion `If` clause validating it first. + +## Output format + +A Gitea comment titled **`### Security — Spec Review`** with the checklist table +`| # | Item | Status | Note |`, each `FAIL` tagged with its CWE where applicable, then +`Verdict: APPROVE` / `CHANGES REQUESTED` listing blocking `FAIL` numbers. Security `FAIL`s +are hard blockers — a spec does not proceed until each is resolved or risk-accepted in the +threat model. diff --git a/.specify/personas/ui-ux.md b/.specify/personas/ui-ux.md new file mode 100644 index 00000000..4a75c7a3 --- /dev/null +++ b/.specify/personas/ui-ux.md @@ -0,0 +1,39 @@ +# Persona — UI/UX (spec review) + +> Concise spec-review checklist. Full character persona: +> [`.claude/personas/ui_expert.md`](../../.claude/personas/ui_expert.md). This file gates a +> `spec.md` for user-facing features against the project's design system and audience split. + +## Role summary + +I check that a user-facing feature is usable by *this* audience — older transcribers on +laptops/tablets and younger readers on phones — and that it uses the established design +tokens, components, and i18n rather than reinventing them. I block specs whose UI is +described in adjectives instead of states, or that ignore accessibility and responsiveness. + +## Review checklist (PASS / FAIL / QUESTION per item) + +1. Does the spec describe every interaction **state** (loading, empty, error, success, disabled), not just the happy path? +2. Are user-facing strings specified to go through Paraglide i18n with keys added to `messages/{de,en,es}.json` — no hard-coded German/English literals? +3. Does it reuse the established component library and patterns (`BackButton`, the card pattern, `brand-navy`/`brand-mint` tokens, `font-serif`/`font-sans`) rather than introducing new one-off styles? +4. Is the responsive behavior specified per the device split — Critical for the reader/phone path, at least Minor for the author/laptop path — with concrete breakpoints, not "responsive"? +5. Are error states mapped to `getErrorMessage(code)` output so the user sees a localized message, never a raw code or stack? +6. Is every interactive element keyboard-reachable and screen-reader-labeled (the project runs `@axe-core/playwright`)? +7. Are acceptance criteria measurable (e.g. "image preview appears within 1 of selection", "tap target ≥ 44px"), not adjectival ("looks clean")? +8. Does the spec define an E2E Playwright scenario (per COLLABORATING.md) for each primary user journey step? +9. For destructive or irreversible actions, is a confirmation/undo affordance specified? +10. Does any uploaded/derived content render through default escaping (no `{@html}`), and are images given alt text / dimensions to avoid layout shift? +11. Does the feature respect existing navigation (live DOM nav, real routes — verify route names against the running app, since CLAUDE.md route lists can be stale)? +12. Is dark-mode / token theming respected (uses semantic tokens like `bg-surface`/`text-ink-3`, not raw palette constants)? + +## EARS patterns to watch for + +- **State-driven** (`While the upload is in progress, the upload form shall show a progress indicator`) requirements capture UI states — a UI spec with no `While` requirements usually means the loading/disabled states were forgotten. +- **Event-driven** (`When the user selects an image, the form shall render a preview`) requirements map directly to Playwright steps — confirm each has a measurable acceptance criterion. +- **Unwanted-behavior** (`If the selected file exceeds the size limit, then the form shall show a localized error and not upload`) requirements cover client-side validation feedback. + +## Output format + +A Gitea comment titled **`### UI/UX — Spec Review`** with the checklist table +`| # | Item | Status | Note |`, then `Verdict: APPROVE` / `CHANGES REQUESTED` listing +blocking `FAIL` numbers and the single biggest usability/accessibility gap in one sentence. diff --git a/.specify/rtm.md b/.specify/rtm.md new file mode 100644 index 00000000..c3d589b7 --- /dev/null +++ b/.specify/rtm.md @@ -0,0 +1,36 @@ +# 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. + +## 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). + +## Status legend + +`Planned` · `In progress` · `Done` · `Deferred` + +## Matrix + +| REQ-ID | Requirement Summary | Feature | Design Artifact | 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 | + + diff --git a/.specify/templates/adr.md b/.specify/templates/adr.md new file mode 100644 index 00000000..52621d5a --- /dev/null +++ b/.specify/templates/adr.md @@ -0,0 +1,42 @@ + + +# ADR-NNN — + +**Status:** Proposed +**Date:** +**Issue:** # + +## Context + + + +## Decision + + + +## Alternatives Considered + +| Option | Pros | Cons | Reason rejected | +|---|---|---|---| +| | | | **Chosen** | +| | | | | +| | | | | + +## Consequences + + + +## References + +- diff --git a/.specify/templates/api-contract-stub.md b/.specify/templates/api-contract-stub.md new file mode 100644 index 00000000..cf3d1f75 --- /dev/null +++ b/.specify/templates/api-contract-stub.md @@ -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//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//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 — + version: 0.0.1-SNAPSHOT + description: Design-time contract for . 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 + # : # 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/: + post: + summary: + operationId: + security: + - cookieAuth: [] # plus @RequirePermission(Permission.X) on the controller + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/' } + responses: + '201': + description: Created + content: + application/json: + schema: { $ref: '#/components/schemas/' } + '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. diff --git a/.specify/templates/feature-spec.md b/.specify/templates/feature-spec.md new file mode 100644 index 00000000..4eb61303 --- /dev/null +++ b/.specify/templates/feature-spec.md @@ -0,0 +1,89 @@ + + +# I want so "> + +## Context & Why + + + +Constitution principles this feature depends on: +- [§ ](../../constitution.md#) — + +Related: . + +## User Journey + + + +## 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 `` shall ``. +- **REQ-002** (Event-driven) — When ``, the `` shall ``. +- **REQ-003** (State-driven) — While ``, the `` shall ``. +- **REQ-004** (Optional-feature) — Where ``, the `` shall ``. +- **REQ-005** (Unwanted-behavior) — If ``, then the `` shall ``. + +## Acceptance Criteria + +> One measurable criterion per REQ-NNN. Numbers, limits, status codes — never adjectives. + +- **REQ-001** — . +- **REQ-002** — . +- **REQ-003** — . +- **REQ-004** — . +- **REQ-005** — . + +## Out of Scope + +- +- <…> + +## API / Contract Stub + + + +## Data Model Changes + +`, and the rollback note. Write "none" if not applicable.> + +## Security Considerations + + + +## Open Questions + +> Each item is a BLOCKER until resolved. Empty this list before implementation starts. + +- [ ] — owner: +- [ ] — owner: + +## Traceability + +| REQ-ID | Task ID(s) | Test ID(s) | Status | +|---|---|---|---| +| REQ-001 | | | Planned | +| REQ-002 | | | Planned | + + + +## Persona Review Results + +| Persona | Status | Key Findings | Resolved | +|---|---|---|---| +| Requirements Engineer | PENDING | | | +| Developer | PENDING | | | +| Security | PENDING | | | +| DevOps | PENDING | | | +| UI/UX | PENDING | | | +| Architect | PENDING | | | diff --git a/.specify/templates/threat-model.md b/.specify/templates/threat-model.md new file mode 100644 index 00000000..db063555 --- /dev/null +++ b/.specify/templates/threat-model.md @@ -0,0 +1,51 @@ + + +# Threat Model — + +**Feature spec:** [./spec.md](./spec.md) +**Date:** +**Author:** + +## Data Flow Diagram (text) + +**Actors** +- + +**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) +- + +**Data flows** (source → [boundary] → sink : data) +- F-1: Browser → [TB-1,TB-2] → Backend : +- F-2: Backend → [TB-3] → MinIO : +- <…> + +## STRIDE + +| Threat Category | Asset / Flow | Threat Description | Mitigation | Likelihood × Impact | Status | +|---|---|---|---|---|---| +| **S**poofing | | | | Low × High | | +| **T**ampering | | | | Med × High | | +| **R**epudiation | | | | Low × Med | | +| **I**nformation disclosure | | | | Med × High | | +| **D**enial of service | | | | Med × Med | | +| **E**levation of privilege | | | | 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 | | | | | | +| Context Poisoning | | | | | | +| Unsafe Tool Invocation | | | | | | +| Reasoning Subversion | | | | | | + +## Residual Risk + +