docs(legibility): write CONTRIBUTING.md with three concrete walkthroughs #398
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Part of Epic #394 — Documentation. This is DOC-4: the operational manual for adding things to the codebase. Successor-X (hypothetical future maintainer) opens this when they get their first ticket.
Per the Legibility Rubric, this addresses C5.1, C5.3, C5.4 (all Major).
Required content
A single
CONTRIBUTING.mdat repo root. Not indocs/— convention is to keep CONTRIBUTING at the root for tool discoverability.1. Environment setup
2. Daily development workflow
npm run generate:apiwith backend in dev profile)<type>/<issue-number>-<short-description>Closes #N, atomic commits)3. Walkthrough A — Add a new domain
Concrete step-by-step:
backend/src/main/java/.../<domain>/frontend/src/lib/<domain>/README.mdon both stacks (per DOC-6)docs/ARCHITECTURE.mdto mention the new domaindocs/GLOSSARY.mdif any new terms are introduced@RequirePermissionon every state-mutating controller method4. Walkthrough B — Add a new endpoint to an existing domain
Concrete step-by-step using a real example (e.g., "add
POST /api/persons/{id}/aliases"):<domain>/.../<Domain>Controller.java<domain>/.../<Domain>Service.java@RequirePermissionif mutating@Schema(requiredMode = REQUIRED)DomainException.notFound(ErrorCode.X, ...)for errors; addErrorCodevalue if new--spring.profiles.active=dev, thennpm run generate:apiinfrontend/5. Walkthrough C — Add a new frontend page
Concrete step-by-step using a real example (e.g., "add
/persons/[id]/timeline"):frontend/src/routes/persons/[id]/timeline/+page.svelte+page.server.tsfor SSR data load (use the typed API client from$lib/api.server.ts)lib/<domain>/folderlib/shared/<BackButton>, Germandd.mm.yyyydate input, brand color utilities (brand-navy,brand-mint,brand-sand)messages/{de,en,es}.jsonErrorCodeenum infrontend/src/lib/errors.ts6. Conventions reference
Brief tables (links out to longer docs where appropriate):
dto/; response = entity)!result.response.ok,result.error as { code },result.data!)Anti-patterns
CLAUDE.mdfiles into here — only the rules a human contributor needs. Keep CONTRIBUTING tight.docs/ARCHITECTURE.mdinstead.Acceptance criteria
CONTRIBUTING.mdexists at repo root with the 6 sections aboveREADME.md(DOC-1)backend/CLAUDE.mdandfrontend/CLAUDE.md(per DOC-7)Dependency
Soft dependency on Epic 4 (Refactor) being complete OR planned — walkthroughs A/B reference the target domain-based structure, not the current layered one.
Definition of Done
CONTRIBUTING.mdcommitted onmain. Closing comment summarizes the three walkthrough examples.🏗️ Markus Keller — Senior Application Architect
Observations
org.raddatz.familienarchiv.document.DocumentControlleretc.), but the actual codebase today is still layered (controller/,service/,repository/). I checked —backend/src/main/java/org/raddatz/familienarchiv/hascontroller/,service/,repository/as its top-level packages.docs/ARCHITECTURE.mdanddocs/GLOSSARY.mdas link targets in Walkthrough A. Neither file exists. What does exist:docs/architecture/c4-diagrams.md(C4 diagrams) and 6 ADRs indocs/adr/. The walkthrough needs to reference what's actually there.docs/ARCHITECTURE.md" and step 6 says "Updatedocs/GLOSSARY.md". Both are dead links on day one.Recommendations
docs/architecture/c4-diagrams.mdfor architecture, and either createdocs/GLOSSARY.mdas a stub or link to the domain model table already inCLAUDE.md.COLLABORATING.mdandCODESTYLE.mdat repo root are solid. CONTRIBUTING.md should link to them for the conventions reference section (Section 6) rather than duplicating their content.Open Decisions
👨💻 Felix Brandt — Senior Fullstack Developer
Observations
POST /api/persons/{id}/aliases,/persons/[id]/timeline).lib/<domain>/" — but the actual structure islib/components/as a flat-ish collection with some subdirectories (document/,chronik/,user/). The issue's guidance would send a contributor tolib/geschichten/when the actual pattern appears to belib/components/. Verified this by inspectingfrontend/src/lib/— no per-domainlib/subfolders exist beyondcomponents/.ErrorCodeenum infrontend/src/lib/errors.ts" — that's correct and exactly right. Good to call this out explicitly.lib/shared/for shared primitives — this directory doesn't exist in the current codebase. The actual shared components live directly inlib/components/(e.g.,BackButton.svelte,DateInput.svelte,Pagination.svelte).cd frontend && npm run lint. The issue mentions Husky correctly — that's good.--spring.profiles.active=dev, thennpm run generate:api. This is the real workflow.Recommendations
lib/<domain>/claim in Walkthrough C to match reality: domain-specific components go inlib/components/or a subdirectory of it (e.g.,lib/components/document/). Drop the reference tolib/shared/.COLLABORATING.md) is that tests come first. The walkthrough shouldn't teach the wrong order even implicitly. Reorder: write the failing test first, then add the implementation.README.mdon both stacks (per DOC-6)" reference in Walkthrough A step 4 should clarify that this is a planned DOC-6 pattern — it doesn't currently exist, and a newcomer looking for examples will find none.Open Decisions (none — all of the above are concrete fixes)
🔒 Nora "NullX" Steiner — Application Security Engineer
Observations
@RequirePermissionif mutating" — the word if is a security smell. In this codebase, the rule is: every state-mutating endpoint must have@RequirePermission. "If" suggests it's optional, which it isn't. A contributor who reads "if mutating" may skip it and accidentally publish an unauthenticated write endpoint.@Schema(requiredMode = REQUIRED)) is type-codegen guidance, not security — but it's in the right place contextually.ResponseStatusExceptionfor auth errors is a known source of broken error code propagation to the frontend.CLAUDE.mdhas it (validate and sanitize at the controller boundary, trust internal service code) but it's absent from the walkthrough's conventions table. A new contributor adding an endpoint has no signal to validate at the boundary.CLAUDE.mdandSecurityConfig) includes:@RequirePermission, input validation, parameterized queries, no logging of raw user input. None of these appear explicitly as a "security checklist" step in Walkthrough B.Recommendations
@RequirePermissionif mutating" to "Add@RequirePermission(Permission.WRITE_ALL)on everyPOST,PUT,PATCH, andDELETEendpoint — this is not optional." Then clarify read-only endpoints stay unannotated.ResponseStatusException(BAD_REQUEST, ...)orDomainException.badRequest(...). Never trust data that hasn't been validated at the entry point."@RequirePermissionrequired on writes; parameterized queries only (@Param); no raw user input in logs; content-type validation on uploads.Open Decisions (none — these are corrections to existing project rules, not new choices)
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
COLLABORATING.mdmandates Red/Green TDD: test first. The walkthroughs as written teach implementation-first, test-last — which is the opposite of the project rule.COLLABORATING.mdhas a clear test-type matrix: business logic → unit, HTTP contract → controller slice, full behavior → E2E. CONTRIBUTING.md should either reproduce that table or link to it.loadfunction gets a Vitest unit test; a user-facing navigation flow gets an E2E Playwright spec; a shared UI component gets a Vitest browser-mode component test.docs/ARCHITECTURE.mdin walkthroughs — that file doesn't exist, which means step 5 of Walkthrough A is a broken link from day one. This is a correctness defect in the spec.Recommendations
loadfunctions → Vitest unit test (import directly, mockfetch); UI interaction flows → Playwright E2E; shared components → Vitest browser-mode withrender()+getByRole().COLLABORATING.mdto the Section 2 walkthrough preamble — a single table that maps "type of change" to "test type" so contributors can navigate the decision without hunting through multiple files.Open Decisions (none — test ordering is settled by the project's own
COLLABORATING.md)🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
min-h-[44px]touch targets andfocus-visible:ring-2focus rings as established patterns — but neither appears in the CONTRIBUTING.md walkthrough. A contributor creating a new page has no signal to implement these.<BackButton>, German date input, and brand color utilities. These are good concrete anchors. The gap is that the accessibility equivalents of these patterns are missing (touch targets, focus rings,aria-labelon icon buttons,aria-liveon toasts).Recommendations
min-h-[44px]); focus rings on all focusable elements (focus-visible:ring-2 focus-visible:ring-brand-navy);aria-labelon all icon-only buttons."focus-visible;aria-live="polite"on dynamic status messages; color never used as the sole status indicator.docs/STYLEGUIDE.md(which exists atdocs/STYLEGUIDE.md).Open Decisions (none — these are corrections reflecting existing project standards, not new requirements)
⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
source "$HOME/.sdkman/bin/sdkman-init.sh"andexport NVM_DIR+sourceare non-obvious if you've never used them.docker-compose up -dcommand in the root correctly starts PostgreSQL, MinIO, and the backend. But contributors also need to know thatnpm run devfor the frontend runs on port 3000 and needs the backend already running. The sequencing should be explicit: (1) docker-compose up, (2) backend via IDE or./mvnw spring-boot:run, (3)npm run dev.cd frontend && npm run lint— CONTRIBUTING.md should warn contributors that their first commit will trigger this and they neednode_modulesinstalled (npm install) infrontend/before committing.frontend/package.json'spreparescript (git -C .. config core.hooksPath .husky). Contributors whonpm installin/frontendwill have hooks configured automatically — but only if they installed from within the frontend directory. This is non-obvious and should be spelled out.docker-compose.ci.ymloverlay for CI — the issue doesn't mention it. New contributors shouldn't need to know about it for local dev, but noting its existence ("don't use the CI compose file locally — it removes bind mounts") prevents a common support question.Recommendations
source "$HOME/.sdkman/bin/sdkman-init.sh"and the nvm equivalent is worth 10 words of prose.npm run devbeforedocker-compose upand wonder why the API calls fail.cd frontend && npm installas a prerequisite step. The Husky hook setup runs as part ofnpm install'spreparescript — contributors who skip this step will get a hook failure on first commit with no clear error.docker-compose.ci.yml: "Do not use the CI compose overlay locally — it is for the CI pipeline and disables bind mounts that the dev workflow depends on."Open Decisions (none)
📋 Elicit — Requirements Engineer
Observations
backend/CLAUDE.mdandfrontend/CLAUDE.md(per DOC-7)". The use of "migrates" is ambiguous — does it mean copy? Summarize? Replace? Link? The difference matters for maintainability. If the rules are copied, you now have three places to update whenever a convention changes (CONTRIBUTING.md, backend/CLAUDE.md, frontend/CLAUDE.md). If they are linked, CONTRIBUTING.md stays thin and the canonical rule lives in one place.README.md(DOC-1)" impliesREADME.mdexists and has a place for this link.README.mddoes not currently exist at the repo root (verified by inspection). This is a blocking dependency that isn't flagged as such in the issue.Recommendations
backend/CLAUDE.mdandfrontend/CLAUDE.md— do not duplicate content that already has a canonical home." This prevents a three-way sync problem.README.mdas an explicit prerequisite: The acceptance criterion "Linked fromREADME.md(DOC-1)" is blocked if DOC-1 (create README.md) hasn't shipped yet. Either re-order the epic or add a note: "This AC requires DOC-1 to be completed first."Open Decisions
backend/CLAUDE.md/frontend/CLAUDE.mdinto CONTRIBUTING.md (single entry point, three-way sync risk) vs. summarize and link to the canonical sources (thin CONTRIBUTING, reader follows links). The right answer depends on whether the CLAUDE.md files are considered stable canonical sources or LLM-specific overrides that human contributors shouldn't need to read.🗳️ Decision Queue — Action Required
2 decisions need your input before implementation starts.
Architecture / Documentation
controller/,service/,repository/). If Epic 4 slips, CONTRIBUTING.md is factually wrong on day one. Option A: Write against current structure, add a callout box about the planned refactor, update CONTRIBUTING.md when Epic 4 lands (one small PR). Option B: Write against target structure as-is, accept that the walkthroughs will be inconsistent with reality until Epic 4 ships. Cost of A: a small follow-up edit. Cost of B: actively misleading new contributors until the refactor is done. (Raised by: Markus, Felix, Elicit)Requirements
backend/CLAUDE.mdandfrontend/CLAUDE.md." If "migrate" means copy, you create three places to maintain the same rules. If it means link, CONTRIBUTING.md stays thin but requires contributors to follow links. Option A (link): Summarize in 1–2 bullets per area, link to the canonical file for details. Single source of truth. Option B (copy): Full rules in CONTRIBUTING.md for a single-document experience; accept that they may drift from the CLAUDE.md files over time. The CLAUDE.md files are currently flagged as LLM-specific — if that's their primary audience, copying the human-relevant subset into CONTRIBUTING.md is the right call. (Raised by: Elicit)✅ Decision Queue — Resolved
The 2 decisions raised in #398#issuecomment-6290:
1. Current vs target package structure → write against current; add a callout for the planned refactor (Option A)
Same shape as DOC-2's resolution. Markus, Felix, and Elicit all converge: Anja opens the backend on day one and sees
controller/,service/,repository/,model/. A walkthrough that says "createorg.raddatz.familienarchiv.tag.TagController.java" while the repo hascontroller/TagController.javais actively misleading.Implementation pattern:
main.controller/,service/,model/,repository/). Epic 4 (Refactor) will restructure it into per-domain packages — when that lands, this walkthrough is updated in the same PR."2. "Migrate" vs "link" for the conventions section → migrate (Option B), but only because DOC-7 makes CONTRIBUTING.md the canonical home
Per epic-level Decision Queue D3 (the pointer-comment policy resolved on #394), CLAUDE.md sections become 1-line pointers TO CONTRIBUTING.md (and other human docs). Once that's done:
So Option B (copy into CONTRIBUTING.md) is correct — it's not duplication; it's the migration target. The follow-up is DOC-7 (#401) replacing the now-stale CLAUDE.md sections with pointers.
Sequencing: DOC-4 ships with the conventions content fully written. DOC-7 then strips the migrated sections out of CLAUDE.md.
📌 Additional persona feedback to fold into implementation
docs/ARCHITECTURE.md" → use the actual path that will exist after DOC-2 (#396) lands; if not yet, link todocs/architecture/c4-diagrams.mdprovisionally. "docs/GLOSSARY.md" same.lib/components/(flat-ish) and recent refactors are moving things to per-domainlib/<domain>/(issue #408). Verify againstmainat implementation time and use the structure that's actually there.COLLABORATING.mdand memoriesfeedback_tdd/feedback_tdd_flow) is non-negotiable. "Write a failing test" comes first, "make it pass" comes second. Walkthrough A may put TDD later (no behaviour to test until controller exists), but it must be present.load→ Vitest unit (mockfetch); UI flow → Playwright E2E; shared component → Vitest browser-moderender()+getByRole(); backend service logic →@ExtendWith(MockitoExtension.class); HTTP contract →@WebMvcTestslice. Replace the vague "unit test or e2e test" wording.@RequirePermissionif mutating" to "Add@RequirePermission(Permission.WRITE_ALL)on everyPOST,PUT,PATCH,DELETE— this is not optional. Read-only endpoints stay unannotated."ResponseStatusException(BAD_REQUEST, …)orDomainException.badRequest(…). Trust internal service code; never trust unvalidated boundary input."@RequirePermissionrequired on writes; parameterised queries only (@Param); no raw user input in logs (use{}placeholders); content-type validation on uploads.min-h-[44px])focus-visible:ring-2 focus-visible:ring-brand-navy)aria-labelon icon-only buttonsaria-live="polite"on dynamic status messagesCap at ≤3 bullets; link to
docs/STYLEGUIDE.mdfor full WCAG checklist.docker compose up -d, (2) backend (./mvnw spring-boot:runor IDE), (3)cd frontend && npm install && npm run dev. Note thatnpm installtriggers Husky hook setup via thepreparescript — required before the first commit.docker-compose.ci.yml— "Do not use the CI compose overlay locally; it disables bind mounts that the dev workflow depends on."mainbefore merge."Status: Ready for implementation. Read
frontend/src/lib/andbackend/src/main/java/...against the latestmainbefore writing exact paths — they shift as #408 lands.DOC-4 implemented — PR #442
Walkthrough examples:
citationdomain — 12 steps from creating the backend package through Flyway migration, TDD-first tests, frontendlib/citation/, docs update, and ESLint boundary rulePOST /api/persons/{id}/aliases— TDD Red step first, then@RequirePermission(Permission.WRITE_ALL)(mandatory, not optional), input validation at boundary,DomainExceptionfor errors, and the ⚠️ type-regen warning/persons/[id]/timeline— TDD Red first (Playwright E2E), SSR load with typed API client, domain component inlib/person/, shared primitives inlib/shared/primitives/, accessibility baseline, Paraglide i18nAll file paths verified against current
main. All walkthroughs in TDD order.PR: http://heim-nas:3005/marcel/familienarchiv/pulls/442