Timeline: shared precision-aware date-label helper (#778) #824
Reference in New Issue
Block a user
Delete Branch "feat/778-timeline-date-label"
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?
Summary
Adds a thin timeline façade
frontend/src/lib/timeline/dateLabel.tsover the existing shared formatterformatDocumentDate($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 outsidedocumentDate.ts.timelineDateLabel(eventDate, precision, eventDateEnd?):formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale())for any dated, non-UNKNOWN event;null(no formatter call) forUNKNOWNprecision ornull/undefined/''eventDate;raw = null— timeline events have no verbatim spreadsheet cell, so season words derive only from the structured anchor month.REQ → test mapping
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 formatDocumentDateraw=null,getLocale()renders a DAY date localized in German,renders a DAY date localized in EnglishUNKNOWN→nullreturns null for UNKNOWN precision even with a date,returns null for UNKNOWN precision without a datenull/undefined/''eventDate →null, no formatter callreturns 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 RANGEdocumentDate.tsdelegates a same-year RANGE to formatDocumentDate(asserts byte-identical delegation)timelinein coverageincludesrc/lib/timeline/**added to vitest coverageinclude; exercised by all spec casesOther edits (per SDD spec review)
timelinefrontend domain infrontend/eslint.config.jsboundaries (allowed to import onlyshared).DatePrecisiontype: it is a hand-maintained mirror of the Java enum and must NOT be migrated to the OpenAPI-generated type.src/lib/timeline/**to the coverageincludeinfrontend/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
Requirements Engineer — PR Review
Verdict: ✅ Approved
Traceability of every REQ-001…006 from issue #778 against the diff:
Done?dateLabel.tsreturnsformatDocumentDate(...)raw=null,getLocale())formatDocumentDate(eventDate, precision, eventDateEnd ?? null, null, getLocale())UNKNOWN→null, no chip)precision === 'UNKNOWN'guardnull/undefined/''→null, no formatter call)!eventDateguard short-circuitsdocumentDate.ts)toBe(...)delegationtimelinein coverageinclude, 80% branch)src/lib/timeline/**invite.config.tsAll six REQ are implemented and tested. The RTM rows were added on the feature branch with
Status: Doneand 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.jsregistration and thedocumentDate.tsdrift-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.
🛠️ Developer (Felix Brandt) — PR Review
Verdict: ✅ Approved
formatDocumentDate; zero precision logic re-typed (KISS-over-DRY, constitution §3.2)timeline → sharedonly; registered ineslint.config.jsnpx vitest run→ 9 passed locally (confirmed)timelineDateLabel,eventDate,eventDateEnd,precision— nod/objDatePrecisionimport { formatDocumentDate, type DatePrecision }per issue import note.jssuffix on runtime import'$lib/paraglide/runtime.js'— matches the mock path in the spec (mismatch would silently skip the mock)generate:apineeded?ErrorCode?Comments-explaining-what vs why: the JSDoc and the inline
raw=nullcomment 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.
🧪 Tester (Sara Holt) — PR Review
Verdict: ✅ Approved
*.spec.ts(not*.svelte.spec.ts) → Nodeserverproject, as the issue specifies; no browser needed for a pure functionrequireAssertions: trueis on in this config, so no vacuous testformatDocumentDate(...)directly andtoBe(...)— not a brittle literal; if the shared formatter changes, this test tracks it instead of going stalenull,'',undefinedend-date all covered — both guard sub-conditionsruntime.jsviaimportOriginal, overriding onlygetLocale; realm.date_*exports preserved so the shared formatter runs its real logicJuli/en→JulyprovesgetLocale()actually threads through, not a no-op mockinclude; both guard branches + the delegation path are hit → comfortably within 80%beforeEachresets locale tode; ran locally → 9 passed in 1.17sCoverage note: I confirmed the run locally (single file, per the no-full-suite rule). Every executable branch in
dateLabel.tsis 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.
🔐 Security (Nora "NullX") — PR Review
Verdict: ✅ Approved
@RequirePermissionsurface.createdBy/updatedBy.raw=nullpassed explicitly (CWE-79 / untrusted-text)null. The shared formatter only ever usesrawto 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.{@html}/ escapingstring/null; no HTML, no interpolation. Card-level{@html}concern is correctly deferred to #779 (noted in issue).Adversarial read: the only data flowing in is
eventDate/eventDateEnd(structured ISO from a trusted DTO) andprecision(enum). Even if a future caller passed an attacker-influencedeventDate, the value reachesnew Intl.DateTimeFormat(...).format(noon(iso))andiso.slice(...)only — noeval, no HTML sink, no path/URL/query use. Theraw=nulldecision proactively closes the one channel (verbatim cell) that could carry untrusted text into a season word. Correct least-exposure choice.Verdict: ✅ Approved —
raw=nullis exactly the right defensive default; no new attack surface.⚙️ DevOps (Tobias) — PR Review
Verdict: ✅ Approved
upload/download-artifactpin untouchedvite.config.tsaddssrc/lib/timeline/**toinclude— tightens the gate (more code measured), never loosens it; thresholds (80/80/80/80) unchangedeslint.config.jsboundary entry well-formed; PR statesnpm run lintgreenDeploy 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.
🎨 UI/UX (Leonie) — PR Review
Verdict: ✅ Approved
date_*keys (already present in de/en/es); the en/de tests prove localization works; no new strings hard-codedDocumentDate.svelte/DocumentTopBarTitle.svelte/DocumentMultiSelect.svelteuse — so a timeline chip reads byte-identically to the same date elsewhere (Nielsen #4 consistency)UNKNOWN/undated →null(no chip); the warmer "Ohne Datum" bucket-header copy is correctly left toYearBand/TimelineView(#779), per Decision 4 — clean item-label vs bucket-label separation<time>/<abbr><time datetime>wrapper andca.→<abbr>belong inEventCard/LetterCard(#779), not in this string helper. Correctly out of scope here.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.
🏛️ Architect (Markus) — PR Review
Verdict: ✅ Approved
src/lib/timeline/domain folder; helper lives in its own domain, not smeared acrosssharedtimelineadded toboundaries/elementsand a{ from: timeline, allow: shared }dependency rule — the frontend analogue of "new backend domain → ArchUnit allow-list".timeline → sharedonly; nothing broader grantedroutes → timelineedgedocumentDate.ts, inside thedocs/date-label-fixtures.jsoncross-language drift-guard (#666). A forked formatter would have silently dropped timeline from the Java-drift contract — correctly avoidedDatePrecisiontype disciplinedocumentDate.tsper Decision 8, documenting the migration decision point before #779 landsPerson/AppUserseparationDo-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)-artifactpin 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.
33c6035199tob05990fffb⚠️ Constitution changed — Sync Impact review
.specify/constitution.mdwas 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.mdCLAUDE.mdCOLLABORATING.mdSPEC_DRIVEN_DEVELOPMENT.mddocs/adr/042-sdd-adoption.md