Timeline: shared precision-aware date-label helper (façade over formatDocumentDate) #778
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?
Milestone: Zeitstrahl — Family Timeline
Spec:
docs/superpowers/specs/2026-06-07-family-timeline-design.md§ "Date rendering" / "Frontend"Context
Events and letters on the timeline render dates the same way. A fully-tested, locale-aware, precision-aware formatter already exists in the codebase:
frontend/src/lib/shared/utils/documentDate.ts→formatDocumentDate(iso, precision, end, raw, locale). It covers all 7DatePrecisionvalues, is localized de/en/es, is consumed byDocumentDate.svelte,DocumentTopBarTitle.svelteandDocumentMultiSelect.svelte, and is guarded against Java drift bydocs/date-label-fixtures.json(asserted by bothdocumentDate.spec.tsand the JavaDocumentTitleFormatterTest, per issue #666).This issue is therefore NOT "implement a formatter" — it is "wrap the existing formatter behind a thin timeline façade". No precision logic is re-typed; all rendering stays in the shared module so it remains inside the cross-language fixtures drift-guard.
Scope
Add
frontend/src/lib/timeline/dateLabel.tsas a façade that delegates toformatDocumentDate. The timeline carries no verbatim spreadsheet cell, sorawis passed asnullexplicitly (the SEASON word derives from the structured anchor month viaseasonOfMonth()); the active locale is threaded viagetLocale().Import notes:
import type { DatePrecision }(type-only import) — consistent with how type-only imports are used elsewhere in the frontend and prevents any accidental tree-shaking difference.getLocalefrom'$lib/paraglide/runtime.js'(with.jsextension) — matches the majority pattern in utility code (DocumentDate.svelte,DocumentMultiSelect.svelte).DatePrecisionis hand-declared indocumentDate.ts, not OpenAPI-generated, so codegen does not surface it. See the drift-risk note below.Rendering by precision
Per the shared
formatDocumentDatecontract (authoritative; seedocs/date-label-fixtures.json):DAY28. Juli 1914MONTHJuli 1914SEASONraw=null)Sommer 1914YEAR1914APPROXca. 1914RANGE28. Juli 1914 – 11. Nov. 1918UNKNOWNnull— no per-item labelAll structured parts are localized de/en/es via
Intl.DateTimeFormat(locale, …)and Paraglidedate_*keys (already present in all three locales — no new strings).DatePrecision drift-risk note
DatePrecisionis hand-declared indocumentDate.tsand is the contract fordocs/date-label-fixtures.json. When the backendTimelineEventDTOis generated via OpenAPI (after issues 5/7 merge), a generatedDatePrecisiontype will also appear.dateLabel.tsshould stay on the hand-declared type — it is the fixtures-guard contract and must be updated manually together with the Java enum. A comment indocumentDate.tsdocuments this:// mirrors the backend DatePrecision enum; update manually together with the Java enum when values are added — do not migrate to the OpenAPI-generated type.CLAUDE.md + C4 diagram update
Doc-update rule triggers on "new domain module". This issue creates
lib/timeline/as a scaffold with a single helper — defer CLAUDE.md table +docs/architecture/c4/l3-frontend-*.pumlto issue 7, the first issue that adds a route. Note it explicitly in the issue 7 PR description so it is not forgotten. Do not add a stub C4 entry now (it would be incomplete/misleading).Notes for card issues 7–9 (planning)
timelineDateLabelshould be rendered inside a<time datetime="{eventDate}">element inEventCard.svelte/LetterCard.svelte, not a bare<span>— gives screen readers and search engines the machine-readable ISO date alongside the human-readable label.ca.may need<abbr title="circa">ca.</abbr>for screen reader clarity — a card-level concern.EventCard.svelteandLetterCard.sveltedo not use{@html}forevent.title,event.description, orletter.title. Svelte auto-escapes{value}interpolations;{@html}requires DOMPurify if ever added for rich text.Decisions resolved
dateLabel.tsis a one-line delegation toformatDocumentDate(…, raw=null, getLocale()). Rationale: DRY; keeps all precision logic in the shared module and inside the fixtures drift-guard; a forked formatter would silently drop timeline rendering from the Java-drift contract (#666). (Unanimous: Markus, Felix, Sara, Elicit, Leonie.)npm run generate:apitask from this issue. Timeline DTOs do not exist in the OpenAPI spec yet (backend issues 2–5 unmerged); running codegen now is a no-op. The codegen step belongs in the first frontend issue that consumes a timeline DTO (issue 7), after the assembly endpoint (issue 5) is merged. (Markus, Felix, Tobias, Elicit.)1914–1918year-span is a band-placement concern owned byYearBand/TimelineView, out of scope here. (Markus, Elicit, Leonie.)null; "Ohne Datum" is a bucket header. Undated items render no date chip; warmer family-reader copy lives on the bucket header rendered byYearBand/TimelineView. Separates item-label from bucket-label cleanly. (Elicit, Leonie.)raw=nullexplicitly. The timeline has no operator-entered verbatim cell; passingnullkeeps the SEASON word coming from the structured anchor month, never from untrusted free text. (Nora.)documentDate.tsnow documents the migration decision point before issue 7 lands. (Decision resolved R1.)Tasks
frontend/src/lib/timeline/dateLabel.tsas the façade above.formatDocumentDate, passraw=null, threadgetLocale(), returnnullfor UNKNOWN/undated.getLocalefrom'$lib/paraglide/runtime.js'(with.jsextension).import type { DatePrecision }(type-only import).raw=nullis intentional (see code block above).undefinedandnullare equivalent foreventDateEnd.DatePrecisioninfrontend/src/lib/shared/utils/documentDate.ts(see note above).'src/lib/timeline/**'to the coverageincludearray infrontend/vite.config.ts(1-line change; ensures the 80% branch gate applies to the new module).frontend/src/lib/timeline/dateLabel.spec.ts(see Tests section below).Acceptance criteria
formatDocumentDatehelper, so event and letter dates are identical to how they render elsewhere in the app, localized de/en/es.UNKNOWN/undated entries yieldnullfrom the façade (no date chip); the "Ohne Datum" bucket label is not produced by this helper.documentDate.ts; the fixtures drift-guard remains the single source of truth.'src/lib/timeline/**'is present invite.config.tscoverage includes so the module is within the 80% branch gate.Tests
frontend/src/lib/timeline/dateLabel.spec.ts— a thin delegation/integration suite, NOT a precision matrix (the full matrix lives indocumentDate.spec.ts+ the fixtures guard). File extension is*.spec.ts(not*.svelte.spec.ts) — runs in the Vitestserverproject (Node environment). Mock pattern:vi.mock('$lib/paraglide/runtime', () => ({ getLocale: vi.fn(() => 'de') })).Test cases (name as full sentences for self-documenting CI output):
'returns localized DAY label containing the day number and month name in German'—timelineDateLabel('1914-07-28', 'DAY')withgetLocale→'de', assert contains28andJuli.'returns localized DAY label in English when locale is en'— same date withgetLocale→'en', assert containsJuly.'derives localized season word from anchor month when precision is SEASON (raw=null path)'—timelineDateLabel('1916-04-01', 'SEASON')→ containsFrühling(de) /Spring(en).'returns null for UNKNOWN precision with a date'—timelineDateLabel('1914-07-28', 'UNKNOWN')→null.'returns null for UNKNOWN precision without a date'—timelineDateLabel(null, 'UNKNOWN')→null.'returns null for non-UNKNOWN precision when eventDate is null'—timelineDateLabel(null, 'APPROX')→null(façade guard short-circuits beforeformatDocumentDate).'returns null for non-UNKNOWN precision when eventDate is empty string'—timelineDateLabel('', 'DAY')→null(empty string is falsy).'delegates same-year RANGE to shared formatter with concrete output'—timelineDateLabel('1914-01-01', 'RANGE', '1914-11-11')→toContain('1914')andtoContain('Nov')(or match exact shared-formatter output; useformatDocumentDatedirectly to compute expected value rather than a literal string).Single-file
*.spec.ts, runs locally in seconds; full sweep left to CI.Timeline: shared precision-aware date-label helper + API typesto Timeline: shared precision-aware date-label helper (façade over formatDocumentDate)🏗️ Markus Keller — Senior Application Architect
Observations
The façade pattern is the correct architectural choice here. Verified in the codebase:
formatDocumentDatealready servesDocumentDate.svelte,DocumentTopBarTitle.svelte, andDocumentMultiSelect.svelte— adding a timeline façade that delegates to it keeps exactly one precision-rendering code path. This is the right boundary.One structural point that deserves explicit attention: the issue correctly notes that
DatePrecisionis hand-declared indocumentDate.tsand must not be migrated to the OpenAPI-generated type. But the drift-risk comment the issue requests adding todocumentDate.tsshould be specific enough to prevent a future developer from doing the "obvious cleanup" of importing the generated type. The comment in the code block shown (// mirrors the backend DatePrecision enum...) is good — make sure it lands indocumentDate.tsbefore the type declaration, not after, so it's the first thing a reader sees.On the
vite.config.tscoverage include: I checked the current list. It coverssrc/lib/shared/utils/**andsrc/lib/document/**but has no entry forsrc/lib/timeline/**. The issue correctly identifies this as a required 1-line addition. This is not optional — without it, the newlib/timeline/directory is outside the 80% branch gate and coverage can silently drop. The task list includes it; confirm it lands in the same commit asdateLabel.ts.On the import extension
.jsvs no extension: The issue specifies'$lib/paraglide/runtime.js'with the.jsextension. Verified in the codebase:DocumentDate.svelteandDocumentMultiSelect.svelteboth use the.jsextension;TimelineDensityFilter.svelteimports without.js. The.jsextension is the dominant pattern and matches SvelteKit's ESM resolution. Useruntime.js.On CLAUDE.md + C4 deferral: The decision to defer CLAUDE.md table and C4 diagram updates to issue 7 is correct. A single helper file does not constitute a "new domain module" in the sense of the doc-update trigger. Issue 7 (first route) is the right boundary. This must be called out in the issue 7 PR description — record it as a TODO there, not just as a note here.
Recommendations
documentDate.tsabove theDatePrecisiontype declaration (not below), so it's the first signal a reader encounters when they look at the type.'src/lib/timeline/**'is added tovite.config.tscoverageincludein the same commit as the new module — never in a follow-up.lib/timeline/" as a required task. Put it in the PR description checklist, not just in this issue body.Open Decisions (none)
All architectural decisions on this issue are resolved and correctly documented in the "Decisions resolved" section.
👨💻 Felix Brandt — Senior Fullstack Developer
Observations
I read
formatDocumentDatein full. One behavioral subtlety that the test suite must capture explicitly:formatDocumentDateforUNKNOWNreturnsm.date_precision_unknown()— a localized string like"Datum unbekannt"— notnull. The façade intercepts before callingformatDocumentDatewhenprecision === 'UNKNOWN' || !eventDateand returnsnullinstead. This is intentional (per the spec: "façade returnsnull— no per-item label"), but means the façade and the shared formatter have different contracts for the UNKNOWN case.The test named
'returns null for UNKNOWN precision with a date'covers this correctly. What's missing: a test that confirms the façade does not fall through toformatDocumentDatefor UNKNOWN — i.e., that it doesn't return the localized "Datum unbekannt" string. The existing proposed test→ nullcovers the return value, but an assertion likeexpect(result).not.toBe(m.date_precision_unknown(...))would make the behavioral distinction from the shared formatter explicit. That said,toBe(null)already excludes the string — this is a documentation concern, not a coverage gap.On the mock pattern: The issue specifies
vi.mock('$lib/paraglide/runtime', () => ({ getLocale: vi.fn(() => 'de') }))— without.js. But the import indateLabel.tsis'$lib/paraglide/runtime.js'. In Vitest,vi.mockmust match the exact import specifier. IfdateLabel.tsimports from'$lib/paraglide/runtime.js', the mock path must be'$lib/paraglide/runtime.js'too. Mismatch causes the mock to be silently skipped andgetLocale()returns the real locale (which may or may not be'de'in the Node test environment). This is a concrete risk — the tests for locale switching (the English/Spanish assertions) would pass vacuously if the mock silently misses.On
'src/lib/timeline/**'in vite.config.ts coverage include: Theserverproject currently coverssrc/lib/shared/utils/**andsrc/lib/document/**.dateLabel.tswill be atsrc/lib/timeline/dateLabel.ts. The 1-line addition toincludeis correct and necessary; without it, Vitest won't measure branch coverage fordateLabel.tseven if the tests run.On import style:
import type { DatePrecision }— type-only import is correct and required. Confirm this is a type-only import sinceDatePrecisionis a union type string literal, not a runtime value.Recommendations
vi.mock('$lib/paraglide/runtime.js', ...)(with.jsextension) to match the exact import specifier indateLabel.ts. Verify by temporarily assertinggetLocaleis called and confirming the spy records the call.formatDocumentDate('1914-01-01', 'RANGE', '1914-11-11', null, 'de')as the expected value (dynamically computed), not a hardcoded string — the spec says this, and it's the right approach. Hardcoded strings will fail when the shared formatter's RANGE logic changes.dateLabel.ts, then the drift-risk comment todocumentDate.ts, then the coverage include line. Keep these as separate atomic commits.Open Decisions (none)
🔒 Nora "NullX" Steiner — Application Security Engineer
Observations
The
raw=nulldecision (resolved Decision 6) is the security-correct call. I verified informatDocumentDatethat therawparameter is only used in theSEASONbranch viaseasonFromRaw(raw)— it extracts the first token, lowercases it, and looks it up in a fixedRecord<string, Season>. The function returnsnullfor any non-matching token. It is not reflected into HTML and is not displayed verbatim. So even ifrawwere passed through from the timeline, the actual attack surface would be minimal — but passingraw=nullexplicitly is still the right design because:nullexplicitly documents that this is intentional, not an oversight.descriptionortitlecould accidentally be passed asrawduring a refactor.The JSDoc comment the spec prescribes (
// raw is always null for timeline events — no verbatim spreadsheet cell) is the correct self-documenting approach. This is exactly the kind of security comment I want to see: it explains why, not what.On the
{@html}note in the issue body (Notes for cards 7–9): The issue correctly notes thatEventCard.svelteandLetterCard.svelteshould not use{@html}for title/description/letter title. This is worth making an explicit acceptance criterion on issue 7, not just a planning note. Svelte auto-escapes{value}interpolations — as long as no{@html}is added, XSS from event titles is structurally impossible. But "confirm it's not there" should be a checklist item on the card-component issues.On the
getLocale()call being inside the façade: The locale is called at render time, not at module import time, which is correct. A module-levelconst locale = getLocale()would capture the locale once at initialization and be stale for users who switch languages. CallinggetLocale()inside the function body ensures the current locale is used on each call. No concern here.On input validation:
eventDatecomes through the façade typed asstring | null | undefined. The function guards on!eventDate(which catchesnull,undefined, and''). This is sufficient. No additional sanitization is needed since the string goes toformatDocumentDate, which passes it toIntl.DateTimeFormatvianew Date(iso + 'T12:00:00')— an unparseable date producesInvalid Date, whichIntl.DateTimeFormat.format()renders as "Invalid Date", not a script injection point.Recommendations
raw=nullsecurity comment indateLabel.tsmust be present from the first commit (not added in a follow-up). It documents the deliberate security decision.EventCard.svelteandLetterCard.svelteuse{value}interpolation, not{@html}, for all user-controlled string fields." One line prevents a class of future vulnerabilities.documentDate.spec.tssecurity test ('ignores a malicious raw value for the structured label') covers the shared formatter. The timeline façade's spec does not need a dedicated security test because the façade always passesraw=null— there is no path to reach the vulnerable parameter. This is correct scope-bounding.Open Decisions (none)
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
The test specification in the issue is the most detailed I've seen on this milestone so far. Eight named test cases, explicitly designated as a delegation/integration suite (not a precision matrix), with the right environment annotation (
*.spec.ts→serverproject). This is solid.Critical: mock path must match import specifier. Felix flagged this from the implementation angle; I'm flagging it from the test-reliability angle. If
vi.mock('$lib/paraglide/runtime', ...)(no.js) is used while the source imports from'$lib/paraglide/runtime.js', Vitest's module mock does not intercept the call. ThegetLocalespy never fires. The test for English locale ('returns localized DAY label in English when locale is en') would then pass only if the test environment happens to return'en'as its locale — which it won't (Node's default is typically'en'but Paraglide'sgetLocale()in a Node test environment returns whatever the runtime has initialized, which may not be'de'). This is a silent false-positive risk — the test passes for the wrong reason. The mock path must be'$lib/paraglide/runtime.js'.On coverage gate applicability: The
serverproject's coverage config (invite.config.ts) runs onsrc/lib/shared/utils/**,src/lib/shared/server/**,src/lib/shared/discussion/**,src/lib/document/**, andsrc/hooks.server.ts. The newsrc/lib/timeline/dateLabel.tswill be at a path outside all currentincludeentries. Without adding'src/lib/timeline/**', the 8 proposed tests will run and pass, butdateLabel.tswill never appear in the coverage report — the 80% branch gate gives false assurance. This is specifically called out in the tasks, but I want to highlight it as a pre-commit checklist item: runnpm run test:coverageafter adding the include line and confirmdateLabel.tsappears in the report with ≥80% branch coverage.On the RANGE delegation test: The spec says
use formatDocumentDate directly to compute expected value rather than a literal string. This is the right approach, and I'd strengthen it slightly: the computed expected value should be captured in aconst expected = formatDocumentDate(...)at the top of the test body, then asserted againsttimelineDateLabel(...). This makes the test self-documenting (the two calls sit side by side, showing the equivalence intent) and eliminates hardcoded literals that rot.On the "SEASON derives from anchor month when raw=null" test: The test for
'1916-04-01'expectsFrühling(spring). This matchesseasonOfMonth(4)indocumentDate.tswhich returns'spring'. Verified correct. But the test should also implicitly verify thatrawwas not passed through — since the façade always passesraw=null, the season word must come from the anchor month. The test is correct by construction (passingnullraw), but a descriptive comment in the test body would make this intent clear for reviewers.On empty-string guard: The test
'returns null for non-UNKNOWN precision when eventDate is empty string'is valuable and often missed.''is falsy in JavaScript, so!eventDatecatches it. Confirmed correct behavior.Missing edge case (minor): No test for
eventDateEndbeingundefinedvsnullforRANGE. The JSDoc notes they're equivalent, and the implementation useseventDateEnd ?? null— but a test confirmingtimelineDateLabel('1914-01-01', 'RANGE', undefined)behaves identically totimelineDateLabel('1914-01-01', 'RANGE', null)would be a clean regression guard for the??operator.Recommendations
'$lib/paraglide/runtime.js'. Add a spy assertion (expect(getLocale).toHaveBeenCalled()) in at least one locale-switching test to confirm the mock is actually intercepted.npm run test:coverage(server project) and confirmsrc/lib/timeline/dateLabel.tsappears in the report.'treats undefined eventDateEnd as null for RANGE'— passesundefinedas the third arg and asserts the output equals thenullcase.Open Decisions (none)
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
This issue is purely infrastructure — a date-label helper with no UI surface of its own. My review therefore focuses on the downstream contracts this helper establishes for the card issues (7–9) that will render its output.
The
<time datetime>note is exactly right and critical. The spec notes: "The date label string returned bytimelineDateLabelshould be rendered inside a<time datetime="{eventDate}">element." This is not optional for accessibility:<time>(e.g., NVDA + Chrome) can expose the machine-readabledatetimeattribute alongside the human label.datetimeattribute must be a valid date/time string. ForDAYprecision,eventDate(ISOYYYY-MM-DD) is a valid value. ForYEARprecision,1914is a valid year value. ForRANGE, thedatetimeshould be the start date (end can go in a separate attribute or omitted).APPROXprecision, thedatetimeshould beeventDate(the ISO anchor), even though the label says "ca. 1914" — the machine-readable value is more precise than the label, which is fine (the label is the honest human representation, the datetime is the best-known anchor).SEASON,datetime="1916-04"(month precision) is arguably more honest than the full1916-04-01anchor, butYYYY-MM-DDis also valid and simpler to implement. Either is acceptable; pick one and document it in the card spec.The
<abbr title="circa">ca.</abbr>note: This is a good accessibility affordance for screen reader users who may not know "ca." means "circa". However,<abbr>is inconsistently supported across screen readers — some announce the title, many do not. A more reliable approach is to usearia-labelon the<time>element:<time datetime="1914" aria-label="circa 1914">ca. 1914</time>. This ensures the accessible name is "circa 1914" regardless of screen reader support for<abbr>. Flag this as a card-level decision for issue 7."Ohne Datum" copy: The spec correctly separates the "Ohne Datum" bucket header from the per-item date label. The bucket header copy is a UX concern for
YearBand/TimelineView— but I'll note now that "Ohne Datum" is appropriate German for the family archive context. The English equivalent indate_precision_unknownis presumably "Date unknown" (or "Undated") — confirm both keys are present and natural-sounding inmessages/{en,es}.jsonbefore issue 7 ships.On precision rendering parity: The spec table in the issue body matches the spec file exactly for the German forms. The RANGE label (
28. Juli 1914 – 11. Nov. 1918) uses an en-dash (–), which is typographically correct. Confirm the existingrangeLabelfunction indocumentDate.tsuses an en-dash and not a hyphen-minus — verified: line 100 showsformatMCDate(iso, locale)} – ${formatMCDate(end, locale)with an en-dash. Good.Recommendations
<time datetime>rendering contract explicitly in issue 7's task list, including the value to use for each precision:DAY→YYYY-MM-DD,MONTH→YYYY-MM,YEAR→YYYY,RANGE→start date,APPROX→YYYY-MM-DDanchor,SEASON→YYYY-MManchor month.APPROX, prefer<time datetime="1914" aria-label="circa 1914">ca. 1914</time>over<abbr>for better screen reader coverage.date_precision_unknownkey produces natural copy in all three locales (de/en/es) before issue 7. "Date unknown" (en) and "Fecha desconocida" (es) are reasonable — check the actual values inmessages/en.jsonandmessages/es.json.Open Decisions (none)
📋 Elicit — Requirements Engineer
Observations
The issue body is implementation-spec-level and very well structured. The scope is tightly bounded: one helper file, one drift-risk comment, one coverage include line, eight tests. There is no ambiguity in what "done" looks like.
One specification gap worth noting: The acceptance criteria say "No precision/rendering logic is duplicated outside
documentDate.ts" — this is verifiable by inspection. But there is no criterion covering what happens when a future enum value is added toDatePrecisionon the backend that the frontend hasn't seen yet. TheswitchinformatDocumentDatehas adefaultcase that returnsm.date_precision_unknown(). The façade's guard returnsnullforUNKNOWNand then delegates for all other values — including any unknown future value. This means a new backend precision value (e.g.,DECADE) would silently fall through toformatDocumentDate'sdefaultbranch and render as "Datum unbekannt". This is arguably correct (fail-safe), but it should be explicit in the drift-risk comment so future developers know this is the intended behavior.On the "Decisions resolved" section: All 8 resolved decisions are well-documented with the rationale and the personas who confirmed them. This is the correct pattern for issue bodies that have gone through multiple review rounds. No open questions remain.
On the breakdown of dependencies: This issue (issue 6 in the milestone) depends on nothing from the backend (issues 2–5 unmerged). It is correctly scheduled as a standalone frontend piece. The codegen deferral (Decision 2) is the right call — running
npm run generate:apinow would be a no-op and create confusion.One missing acceptance criterion: The issue's AC section does not include: "The drift-risk comment is present in
documentDate.tsand accurately describes the migration decision point." The drift-risk comment is in the task list but not in the ACs. Since ACs are what CI and reviewers check, add it there.Recommendations
documentDate.tsabove theDatePrecisiontype declaration, explaining that this type must be updated manually with the Java enum and must not be migrated to the OpenAPI-generated type."formatDocumentDate'sdefault→ "Datum unbekannt"). This is the expected fail-safe behavior — document it.Open Decisions (none)
🖥️ Tobias Wendt — DevOps & Platform Engineer
No concerns from my angle. This issue creates a single TypeScript helper file and a spec — no new Docker services, no infrastructure changes, no environment variables, no CI workflow changes. The coverage include addition to
vite.config.tsis a build-tool config touch, not an infrastructure one.One thing I verified: the
serverproject invite.config.tsruns in Node environment and excludes*.svelte.spec.ts. The proposeddateLabel.spec.ts(without.svelte.) will correctly run in theserverproject (Node), not the browser project. The mock patternvi.mock('$lib/paraglide/runtime.js', ...)will work in Node without browser setup —getLocale()doesn't need a DOM.The only CI-adjacent observation: the
npm run test:coveragecommand currently runs theserverproject coverage. After adding'src/lib/timeline/**'to the include list, that project's 80% branch gate will now includedateLabel.ts. With 8 tests covering all 7 precision values plus null/empty guards, branch coverage ondateLabel.tsshould comfortably exceed 80%. No risk to the gate from this addition.