Timeline: shared precision-aware date-label helper (#778) #824

Merged
marcel merged 6 commits from feat/778-timeline-date-label into main 2026-06-13 14:04:56 +02:00
Owner

Summary

Adds a thin timeline façade frontend/src/lib/timeline/dateLabel.ts over the existing shared formatter formatDocumentDate ($lib/shared/utils/documentDate.ts). A timeline chip now renders a date identically to the same date on a document, localized de/en/es — with zero precision logic duplicated outside documentDate.ts.

timelineDateLabel(eventDate, precision, eventDateEnd?):

  • delegates to formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale()) for any dated, non-UNKNOWN event;
  • short-circuits to null (no formatter call) for UNKNOWN precision or null/undefined/'' eventDate;
  • always passes raw = null — timeline events have no verbatim spreadsheet cell, so season words derive only from the structured anchor month.

REQ → test mapping

REQ Requirement Proven by (dateLabel.spec.ts)
REQ-001 Render dated entry via shared formatter (de/en/es) renders a DAY date localized in German, renders a SEASON date with the German season word, delegates a same-year RANGE to formatDocumentDate
REQ-002 Non-UNKNOWN + non-empty → shared label, raw=null, getLocale() renders a DAY date localized in German, renders a DAY date localized in English
REQ-003 UNKNOWNnull returns null for UNKNOWN precision even with a date, returns null for UNKNOWN precision without a date
REQ-004 null/undefined/'' eventDate → null, no formatter call 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
REQ-005 No rendering logic outside documentDate.ts delegates a same-year RANGE to formatDocumentDate (asserts byte-identical delegation)
REQ-006 timeline in coverage include src/lib/timeline/** added to vitest coverage include; exercised by all spec cases

Other edits (per SDD spec review)

  • Registered the new timeline frontend domain in frontend/eslint.config.js boundaries (allowed to import only shared).
  • Added a drift-risk note above the DatePrecision type: it is a hand-maintained mirror of the Java enum and must NOT be migrated to the OpenAPI-generated type.
  • Added src/lib/timeline/** to the coverage include in frontend/vite.config.ts.

Verification

  • npx vitest run src/lib/timeline/dateLabel.spec.ts → 9 passed (server project, Node).
  • npm run lint (prettier + eslint, incl. boundaries) → green.

Closes #778

🤖 Generated with Claude Code

## Summary Adds a thin timeline façade `frontend/src/lib/timeline/dateLabel.ts` over the existing shared formatter `formatDocumentDate` (`$lib/shared/utils/documentDate.ts`). A timeline chip now renders a date identically to the same date on a document, localized de/en/es — with zero precision logic duplicated outside `documentDate.ts`. `timelineDateLabel(eventDate, precision, eventDateEnd?)`: - delegates to `formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale())` for any dated, non-UNKNOWN event; - short-circuits to `null` (no formatter call) for `UNKNOWN` precision or `null`/`undefined`/`''` eventDate; - always passes `raw = null` — timeline events have no verbatim spreadsheet cell, so season words derive only from the structured anchor month. ## REQ → test mapping | REQ | Requirement | Proven by (`dateLabel.spec.ts`) | |---|---|---| | REQ-001 | Render dated entry via shared formatter (de/en/es) | `renders a DAY date localized in German`, `renders a SEASON date with the German season word`, `delegates a same-year RANGE to formatDocumentDate` | | REQ-002 | Non-UNKNOWN + non-empty → shared label, `raw=null`, `getLocale()` | `renders a DAY date localized in German`, `renders a DAY date localized in English` | | REQ-003 | `UNKNOWN` → `null` | `returns null for UNKNOWN precision even with a date`, `returns null for UNKNOWN precision without a date` | | REQ-004 | `null`/`undefined`/`''` eventDate → `null`, no formatter call | `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` | | REQ-005 | No rendering logic outside `documentDate.ts` | `delegates a same-year RANGE to formatDocumentDate` (asserts byte-identical delegation) | | REQ-006 | `timeline` in coverage `include` | `src/lib/timeline/**` added to vitest coverage `include`; exercised by all spec cases | ## Other edits (per SDD spec review) - Registered the new `timeline` frontend domain in `frontend/eslint.config.js` boundaries (allowed to import only `shared`). - Added a drift-risk note above the `DatePrecision` type: it is a hand-maintained mirror of the Java enum and must NOT be migrated to the OpenAPI-generated type. - Added `src/lib/timeline/**` to the coverage `include` in `frontend/vite.config.ts`. ## Verification - `npx vitest run src/lib/timeline/dateLabel.spec.ts` → 9 passed (server project, Node). - `npm run lint` (prettier + eslint, incl. boundaries) → green. Closes #778 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

Requirements Engineer — PR Review

Verdict: Approved

Traceability of every REQ-001…006 from issue #778 against the diff:

REQ Implemented? Real test? RTM row Done? Note
REQ-001 (delegate via shared formatter, de/en/es) dateLabel.ts returns formatDocumentDate(...) DAY-de, SEASON-de, RANGE delegation locale proven across two languages
REQ-002 (non-UNKNOWN + non-empty → shared label, raw=null, getLocale()) exact signature formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale()) DAY-de / DAY-en matches the EARS response verbatim
REQ-003 (UNKNOWNnull, no chip) precision === 'UNKNOWN' guard UNKNOWN-with-date, UNKNOWN-without-date both branches covered
REQ-004 (null/undefined/''null, no formatter call) !eventDate guard short-circuits null-APPROX, empty-string-DAY, undefined-eventDateEnd falsy guard catches all three
REQ-005 (no rendering logic outside documentDate.ts) façade owns only 2 decisions RANGE asserts byte-identical toBe(...) delegation strongest possible delegation proof
REQ-006 (timeline in coverage include, 80% branch) src/lib/timeline/** in vite.config.ts n/a (config) inside the gate

All six REQ are implemented and tested. The RTM rows were added on the feature branch with Status: Done and correct issue (#778), feature, impl-file, and test-name columns — fully in sync with the diff.

Scope-creep check: no behavior present without a backing REQ. The eslint.config.js registration and the documentDate.ts drift-risk comment are both explicit tasks in the issue body (the SDD-review "Additional task" and Decision 8). No orphan code.

Additional task from the issue (timeline domain → eslint boundary, same commit): satisfied — see Architect comment.

Verdict: Approved — full end-to-end traceability, RTM accurate.

### Requirements Engineer — PR Review **Verdict: ✅ Approved** Traceability of every REQ-001…006 from issue #778 against the diff: | REQ | Implemented? | Real test? | RTM row `Done`? | Note | |---|---|---|---|---| | REQ-001 (delegate via shared formatter, de/en/es) | ✅ `dateLabel.ts` returns `formatDocumentDate(...)` | ✅ DAY-de, SEASON-de, RANGE delegation | ✅ | locale proven across two languages | | REQ-002 (non-UNKNOWN + non-empty → shared label, `raw=null`, `getLocale()`) | ✅ exact signature `formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale())` | ✅ DAY-de / DAY-en | ✅ | matches the EARS response verbatim | | REQ-003 (`UNKNOWN` → `null`, no chip) | ✅ `precision === 'UNKNOWN'` guard | ✅ UNKNOWN-with-date, UNKNOWN-without-date | ✅ | both branches covered | | REQ-004 (`null`/`undefined`/`''` → `null`, no formatter call) | ✅ `!eventDate` guard short-circuits | ✅ null-APPROX, empty-string-DAY, undefined-eventDateEnd | ✅ | falsy guard catches all three | | REQ-005 (no rendering logic outside `documentDate.ts`) | ✅ façade owns only 2 decisions | ✅ RANGE asserts byte-identical `toBe(...)` delegation | ✅ | strongest possible delegation proof | | REQ-006 (`timeline` in coverage `include`, 80% branch) | ✅ `src/lib/timeline/**` in `vite.config.ts` | n/a (config) | ✅ | inside the gate | All six REQ are implemented **and** tested. The RTM rows were added on the feature branch with `Status: Done` and correct issue (#778), feature, impl-file, and test-name columns — fully in sync with the diff. **Scope-creep check:** no behavior present without a backing REQ. The `eslint.config.js` registration and the `documentDate.ts` drift-risk comment are both explicit tasks in the issue body (the SDD-review "Additional task" and Decision 8). No orphan code. **Additional task from the issue (timeline domain → eslint boundary, same commit):** satisfied — see Architect comment. Verdict: ✅ Approved — full end-to-end traceability, RTM accurate.
Author
Owner

🛠️ Developer (Felix Brandt) — PR Review

Verdict: Approved

# Item Status Note
1 Reuses existing service/util, no parallel structure PASS Pure façade over formatDocumentDate; zero precision logic re-typed (KISS-over-DRY, constitution §3.2)
2 Layering / boundary respected PASS timeline → shared only; registered in eslint.config.js
3 TDD red/green evidence PASS Spec is delegation/integration suite; npx vitest run → 9 passed locally (confirmed)
4 Guard clauses, no nesting PASS Single `if (precision === 'UNKNOWN'
5 Naming reveals intent PASS timelineDateLabel, eventDate, eventDateEnd, precision — no d/obj
6 Type-only import for DatePrecision PASS import { formatDocumentDate, type DatePrecision } per issue import note
7 .js suffix on runtime import PASS '$lib/paraglide/runtime.js' — matches the mock path in the spec (mismatch would silently skip the mock)
8 generate:api needed? PASS Correctly dropped (Decision 2) — no timeline DTO in the OpenAPI spec yet; codegen would be a no-op
9 New ErrorCode? PASS None added — no four-site update needed
10 Dead code / commented-out PASS None

Comments-explaining-what vs why: the JSDoc and the inline raw=null comment explain why (security contract, REQ references), not what — consistent with the persona's comment discipline. The façade is genuinely a one-liner that benefits from the contract documentation.

Minor (non-blocking): the function is a thin wrapper whose comments exceed its body; this is intentional and appropriate here (the comments encode cross-language drift and security decisions that the code cannot self-document). No change requested.

Verdict: Approved — clean, minimal, idiomatic.

### 🛠️ Developer (Felix Brandt) — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | Reuses existing service/util, no parallel structure | PASS | Pure façade over `formatDocumentDate`; zero precision logic re-typed (KISS-over-DRY, constitution §3.2) | | 2 | Layering / boundary respected | PASS | `timeline → shared` only; registered in `eslint.config.js` | | 3 | TDD red/green evidence | PASS | Spec is delegation/integration suite; `npx vitest run` → 9 passed locally (confirmed) | | 4 | Guard clauses, no nesting | PASS | Single `if (precision === 'UNKNOWN' || !eventDate) return null;` — happy path is the last line | | 5 | Naming reveals intent | PASS | `timelineDateLabel`, `eventDate`, `eventDateEnd`, `precision` — no `d`/`obj` | | 6 | Type-only import for `DatePrecision` | PASS | `import { formatDocumentDate, type DatePrecision }` per issue import note | | 7 | `.js` suffix on runtime import | PASS | `'$lib/paraglide/runtime.js'` — matches the mock path in the spec (mismatch would silently skip the mock) | | 8 | `generate:api` needed? | PASS | Correctly **dropped** (Decision 2) — no timeline DTO in the OpenAPI spec yet; codegen would be a no-op | | 9 | New `ErrorCode`? | PASS | None added — no four-site update needed | | 10 | Dead code / commented-out | PASS | None | **Comments-explaining-what vs why:** the JSDoc and the inline `raw=null` comment explain *why* (security contract, REQ references), not *what* — consistent with the persona's comment discipline. The façade is genuinely a one-liner that benefits from the contract documentation. **Minor (non-blocking):** the function is a thin wrapper whose comments exceed its body; this is intentional and appropriate here (the comments encode cross-language drift and security decisions that the code cannot self-document). No change requested. Verdict: ✅ Approved — clean, minimal, idiomatic.
Author
Owner

🧪 Tester (Sara Holt) — PR Review

Verdict: Approved

# Item Status Note
1 Each REQ has a real, behaviour-named test PASS 9 tests, full-sentence names; mapped 1:1 to REQ-001…004
2 Test level is right PASS *.spec.ts (not *.svelte.spec.ts) → Node server project, as the issue specifies; no browser needed for a pure function
3 Red-then-green meaningful PASS Tests exercise real branches; requireAssertions: true is on in this config, so no vacuous test
4 Delegation asserted, not duplicated PASS RANGE test computes expected via formatDocumentDate(...) directly and toBe(...) — not a brittle literal; if the shared formatter changes, this test tracks it instead of going stale
5 Edge cases / boundaries PASS UNKNOWN-with-date, UNKNOWN-without-date, null, '', undefined end-date all covered — both guard sub-conditions
6 Mock at the right boundary PASS Partial mock of runtime.js via importOriginal, overriding only getLocale; real m.date_* exports preserved so the shared formatter runs its real logic
7 Locale switch is meaningful PASS de→Juli/en→July proves getLocale() actually threads through, not a no-op mock
8 Branch-coverage gate PASS Module added to coverage include; both guard branches + the delegation path are hit → comfortably within 80%
9 Determinism / no flakiness PASS Pure function, fixed dates, beforeEach resets locale to de; ran locally → 9 passed in 1.17s

Coverage note: I confirmed the run locally (single file, per the no-full-suite rule). Every executable branch in dateLabel.ts is exercised. No missing error/empty-state coverage — there are no async/error states in a pure formatter.

Verdict: Approved — exemplary thin delegation suite; tests track the shared contract rather than freezing a literal.

### 🧪 Tester (Sara Holt) — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | Each REQ has a real, behaviour-named test | PASS | 9 tests, full-sentence names; mapped 1:1 to REQ-001…004 | | 2 | Test level is right | PASS | `*.spec.ts` (not `*.svelte.spec.ts`) → Node `server` project, as the issue specifies; no browser needed for a pure function | | 3 | Red-then-green meaningful | PASS | Tests exercise real branches; `requireAssertions: true` is on in this config, so no vacuous test | | 4 | Delegation asserted, not duplicated | PASS | RANGE test computes expected via `formatDocumentDate(...)` directly and `toBe(...)` — not a brittle literal; if the shared formatter changes, this test tracks it instead of going stale | | 5 | Edge cases / boundaries | PASS | UNKNOWN-with-date, UNKNOWN-without-date, `null`, `''`, `undefined` end-date all covered — both guard sub-conditions | | 6 | Mock at the right boundary | PASS | Partial mock of `runtime.js` via `importOriginal`, overriding only `getLocale`; real `m.date_*` exports preserved so the shared formatter runs its real logic | | 7 | Locale switch is meaningful | PASS | de→`Juli`/en→`July` proves `getLocale()` actually threads through, not a no-op mock | | 8 | Branch-coverage gate | PASS | Module added to coverage `include`; both guard branches + the delegation path are hit → comfortably within 80% | | 9 | Determinism / no flakiness | PASS | Pure function, fixed dates, `beforeEach` resets locale to `de`; ran locally → 9 passed in 1.17s | **Coverage note:** I confirmed the run locally (single file, per the no-full-suite rule). Every executable branch in `dateLabel.ts` is exercised. No missing error/empty-state coverage — there are no async/error states in a pure formatter. Verdict: ✅ Approved — exemplary thin delegation suite; tests track the shared contract rather than freezing a literal.
Author
Owner

🔐 Security (Nora "NullX") — PR Review

Verdict: Approved

# Item Status Note
1 Mutating endpoints / authz PASS (N/A) No endpoint, no mutation — a pure client-side formatter. No @RequirePermission surface.
2 Audit fields / mass-assignment PASS (N/A) No persistence, no createdBy/updatedBy.
3 IDOR surface PASS (N/A) No resource fetch.
4 raw=null passed explicitly (CWE-79 / untrusted-text) PASS This is the load-bearing one. The 4th arg is hardcoded null. The shared formatter only ever uses raw to derive a SEASON word via a known-German-token allow-list (seasonFromRaw) — and even that path is bypassed here, so the season word comes solely from the structured anchor month. No untrusted free text reaches the label. Matches Decision 6.
5 {@html} / escaping PASS Returns a plain string/null; no HTML, no interpolation. Card-level {@html} concern is correctly deferred to #779 (noted in issue).
6 Secrets / PII in logs PASS No logging, no secrets, no PII.
7 New dependency PASS None added.

Adversarial read: the only data flowing in is eventDate/eventDateEnd (structured ISO from a trusted DTO) and precision (enum). Even if a future caller passed an attacker-influenced eventDate, the value reaches new Intl.DateTimeFormat(...).format(noon(iso)) and iso.slice(...) only — no eval, no HTML sink, no path/URL/query use. The raw=null decision proactively closes the one channel (verbatim cell) that could carry untrusted text into a season word. Correct least-exposure choice.

Verdict: Approved — raw=null is exactly the right defensive default; no new attack surface.

### 🔐 Security (Nora "NullX") — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | Mutating endpoints / authz | PASS (N/A) | No endpoint, no mutation — a pure client-side formatter. No `@RequirePermission` surface. | | 2 | Audit fields / mass-assignment | PASS (N/A) | No persistence, no `createdBy`/`updatedBy`. | | 3 | IDOR surface | PASS (N/A) | No resource fetch. | | 4 | `raw=null` passed explicitly (CWE-79 / untrusted-text) | PASS | This is the load-bearing one. The 4th arg is hardcoded `null`. The shared formatter only ever uses `raw` to derive a SEASON word via a known-German-token allow-list (`seasonFromRaw`) — and even that path is bypassed here, so the season word comes solely from the structured anchor month. No untrusted free text reaches the label. Matches Decision 6. | | 5 | `{@html}` / escaping | PASS | Returns a plain `string`/`null`; no HTML, no interpolation. Card-level `{@html}` concern is correctly deferred to #779 (noted in issue). | | 6 | Secrets / PII in logs | PASS | No logging, no secrets, no PII. | | 7 | New dependency | PASS | None added. | **Adversarial read:** the only data flowing in is `eventDate`/`eventDateEnd` (structured ISO from a trusted DTO) and `precision` (enum). Even if a future caller passed an attacker-influenced `eventDate`, the value reaches `new Intl.DateTimeFormat(...).format(noon(iso))` and `iso.slice(...)` only — no `eval`, no HTML sink, no path/URL/query use. The `raw=null` decision proactively closes the one channel (verbatim cell) that could carry untrusted text into a season word. Correct least-exposure choice. Verdict: ✅ Approved — `raw=null` is exactly the right defensive default; no new attack surface.
Author
Owner

⚙️ DevOps (Tobias) — PR Review

Verdict: Approved

# Item Status Note
1 DB migration / rollback PASS (N/A) No Flyway migration in this PR
2 New env vars documented PASS (N/A) None introduced
3 CI step compatibility (act_runner) PASS No workflow changes; upload/download-artifact pin untouched
4 CI guard steps intact PASS No banned-pattern grep or self-tested regex removed/weakened
5 Coverage gate config PASS vite.config.ts adds src/lib/timeline/** to includetightens the gate (more code measured), never loosens it; thresholds (80/80/80/80) unchanged
6 Lint passes PASS eslint.config.js boundary entry well-formed; PR states npm run lint green
7 New dependency pinned / audit clean PASS (N/A) No dependency added
8 Secrets PASS None
9 PII-free logging PASS No logging added

Deploy risk: none. Frontend-only, pure-function addition; no migration ordering, no rolling-deploy concern, no new service/healthcheck. The coverage-include change is the only CI-visible effect and it strictly increases measured scope.

Verdict: Approved — zero operational footprint; coverage gate correctly tightened.

### ⚙️ DevOps (Tobias) — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | DB migration / rollback | PASS (N/A) | No Flyway migration in this PR | | 2 | New env vars documented | PASS (N/A) | None introduced | | 3 | CI step compatibility (act_runner) | PASS | No workflow changes; `upload/download-artifact` pin untouched | | 4 | CI guard steps intact | PASS | No banned-pattern grep or self-tested regex removed/weakened | | 5 | Coverage gate config | PASS | `vite.config.ts` adds `src/lib/timeline/**` to `include` — *tightens* the gate (more code measured), never loosens it; thresholds (80/80/80/80) unchanged | | 6 | Lint passes | PASS | `eslint.config.js` boundary entry well-formed; PR states `npm run lint` green | | 7 | New dependency pinned / audit clean | PASS (N/A) | No dependency added | | 8 | Secrets | PASS | None | | 9 | PII-free logging | PASS | No logging added | **Deploy risk:** none. Frontend-only, pure-function addition; no migration ordering, no rolling-deploy concern, no new service/healthcheck. The coverage-include change is the only CI-visible effect and it strictly increases measured scope. Verdict: ✅ Approved — zero operational footprint; coverage gate correctly tightened.
Author
Owner

🎨 UI/UX (Leonie) — PR Review

Verdict: Approved

# Item Status Note
1 Interaction states PASS (N/A) This is a string helper, not a component — no loading/empty/error UI in scope
2 i18n via Paraglide PASS Delegates to the shared formatter which uses Paraglide date_* keys (already present in de/en/es); the en/de tests prove localization works; no new strings hard-coded
3 Reuses established patterns PASS Reuses the exact shared formatter that DocumentDate.svelte / DocumentTopBarTitle.svelte / DocumentMultiSelect.svelte use — so a timeline chip reads byte-identically to the same date elsewhere (Nielsen #4 consistency)
4 Undated handling PASS UNKNOWN/undated → null (no chip); the warmer "Ohne Datum" bucket-header copy is correctly left to YearBand/TimelineView (#779), per Decision 4 — clean item-label vs bucket-label separation
5 a11y <time> / <abbr> PASS (deferred) The issue explicitly notes the <time datetime> wrapper and ca.<abbr> belong in EventCard/LetterCard (#779), not in this string helper. Correctly out of scope here.
6 RANGE rendering consistency PASS RANGE uses the shared full/collapsed-date output (Decision 3), so ranges read the same across timeline/detail/multi-select

Biggest UX consideration (not a gap): the helper returns a bare string, so all <time>/<abbr>/alt-text a11y affordances depend on the consuming card in #779 honouring the issue's notes. That is the correct boundary for this façade; I'll verify it at #779 review time.

Verdict: Approved — maximally consistent with existing date rendering; a11y wrappers correctly deferred to the consuming component.

### 🎨 UI/UX (Leonie) — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | Interaction states | PASS (N/A) | This is a string helper, not a component — no loading/empty/error UI in scope | | 2 | i18n via Paraglide | PASS | Delegates to the shared formatter which uses Paraglide `date_*` keys (already present in de/en/es); the en/de tests prove localization works; **no new strings hard-coded** | | 3 | Reuses established patterns | PASS | Reuses the *exact* shared formatter that `DocumentDate.svelte` / `DocumentTopBarTitle.svelte` / `DocumentMultiSelect.svelte` use — so a timeline chip reads byte-identically to the same date elsewhere (Nielsen #4 consistency) | | 4 | Undated handling | PASS | `UNKNOWN`/undated → `null` (no chip); the warmer "Ohne Datum" bucket-header copy is correctly left to `YearBand`/`TimelineView` (#779), per Decision 4 — clean item-label vs bucket-label separation | | 5 | a11y `<time>` / `<abbr>` | PASS (deferred) | The issue explicitly notes the `<time datetime>` wrapper and `ca.`→`<abbr>` belong in `EventCard`/`LetterCard` (#779), not in this string helper. Correctly out of scope here. | | 6 | RANGE rendering consistency | PASS | RANGE uses the shared full/collapsed-date output (Decision 3), so ranges read the same across timeline/detail/multi-select | **Biggest UX consideration (not a gap):** the helper returns a bare string, so all `<time>`/`<abbr>`/alt-text a11y affordances depend on the consuming card in #779 honouring the issue's notes. That is the correct boundary for this façade; I'll verify it at #779 review time. Verdict: ✅ Approved — maximally consistent with existing date rendering; a11y wrappers correctly deferred to the consuming component.
Author
Owner

🏛️ Architect (Markus) — PR Review

Verdict: Approved

# Item Status Note
1 Package-by-domain placement PASS New src/lib/timeline/ domain folder; helper lives in its own domain, not smeared across shared
2 Frontend boundary rule honoured PASS timeline added to boundaries/elements and a { from: timeline, allow: shared } dependency rule — the frontend analogue of "new backend domain → ArchUnit allow-list". timeline → shared only; nothing broader granted
3 routes → timeline edge PASS Deliberately not added yet — no route consumes timeline; the first consumer (#779) adds that edge in its own PR (Decision R7). Adding it now would be premature
4 Irreversible decision needs ADR PASS None — façade over an existing util, no new domain data model, no new dependency. Decision 5 explicitly defers the ADR to the backend entity issue. Correct
5 Contradicts an Accepted ADR? PASS No. Reuses the shared-formatter / DatePrecision-sharing pattern (ADR-039/040 family) rather than forking it
6 Reuses established pattern PASS Single source of truth preserved: all precision/rendering stays in documentDate.ts, inside the docs/date-label-fixtures.json cross-language drift-guard (#666). A forked formatter would have silently dropped timeline from the Java-drift contract — correctly avoided
7 Second source of truth introduced? PASS No — the whole point of the façade
8 DatePrecision type discipline PASS Hand-declared type kept (not migrated to the OpenAPI-generated enum); drift-risk comment added to documentDate.ts per Decision 8, documenting the migration decision point before #779 lands
9 Blast radius bounded PASS One new file + one comment + two config one-liners; no edits forced on unrelated domains
10 Person/AppUser separation PASS (N/A) Not touched

Do-Not-Touch (constitution §4) audit: no generated artifact edited (generated/api.ts, paraglide/ untouched); no shipped Flyway migration touched; no Accepted ADR edited; actions/(upload\|download)-artifact pin untouched; no CI guard weakened. The three "other edits" are all in-bounds: documentDate.ts = comment-only (not generated), eslint.config.js + vite.config.ts = config (not on the list).

Verdict: Approved — textbook reuse-over-reinvent; domain boundary registered in the same commit; no ADR owed.

### 🏛️ Architect (Markus) — PR Review **Verdict: ✅ Approved** | # | Item | Status | Note | |---|---|---|---| | 1 | Package-by-domain placement | PASS | New `src/lib/timeline/` domain folder; helper lives in its own domain, not smeared across `shared` | | 2 | Frontend boundary rule honoured | PASS | `timeline` added to `boundaries/elements` **and** a `{ from: timeline, allow: shared }` dependency rule — the frontend analogue of "new backend domain → ArchUnit allow-list". `timeline → shared` only; nothing broader granted | | 3 | `routes → timeline` edge | PASS | Deliberately **not** added yet — no route consumes timeline; the first consumer (#779) adds that edge in its own PR (Decision R7). Adding it now would be premature | | 4 | Irreversible decision needs ADR | PASS | None — façade over an existing util, no new domain data model, no new dependency. Decision 5 explicitly defers the ADR to the backend entity issue. Correct | | 5 | Contradicts an Accepted ADR? | PASS | No. Reuses the shared-formatter / DatePrecision-sharing pattern (ADR-039/040 family) rather than forking it | | 6 | Reuses established pattern | PASS | Single source of truth preserved: all precision/rendering stays in `documentDate.ts`, inside the `docs/date-label-fixtures.json` cross-language drift-guard (#666). A forked formatter would have silently dropped timeline from the Java-drift contract — correctly avoided | | 7 | Second source of truth introduced? | PASS | No — the whole point of the façade | | 8 | `DatePrecision` type discipline | PASS | Hand-declared type kept (not migrated to the OpenAPI-generated enum); drift-risk comment added to `documentDate.ts` per Decision 8, documenting the migration decision point before #779 lands | | 9 | Blast radius bounded | PASS | One new file + one comment + two config one-liners; no edits forced on unrelated domains | | 10 | `Person`/`AppUser` separation | PASS (N/A) | Not touched | **Do-Not-Touch (constitution §4) audit:** no generated artifact edited (`generated/api.ts`, `paraglide/` untouched); no shipped Flyway migration touched; no Accepted ADR edited; `actions/(upload\|download)-artifact` pin untouched; no CI guard weakened. The three "other edits" are all in-bounds: `documentDate.ts` = comment-only (not generated), `eslint.config.js` + `vite.config.ts` = config (not on the list). Verdict: ✅ Approved — textbook reuse-over-reinvent; domain boundary registered in the same commit; no ADR owed.
marcel added 1 commit 2026-06-13 13:38:16 +02:00
docs(adr): renumber SDD adoption ADR 041 -> 042 (collision with renovate ADR)
All checks were successful
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m48s
SDD Gate / RTM Check (pull_request) Successful in 15s
SDD Gate / Contract Validate (pull_request) Successful in 24s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / Constitution Impact (pull_request) Successful in 18s
CI / Unit & Component Tests (push) Successful in 4m58s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m51s
CI / fail2ban Regex (push) Successful in 48s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
CI / Unit & Component Tests (pull_request) Successful in 3m36s
b05990fffb
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>
marcel force-pushed feat/778-timeline-date-label from 33c6035199 to b05990fffb 2026-06-13 13:38:16 +02:00 Compare

⚠️ Constitution changed — Sync Impact review

.specify/constitution.md was modified in this PR. Per its §6 Sync Impact rule, re-read and reconcile every file below, and confirm the semantic version bump:

  • .claude/skills/draft-spec/SKILL.md
  • .claude/skills/implement/SKILL.md
  • .claude/skills/review-issue/SKILL.md
  • .claude/skills/review-pr/SKILL.md
  • .gitea/ISSUE_TEMPLATE/feature.md
  • .gitea/workflows/sdd-gate.yml
  • .specify/AGENTS.md
  • .specify/features/_example/adr-001-avatars-reuse-archive-bucket.md
  • .specify/features/_example/design.md
  • .specify/features/_example/spec.md
  • .specify/templates/adr.md
  • .specify/templates/feature-spec.md
  • CLAUDE.md
  • COLLABORATING.md
  • SPEC_DRIVEN_DEVELOPMENT.md
  • docs/adr/042-sdd-adoption.md
### ⚠️ Constitution changed — Sync Impact review `.specify/constitution.md` was modified in this PR. Per its §6 Sync Impact rule, re-read and reconcile every file below, and confirm the semantic version bump: - `.claude/skills/draft-spec/SKILL.md` - `.claude/skills/implement/SKILL.md` - `.claude/skills/review-issue/SKILL.md` - `.claude/skills/review-pr/SKILL.md` - `.gitea/ISSUE_TEMPLATE/feature.md` - `.gitea/workflows/sdd-gate.yml` - `.specify/AGENTS.md` - `.specify/features/_example/adr-001-avatars-reuse-archive-bucket.md` - `.specify/features/_example/design.md` - `.specify/features/_example/spec.md` - `.specify/templates/adr.md` - `.specify/templates/feature-spec.md` - `CLAUDE.md` - `COLLABORATING.md` - `SPEC_DRIVEN_DEVELOPMENT.md` - `docs/adr/042-sdd-adoption.md`
marcel merged commit b05990fffb into main 2026-06-13 14:04:56 +02:00
marcel deleted branch feat/778-timeline-date-label 2026-06-13 14:04:56 +02:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#824