RTM: add REQ-001–REQ-016 rows with Done status, implementation files, and test IDs. CLAUDE.md: expand timeline package entry with TimelineEntryDTO, DerivedEventType, and assembleDerivedEvents(); add TimelineEntryDTO to domain model table. Refs #776 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.4 KiB
Requirements Traceability Matrix (RTM)
Living document. One row per
REQ-NNNacross all in-flight and shipped features. The spec itself lives in the Gitea issue (issue-only — there is no committedspec.md); this matrix is the part of the spec that is committed: it links each requirement to its issue, the code that implements it, and the test(s) that prove it — so any requirement traces end to end, and any orphan (a requirement with no test) is visible onmain.
How to update
- When a feature's issue is approved (via
/review-issue), add one row perREQ-NNNwith theIssueset to the Gitea issue number andStatus: Planned. Commit these rows on the feature branch (they merge with the feature's PR). - As tasks land, fill
Implementation File(s)+Test(s)and flipStatus→In progress→Done. REQ-IDs are scoped per feature, so always read them together with theIssuecolumn —REQ-001for issue #142 is notREQ-001for issue #150.- The
sdd-gate.ymlCI job (rtm-check) warns (non-blocking, for now) when a row is missing itsIssueorTest(s). It flips to blocking once adoption settles (see the workflow's TODO).
Status legend
Planned · In progress · Done · Deferred
Matrix
| REQ-ID | Requirement Summary | Issue | Feature | Implementation File(s) | Test(s) | Status |
|---|---|---|---|---|---|---|
| REQ-001 | Store avatar at avatars/{userId}, overwrite |
#example | profile-picture-upload (_example) | UserService (planned) |
UserServiceAvatarTest#storesUnderUserKey, #replaceLeavesNoOrphan |
Planned |
| REQ-002 | Upload self avatar → 200 + avatarUrl | #example | profile-picture-upload (_example) | UserAvatarController, UserService (planned) |
UserAvatarControllerTest#uploadReturnsAvatarUrl |
Planned |
| REQ-003 | Delete self avatar → avatarUrl null | #example | profile-picture-upload (_example) | UserAvatarController (planned) |
UserAvatarControllerTest#deleteClearsAvatar |
Planned |
| REQ-004 | No avatar → null + initials placeholder | #example | profile-picture-upload (_example) | UserProfileView, avatar component (planned) |
avatar-placeholder.svelte.spec.ts |
Planned |
| REQ-005 | ADMIN_USER may delete others' avatar | #example | profile-picture-upload (_example) | UserAvatarController (planned) |
UserAvatarControllerTest#adminDeletesOthersAvatar |
Planned |
| REQ-006 | Unauthenticated → 401, store nothing | #example | profile-picture-upload (_example) | SecurityConfig, controller (planned) |
UserAvatarControllerTest#unauthenticatedReturns401 |
Planned |
| REQ-007 | Non-image → 400 UNSUPPORTED_FILE_TYPE | #example | profile-picture-upload (_example) | UserService (planned) |
UserAvatarControllerTest#rejectsNonImage |
Planned |
| REQ-008 | Over 2 MB → 400 AVATAR_TOO_LARGE | #example | profile-picture-upload (_example) | UserService, ErrorCode (planned) |
UserAvatarControllerTest#rejectsOversize |
Planned |
| REQ-009 | Non-admin on others → 403 FORBIDDEN | #example | profile-picture-upload (_example) | UserAvatarController (planned) |
UserAvatarControllerTest#nonAdminForbiddenOnOthers |
Planned |
| REQ-001 | Render dated entry via shared formatDocumentDate (de/en/es) |
#778 | timeline-date-label | frontend/src/lib/timeline/dateLabel.ts |
dateLabel.spec.ts › renders a DAY date localized in German, renders a SEASON date with the German season word, delegates a same-year RANGE to formatDocumentDate |
Done |
| REQ-002 | Non-UNKNOWN + non-empty date → shared label, raw=null, getLocale() |
#778 | timeline-date-label | frontend/src/lib/timeline/dateLabel.ts |
dateLabel.spec.ts › renders a DAY date localized in German, renders a DAY date localized in English |
Done |
| REQ-003 | UNKNOWN → null (no chip) |
#778 | timeline-date-label | frontend/src/lib/timeline/dateLabel.ts |
dateLabel.spec.ts › returns null for UNKNOWN precision even with a date, returns null for UNKNOWN precision without a date |
Done |
| REQ-004 | null/undefined/'' eventDate → null, no formatter call |
#778 | timeline-date-label | frontend/src/lib/timeline/dateLabel.ts |
dateLabel.spec.ts › returns null for APPROX with a null eventDate, without calling the formatter, returns null for DAY with an empty-string eventDate, treats undefined eventDateEnd identically to null for RANGE |
Done |
| REQ-005 | No rendering logic outside documentDate.ts |
#778 | timeline-date-label | frontend/src/lib/timeline/dateLabel.ts (façade) |
dateLabel.spec.ts › delegates a same-year RANGE to formatDocumentDate (asserts byte-identical delegation) |
Done |
| REQ-006 | timeline in coverage include (80% branch gate) |
#778 | timeline-date-label | frontend/vite.config.ts |
coverage include now lists src/lib/timeline/**; covered by all dateLabel.spec.ts cases |
Done |
| REQ-001 | family-member Person with birthDate → 1 BIRTH event, precision passed through | #776 | derive-person-life-events | timeline/TimelineEventService#buildBirthEvents | DerivedEventsAssemblyTest#should_emit_one_geburt_for_person_with_birthdate, #should_pass_birth_precision_through_unchanged | Done |
| REQ-002 | family-member Person with deathDate → 1 DEATH event | #776 | derive-person-life-events | timeline/TimelineEventService#buildDeathEvents | DerivedEventsAssemblyTest#should_emit_one_tod_for_person_with_deathdate, #should_emit_one_tod_and_zero_geburt_for_person_with_deathdate_only | Done |
| REQ-003 | null birthDate → 0 Geburt events | #776 | derive-person-life-events | timeline/TimelineEventService#buildBirthEvents | DerivedEventsAssemblyTest#should_emit_zero_tod_when_person_has_birthdate_but_no_deathdate, #should_emit_one_tod_and_zero_geburt_for_person_with_deathdate_only | Done |
| REQ-004 | null deathDate → 0 Tod events; no dates → 0 events | #776 | derive-person-life-events | timeline/TimelineEventService#buildDeathEvents | DerivedEventsAssemblyTest#should_emit_no_events_for_person_with_neither_date | Done |
| REQ-005 | SPOUSE_OF edge with fromYear → MARRIAGE event, eventDate={fromYear}-01-01, precision=YEAR | #776 | derive-person-life-events | timeline/TimelineEventService#buildMarriageEvents | DerivedEventsAssemblyTest#should_emit_one_heirat_for_spouse_edge_with_fromYear | Done |
| REQ-006 | SPOUSE_OF edge with null fromYear → MARRIAGE emitted, eventDate=null, precision=UNKNOWN | #776 | derive-person-life-events | timeline/TimelineEventService#buildMarriageEvents | DerivedEventsAssemblyTest#should_emit_unknown_precision_heirat_when_fromYear_is_null | Done |
| REQ-007 | exactly one MARRIAGE per SPOUSE_OF row (dedup on relationship id) | #776 | derive-person-life-events | timeline/TimelineEventService#buildMarriageEvents | DerivedEventsAssemblyTest#should_emit_exactly_one_heirat_when_both_spouses_in_scope, #should_emit_two_heirat_for_person_married_to_two_partners | Done |
| REQ-008 | synthetic prefixed ids (birth:/death:/marriage:), never parseable as UUID | #776 | derive-person-life-events | timeline/TimelineEventService#buildBirthEvents,#buildDeathEvents,#buildMarriageEvents | DerivedEventsAssemblyTest#should_mint_prefixed_synthetic_ids_never_uuid | Done |
| REQ-009 | every derived event: derived=true, type=PERSONAL, non-null derivedType, non-UUID id | #776 | derive-person-life-events | timeline/TimelineEventService | structural invariants asserted inline in every event test | Done |
| REQ-010 | every derived event: non-null non-blank primaryPersonName; Heirat also non-null non-blank relatedPersonName | #776 | derive-person-life-events | timeline/TimelineEventService#buildBirthEvents,#buildDeathEvents,#buildMarriageEvents | DerivedEventsAssemblyTest#should_emit_heirat_with_displayname_for_both_spouses | Done |
| REQ-011 | exactly one call to findAllFamilyMembers() and one to findAllSpouseEdges() — no N+1 | #776 | derive-person-life-events | timeline/TimelineEventService#assembleDerivedEvents, relationship/RelationshipService#findAllSpouseEdges | test structure: only batch-fetch mocks used (no per-person stubs) | Done |
| REQ-012 | familyMember=false persons excluded from Geburt/Tod assembly | #776 | derive-person-life-events | timeline/TimelineEventService#assembleDerivedEvents (via PersonService.findAllFamilyMembers) | DerivedEventsAssemblyTest#should_exclude_non_family_member_persons_from_derived_events | Done |
| REQ-013 | SPOUSE_OF edge with one non-family-member spouse still emits 1 MARRIAGE event | #776 | derive-person-life-events | timeline/TimelineEventService#buildMarriageEvents | DerivedEventsAssemblyTest#should_emit_heirat_when_one_spouse_is_not_family_member | Done |
| REQ-014 | empty family-member list → empty result, no error | #776 | derive-person-life-events | timeline/TimelineEventService#assembleDerivedEvents | DerivedEventsAssemblyTest#should_emit_zero_events_when_no_family_members | Done |
| REQ-015 | assembleDerivedEvents() annotated @Transactional(readOnly=true) | #776 | derive-person-life-events | timeline/TimelineEventService#assembleDerivedEvents | code-review check (annotation visible in source) | Done |
| REQ-016 | no PII (names, dates) in log statements — only aggregate counts | #776 | derive-person-life-events | timeline/TimelineEventService#assembleDerivedEvents | log-audit: single log.debug with result.size() and persons.size() only | Done |