timeline_layer_* and timeline_derived_* shipped German values in the
English and Spanish catalogs, so EN/ES screen-reader users heard German
for the world/family layer and birth/death/marriage cues. Translate them;
de.json stays canonical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The undated bucket is assembled from all entries, so it can contain
events as well as letters. Rendering every undated entry with LetterCard
produced a dead /documents/undefined link and "Unknown -> Unknown" for
events. Dispatch on kind/type like YearBand does (WorldBand/EventPill/
LetterCard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the per-entry {#each} key logic into a shared entryKey.ts so the
undated bucket in TimelineView can reuse it. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nav-link smoke + timeline-in-<main> (empty-or-populated), and the 320px
no-overflow guarantee on a timeline seeded with 25+char correspondent names
(REQ-005). Runs against the real stack via the seeded admin session.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SSR-first load fetches GET /api/timeline via createApiClient (auth cookie
forwarded), no query params for the global view (REQ-001), returns { timeline }
with no client-side fetch (REQ-002); 401 -> /login, any other non-ok ->
error(status, getErrorMessage(...)), never raw JSON, no PII logged (REQ-022).
The page renders <TimelineView> under the layout's <main>. Adds the Zeitstrahl
nav link (desktop + mobile) and 'timeline' to the eslint routes boundary
allow-list so the route may import the domain.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
break-words on sender/receiver/title so a 25+char correspondent name cannot
force horizontal overflow on a 320px phone (REQ-005).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renders year bands in DTO order with interior empty-year runs folded into one
GapSpan (REQ-015), a single <ol> in chronological DOM order (REQ-006), the undated
bucket via {#if} (REQ-016), and a calm empty state (REQ-017). personId is a
declared seam (issue #10), undefined here, never passed to leaf cards (REQ-025).
Centered desktop spine / left phone spine via scoped CSS. Owns no <main>.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One <section> per year with a sticky <h2> at top:4rem (REQ-006). Events render in
DTO order as pills/bands; letters render as individual cards while <= 12 (REQ-011)
or collapse to one density strip above that (REQ-012); DTO order is never re-sorted
(REQ-003). Letters carry an alternating data-side for the centered desktop axis
(REQ-004); single left column on phone (REQ-005). Derived-safe {#each} key.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Letter count + 12-month density sparkline + a >=44px keyboard-focusable expand
toggle that reveals that year's LetterCards (REQ-012). Sparkline values from the
shared monthHistogram.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A thin dashed span rendering '{from}–{to} · keine Einträge', collapsing to a
single year when the run has length 1 (REQ-015).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-width muted band; RANGE renders a span pill (1914–1918) with a Zeitraum
aria-label (REQ-009); a RANGE with no end degrades to the start year, no pill,
no crash (REQ-010). World glyph is a redundant non-color cue with sr-only label
(REQ-018); text uses text-ink-2 to hold AA in both themes (REQ-019).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Centered axis pill: derived life-events (* Geburt / † Tod / ⚭ Heirat) and curated
PERSONAL events (★, mint border) via getAccentConfig. Glyph wrapped aria-hidden +
sr-only label (REQ-018). Edit affordance only for a curated event with eventId,
never derived/null (REQ-008). REQ-007.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single archive letter: sender → receiver (Unbekannt fallback for empty names,
REQ-014), title, precision date chip via timelineDateLabel (omitted when null,
REQ-013), linking to exactly /documents/{documentId} with no target (REQ-023).
44px touch target enforced inline + focus-visible ring (REQ-020). OCR/import
text via {...} escaping + whitespace-pre-line, no {@html} (REQ-021).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getAccentConfig(entry) maps each EVENT to its glyph (* / † / ⚭ / ★ / ◍), German
redundant-cue label, and accent kind (REQ-007/008/018). test-factories build
TimelineEntryDTO/TimelineDTO mirroring the real wire shape for component specs.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A minimal presentational bar series (one bar per value, heights scaled to the
max, faint floor for empty buckets). Lives in shared so both the timeline
density strip and the document chart can use it. REQ-012 (supports).
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 Paraglide keys for the /zeitstrahl view: nav link, heading, empty/undated/
gap/unknown-person chrome, letters count, strip expand, range aria, and the
layer/derived labels. The layer (Weltgeschehen/Familie) and derived (Geburt/
Tod/Heirat) labels carry the German term across all locales by design
(documented MVP decision). REQ-024.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isDense(count) thresholds dense year bands at >12 letters (REQ-012);
monthHistogram(letters, year) buckets a band's letters into exactly 12 month
buckets via the shared fillDensityGaps, counting each letter on its eventDate
anchor month and ignoring undated entries (REQ-027). Imports shared only.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relocate the 10 pure helpers (monthBoundaryFrom/To, buildMonthSequence,
fillDensityGaps, clipBucketsToRange, aggregateToYears, selectionBoundaryFrom/To,
tickIndicesFor, formatTickLabel) and their unit tests out of document/timeline.ts
into a shared module so lib/timeline/ can consume them without importing
lib/document/. The /api/documents/density glue (buildDensityUrl, fetchDensity,
DensityState, DensityFilters) stays in document/timeline.ts. Re-point the three
density components and the density-filter spec at the shared module.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous entry referenced fields (id: String, primaryPersonName,
relatedPersonName) from an earlier design that was superseded during
spec review. Replace with the actual 13-field record shape implemented
in PR #826.
Fixes: @markus stale CLAUDE.md entry on PR #826
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without the annotation, Hibernate closes its sub-transaction after
eventRepository.findAll() returns, leaving TimelineEvent entities
detached. Accessing ev.getPersons() or doc.getReceivers() on those
detached entities throws LazyInitializationException in production
(constitution §1.6). @DataJpaTest and @Transactional test classes
masked the bug by keeping an outer session alive.
Fixes: @felix / @markus review blockers on PR #826
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 20 REQ-NNN rows for issue #777 (all Done) to .specify/rtm.md
- Update CLAUDE.md timeline package description with TimelineService/TimelineController
- Extend l3-backend-timeline.puml with TimelineService/TimelineController components
and their edges to PersonService and DocumentService
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies PersonRepository.findByGeneration handles match, no-match (empty
list not NPE), and null-generation persons (excluded). Also confirms
TimelineService.assemble() returns a persisted curated event in the
correct year band against real Postgres via Testcontainers.
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TimelineController exposes GET /api/timeline with @RequirePermission(READ_ALL)
and @Validated so @Min(0) on generation fires a 400. Delegates to
TimelineService.assemble(TimelineFilter). DomainException 404/400 propagate
via GlobalExceptionHandler (no extra mapping needed).
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonRepository.findByGeneration(Integer) — boxed to match nullable entity field.
DocumentRepository.findAllForTimeline() — Document.list entity-graph, single query.
Both services delegate with one-liner methods.
Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GLOSSARY.md entries for derived event, DerivedEventType, derivedType,
and assembleDerivedEvents() to cover the vocabulary introduced by #776.
Update l3-backend-timeline.puml: remove stale "planned, #775" labels,
add Rel from TimelineEventService to personDomain for assembleDerivedEvents
batch-fetch calls, document the on-read strategy in the component notes.
Refs #776
Co-Authored-By: claude-sonnet-4-6 <noreply@anthropic.com>
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>
Returns all SPOUSE_OF edges with JOIN FETCH on both person sides,
preventing N+1 in TimelineService.assembleDerivedEvents() (REQ-011).
Reuses existing findAllByRelationTypeIn query which already JOIN FETCHes.
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DerivedEventType: BIRTH / DEATH / MARRIAGE discriminator for derived events.
TimelineEntryDTO: unified String-id DTO for both curated and derived events;
id is String (not UUID) to accommodate synthetic prefixed ids (birth:/death:/marriage:).
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers three pre-implementation decisions for issue #776:
1. On-read assembly, never persisted (no migration)
2. Synthetic prefixed String ids (birth:/death:/marriage:)
3. assembleDerivedEvents() as the public cross-issue contract on TimelineService
Refs #776
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two ADR-041 files landed on main in parallel (sdd-adoption and
renovate-runner-setup). Renames the SDD one to 042 and repoints its references
(SPEC_DRIVEN_DEVELOPMENT, constitution, .specify/adrs/README, sdd-gate.yml).
The renovate ADR keeps 041; its references are left untouched. Riding this PR
per request.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flip REQ-001..006 for the timeline date-label feature from Planned to Done and
fill the implementing files plus the concrete dateLabel.spec.ts test names that
prove each requirement.
Closes#778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Note above the DatePrecision type that it mirrors the Java DatePrecision enum,
must be updated manually in lockstep with that enum, and must not be migrated
to the OpenAPI-generated type — it drives the shared client-side formatter
shared by documents and the timeline date-label facade.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timelineDateLabel delegates to the shared formatDocumentDate so a timeline
chip renders identically to the same date on a document, in the active
locale (REQ-001/REQ-002). UNKNOWN precision and null/undefined/'' eventDate
short-circuit to null with no formatter call (REQ-003/REQ-004); raw is always
null since timeline events carry no verbatim spreadsheet cell. The facade
owns no precision logic of its own (REQ-005).
Register the new `timeline` frontend domain in the eslint boundaries config
(allowed to import only `shared`) and add src/lib/timeline/** to the vitest
coverage include (REQ-006). The spec partially mocks the paraglide runtime
via importOriginal so getLocale is stubbed while the formatter still resolves
real season/range message exports.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Red phase for the timeline date-label helper. Asserts delegation to the
shared formatDocumentDate (localized DAY de/en, SEASON de, same-year RANGE)
and the null cases for UNKNOWN/empty eventDate. The runtime mock path keeps
the `.js` suffix so it matches the import under test.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
REQ-001..006 -> #778, Status: Planned. Spec (EARS) lives in the issue body;
these rows are the committed traceability anchor. Flipped to Done as tests land.
Refs #778
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pins @stoplight/spectral-cli@6.16.0 and caches ~/.npm keyed on that version, so
Spectral is fetched once and reused across runs instead of re-downloaded each
time. A version bump busts the cache key deterministically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates the full SDD workflow into one view at the top of the guide: a
Mermaid flowchart (skills, the three gates, the TDD loop), a one-time
prerequisites checklist, and a gates table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Spectral v6 ships no implicit ruleset — the CI job exited 'no ruleset found'.
Adds .spectral.yaml (extends spectral:oas, documentation-only warnings relaxed
for design-time stubs), adds operation tags to the _example contract so it lints
clean (0 results), and aligns the api-contract-stub note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Front of the SDD funnel: the requirements engineer interviews the user, elicits
EARS REQ-NNN requirements + measurable acceptance criteria (probing hard for the
Unwanted-behavior clauses specs usually miss), then creates the Gitea feature
issue (issue body = spec), labels it, and emits RTM rows. Authors only — hands
off to /review-issue rather than self-approving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Gitea issue body is the single source of truth for a spec; the only
per-feature artifact in git is the RTM row (REQ-ID -> issue # -> test). Drops
per-feature spec.md/tasks.md/checklist files from the workflow (the _example
stays as a template/reference). Updates the guide, ADR-041, AGENTS.md, CLAUDE.md,
templates, the RTM (adds an Issue column), the implement/review-pr skills, and
replaces the file-spec CI jobs with an rtm-check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
review-issue becomes the SDD spec-review gate (adds the requirements engineer,
pairs each .claude persona identity with its .specify checklist, EARS/REQ-NNN
aware). review-pr verifies the diff against the constitution and the spec's
REQ-NNN traceability. implement reads the spec artifacts, plans from tasks.md,
ties tasks to REQ-NNN, and keeps the RTM and generated API types in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds SPEC_DRIVEN_DEVELOPMENT.md (8-step workflow, before/after issue, persona
review example, agent-prompt example, maintenance rules, cheatsheet) and points
CLAUDE.md, COLLABORATING.md, and CONTRIBUTING.md at the new .specify/ workflow
without altering the existing cycle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds .gitea/ISSUE_TEMPLATE/{feature,bug}.md (the feature template mirrors the
EARS feature-spec) and .gitea/workflows/sdd-gate.yml — spec-lint, contract
validation (Spectral), traceability check (all non-blocking during adoption),
and a constitution-impact PR comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces the SDD root: a v1.0.0 constitution and machine-readable AGENTS.md
grounded in the project's real conventions; six EARS-aware persona spec-review
checklists that cross-reference .claude/personas/; feature-spec/ADR/threat-model/
api-contract templates; a fully worked _example feature; a living RTM; and an
adrs/ pointer that reuses the existing docs/adr/ archive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>