Initial Commit
This commit is contained in:
106
openspec/archive/2026-05-09-i18n-formatter-date/proposal.md
Normal file
106
openspec/archive/2026-05-09-i18n-formatter-date/proposal.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# `@suaveplan/i18n-formatter-date` — `Intl.DateTimeFormat` wrapper
|
||||
|
||||
**Package**: `@suaveplan/i18n-formatter-date` (NEW)
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature (new universal-tier package; verbatim port of `formatting/date/` plus class wrapper + Zod schema + observability uplift)
|
||||
**Date**: 2026-05-05
|
||||
**Wave**: 3 (depends on Wave 1 foundation; required by Wave 6 core slim)
|
||||
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` (especially §5 formatter package shape, §5b FormatBridge)
|
||||
**Path**: `packages/i18n/i18n-formatter-date/`
|
||||
**Tier**: universal
|
||||
|
||||
## Why
|
||||
|
||||
The 14-package i18n modular split lifts each `Intl.*` wrapper out of the monolith into its own sibling package. `@suaveplan/i18n-formatter-date` ports the existing `formatting/date/` module — the broadest of the formatters today — and brings it up to the canonical Wave-3 shape (Zod-validated options, OTel histogram + cache-hit counter, class wrapper for symmetry, sibling test files).
|
||||
|
||||
Today `formatting/date/` ships a rich surface (180 LoC):
|
||||
|
||||
- `formatDate(date, locale, options?)` standalone with preset-or-options resolution (`packages/i18n/i18n/src/formatting/date/date.ts` L64–L73)
|
||||
- `formatTime(date, locale, long?)` standalone (`date.ts` L79–L83) — short / long preset
|
||||
- `formatDateOnly(date, locale, long?)` standalone (`date.ts` L89–L97)
|
||||
- `formatDateRange(start, end, locale, options?)` standalone using `formatRange` (`date.ts` L104–L116)
|
||||
- `getDayNames(locale, style?)` ECMA-402-§10.6-`getWeekInfo()`-aware day-name list (`date.ts` L126–L160)
|
||||
- `getMonthNames(locale, style?)` (`date.ts` L166–L180)
|
||||
- 8-preset table (`PRESET_MAP` at L4–L41): `short`, `medium`, `long`, `full`, `time-short`, `time-long`, `date-short`, `date-long`
|
||||
- BCP-47 normalisation via `LOCALE_BCP47_MAP` from shared constants (`date.ts` L43–L49)
|
||||
- Type definitions (`date.types.ts` L1–L24): `DateFormatPreset`, `DateFormatOptions`, `ExtendedDateOptions`
|
||||
|
||||
This package introduces no new functionality beyond what the monolith ships — the changes are: (a) wrapping the standalones in a `DateFormatter` class for symmetry with `NumberFormatter`/`RelativeTimeFormatter`/`ListFormatter`, (b) adding Zod-validated options, (c) adding the canonical OTel histogram + cache-hit counter (the monolith's date formatter currently does not emit either), and (d) introducing a per-(locale, options-hash) cache (the monolith currently constructs a fresh `Intl.DateTimeFormat` per call).
|
||||
|
||||
## What changes
|
||||
|
||||
### New package: `@suaveplan/i18n-formatter-date`
|
||||
|
||||
- ADDED: `packages/i18n/i18n-formatter-date/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
|
||||
- ADDED: `src/formatter.ts` — `DateFormatter` class (NEW); `formatDate` standalone (port of monolith L64–L73) extended with cache and OTel emission; `formatDateRange` standalone (port of L104–L116); `clearDateFormatterCache` test-only helper. `PRESET_MAP` and `resolvePreset` helper (port of L4–L58). `toBcp47` helper (port of L43–L49).
|
||||
- ADDED: `src/formatter.types.ts` — `DateFormatPreset`, `DateFormatOptions`, `ExtendedDateOptions`, `DateFormatterCacheKey`. Sourced from monolith `date.types.ts` L1–L24 plus new types for the class surface.
|
||||
- ADDED: `src/formatter.schemas.ts` — `DateFormatterOptionsSchema` Zod schema. NEW (the monolith does not currently Zod-validate options).
|
||||
- ADDED: `src/helpers.ts` — `formatTime(date, locale, long?)` (port of monolith L79–L83); `formatDateOnly(date, locale, long?)` (port of L89–L97); `getDayNames(locale, style?)` (port of L126–L160); `getMonthNames(locale, style?)` (port of L166–L180).
|
||||
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.date.duration_ms` + counter `i18n.format.cache_hit{formatter="date"}`.
|
||||
- ADDED: `src/formatter.test.ts` — port of monolith `date.test.ts` (185 lines) plus new tests for `DateFormatter` class, Zod-validation, cache reuse.
|
||||
- ADDED: `src/formatter.spec-conformance.test.ts` — every requirement scenario in this delta.
|
||||
- ADDED: `src/helpers.test.ts` — covers `formatTime`, `formatDateOnly`, `getDayNames`, `getMonthNames` (including `getWeekInfo()`-fallback for older runtimes).
|
||||
- ADDED: `src/index.ts` — explicit named re-exports.
|
||||
- ADDED: `src/formatter.md`, `src/helpers.md`, `src/observability.md` — sibling docs.
|
||||
|
||||
### Subpath exports
|
||||
|
||||
- `.` — root barrel
|
||||
- `./formatter` — `DateFormatter` class + `formatDate` + `formatDateRange` + types + schema only
|
||||
- `./helpers` — `formatTime`, `formatDateOnly`, `getDayNames`, `getMonthNames` only
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`, `LOCALE_BCP47_MAP`
|
||||
- `@suaveplan/types workspace:^`
|
||||
- `@suaveplan/error workspace:^`
|
||||
- `@opentelemetry/api catalog:observability`
|
||||
- `tslib catalog:build`
|
||||
- `zod catalog:forms`
|
||||
- **No** dep on any other formatter package, on `@suaveplan/i18n` core, or on any backend package.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Removing `formatting/date/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
|
||||
- **`formatToParts` for date output** — out of scope for Wave 3; can be added as a follow-up if downstream consumers (e.g. `react-data-table` date columns) need part-by-part rendering.
|
||||
- **`Temporal` API support** — out of scope; `Intl.DateTimeFormat` accepts `Date | number` today and the monolith's surface matches that. Temporal-aware overloads are a follow-up once Temporal stabilises across runtimes.
|
||||
- **Custom calendar systems beyond what `Intl.DateTimeFormat` supports** — out of scope.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Requirement | Source file | Source lines | Notes |
|
||||
|---|---|---|---|
|
||||
| `formatDate` standalone with preset-or-options resolution | `packages/i18n/i18n/src/formatting/date/date.ts` | L64–L73 | Verbatim port; extended with cache + OTel emission |
|
||||
| `PRESET_MAP` (8 presets: short/medium/long/full/time-short/time-long/date-short/date-long) | `packages/i18n/i18n/src/formatting/date/date.ts` | L4–L41 | Verbatim port |
|
||||
| `formatTime` standalone | `packages/i18n/i18n/src/formatting/date/date.ts` | L79–L83 | Verbatim port (helper) |
|
||||
| `formatDateOnly` standalone | `packages/i18n/i18n/src/formatting/date/date.ts` | L89–L97 | Verbatim port (helper) |
|
||||
| `formatDateRange` standalone | `packages/i18n/i18n/src/formatting/date/date.ts` | L104–L116 | Verbatim port |
|
||||
| `getDayNames` (`getWeekInfo()`-aware) | `packages/i18n/i18n/src/formatting/date/date.ts` | L126–L160 | Verbatim port |
|
||||
| `getMonthNames` | `packages/i18n/i18n/src/formatting/date/date.ts` | L166–L180 | Verbatim port |
|
||||
| `toBcp47` BCP-47 normalisation | `packages/i18n/i18n/src/formatting/date/date.ts` | L43–L49 | Verbatim port; use `LOCALE_BCP47_MAP` from `@suaveplan/i18n-shared` |
|
||||
| `resolvePreset` preset+options merge | `packages/i18n/i18n/src/formatting/date/date.ts` | L51–L58 | Verbatim port |
|
||||
| `DateFormatPreset`, `DateFormatOptions`, `ExtendedDateOptions` types | `packages/i18n/i18n/src/formatting/date/date.types.ts` | L1–L24 | Verbatim port |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `DateFormatter` class wrapper | New — symmetric with `NumberFormatter`, `RelativeTimeFormatter`, `ListFormatter`, `DisplayNameFormatter` |
|
||||
| `DateFormatterOptionsSchema` Zod schema | New — current monolith does not validate options |
|
||||
| `INVALID_FORMATTER_OPTIONS` mapping | Bound to the new Zod schema |
|
||||
| Per-(locale, options-hash) cache | New — current monolith constructs a fresh `Intl.DateTimeFormat` per call; Wave-3 formatter shape mandates the cache for parity with sibling packages |
|
||||
| OTel histogram `i18n.format.date.duration_ms` + cache-hit counter | New — canonical Wave-3 formatter shape mandates these; monolith date formatter currently does not emit either |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `packages/i18n/i18n-formatter-date/src/formatter.test.ts` — port of monolith `date.test.ts`; all 8 presets render correctly; raw `Intl.DateTimeFormatOptions` work; preset + raw merged correctly via `resolvePreset`; class wrapper twins helper output; cache reuse on identical key; bogus locale → `UNSUPPORTED_LOCALE`; Zod failure → `INVALID_FORMATTER_OPTIONS`
|
||||
- `packages/i18n/i18n-formatter-date/src/formatter.spec-conformance.test.ts` — every requirement scenario runs as an executable test
|
||||
- `packages/i18n/i18n-formatter-date/src/helpers.test.ts` — `formatTime(d, "en-GB", false)` returns short-style time; `formatTime(d, "en-GB", true)` includes seconds; `formatDateOnly(d, "en-GB", false)` short style; `getDayNames("en-GB")` returns 7 strings starting from Monday (en-GB `getWeekInfo()` → 1); `getMonthNames("en-GB")` returns 12 strings; `getDayNames` falls back to Monday-start when `getWeekInfo()` is unavailable
|
||||
- `packages/i18n/i18n-formatter-date/src/observability.test.ts` — histogram named `i18n.format.date.duration_ms`; counter `i18n.format.cache_hit{formatter="date"}` increments only on cache hit
|
||||
- `packages/i18n/i18n-formatter-date/src/index.test.ts` — every documented public export resolves; subpaths resolve
|
||||
|
||||
## Impact
|
||||
|
||||
- New universal-tier package — joins the rest of the i18n family.
|
||||
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core wires `formatDate` and `formatTime` into the default `FormatBridge` for ICU `{date, date, short}` and `{ts, time, long}` placeholder rendering.
|
||||
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.
|
||||
@@ -0,0 +1,208 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec: i18n-formatter-date (i18n-formatter-date)
|
||||
|
||||
**Package**: `@suaveplan/i18n-formatter-date`
|
||||
**Status**: PROPOSED — first delta; becomes canonical spec on archive.
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: DateFormatter class wrapping `Intl.DateTimeFormat`
|
||||
|
||||
The package SHALL export a `DateFormatter` class that wraps `Intl.DateTimeFormat` with construction-time Zod validation of options and `format(date: Date, locale: string): string` and `formatRange(start: Date, end: Date, locale: string): string` methods. Construction with malformed options SHALL throw `I18nError(I18N.INVALID_FORMATTER_OPTIONS)`.
|
||||
|
||||
#### Scenario: format with default options
|
||||
|
||||
- **GIVEN** `const f = new DateFormatter()`
|
||||
- **WHEN** `f.format(new Date(2026, 0, 1), "en-GB")` is called
|
||||
- **THEN** the result contains `"2026"` (locale-correct rendering)
|
||||
|
||||
#### Scenario: invalid options throw INVALID_FORMATTER_OPTIONS
|
||||
|
||||
- **WHEN** `new DateFormatter({ preset: "invalid" as never })` is called
|
||||
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_FORMATTER_OPTIONS"`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"DateFormatter formats with default options"` AND `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"DateFormatter rejects invalid options with INVALID_FORMATTER_OPTIONS"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `formatDate` standalone helper with preset resolution
|
||||
|
||||
The package SHALL export a `formatDate(date, locale, options?)` standalone helper. When `options.preset` is supplied, it SHALL be resolved against the 8-entry `PRESET_MAP` (`short`, `medium`, `long`, `full`, `time-short`, `time-long`, `date-short`, `date-long`). Raw `Intl.DateTimeFormatOptions` (`weekday`, `year`, `month`, `day`, etc.) supplied alongside `preset` SHALL override the preset's defaults for those fields.
|
||||
|
||||
#### Scenario: preset short renders short style
|
||||
|
||||
- **WHEN** `formatDate(new Date(2026, 0, 1), "en-GB", { preset: "short" })` is called
|
||||
- **THEN** the result is a short-style date+time string (e.g. `"01/01/2026, 00:00"` on en-GB)
|
||||
|
||||
#### Scenario: raw options override preset
|
||||
|
||||
- **WHEN** `formatDate(d, "en-GB", { preset: "short", year: undefined })` is called
|
||||
- **THEN** the result drops the year field while retaining the preset's other defaults
|
||||
|
||||
#### Scenario: standalone twins class output
|
||||
|
||||
- **WHEN** both `formatDate(d, l, opts)` and `new DateFormatter(opts).format(d, l)` are called for the same `(d, l, opts)`
|
||||
- **THEN** the results are identical
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"formatDate preset short renders short style"` AND `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"formatDate raw options override preset"` AND `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"formatDate output twins DateFormatter.format output"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `formatDateRange` standalone helper
|
||||
|
||||
The package SHALL export a `formatDateRange(start, end, locale, options?)` standalone helper that uses `Intl.DateTimeFormat.formatRange` for cross-day ranges and falls back to two-segment rendering when `formatRange` is unavailable on older runtimes.
|
||||
|
||||
#### Scenario: range across two days
|
||||
|
||||
- **WHEN** `formatDateRange(new Date(2026, 0, 1), new Date(2026, 0, 5), "en-GB")` is called
|
||||
- **THEN** the result contains both day numbers `"1"` and `"5"` (locale-correct rendering)
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"formatDateRange renders cross-day range"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `Intl.DateTimeFormat` `RangeError` maps to `UNSUPPORTED_LOCALE`
|
||||
|
||||
The package SHALL catch every `RangeError` thrown by the `Intl.DateTimeFormat` constructor and re-throw it as `I18nError(I18N.UNSUPPORTED_LOCALE)` with the original error attached as `cause` and the locale string in `context`.
|
||||
|
||||
#### Scenario: bogus locale throws UNSUPPORTED_LOCALE
|
||||
|
||||
- **WHEN** `formatDate(new Date(), "xx-bogus-tag-!!!")` is called
|
||||
- **THEN** the call throws `I18nError` with code `"I18N.UNSUPPORTED_LOCALE"` AND the error's `cause` is the original `RangeError`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-(locale, options-hash) cache
|
||||
|
||||
The package SHALL maintain an internal `Map<string, Intl.DateTimeFormat>` keyed by `${locale}|${JSON.stringify(options)}`. Repeated calls with identical `(locale, options)` SHALL reuse the same `Intl.DateTimeFormat` instance. The package SHALL export a `clearDateFormatterCache()` test-only helper.
|
||||
|
||||
#### Scenario: identical options reuse the same Intl.DateTimeFormat
|
||||
|
||||
- **GIVEN** the cache is empty
|
||||
- **WHEN** `formatDate(d, "en-GB", { preset: "short" })` is called twice in sequence
|
||||
- **THEN** only one `Intl.DateTimeFormat` constructor invocation occurs
|
||||
|
||||
#### Scenario: distinct preset creates distinct cache entries
|
||||
|
||||
- **WHEN** `formatDate(d, "en-GB", { preset: "short" })` and `formatDate(d, "en-GB", { preset: "long" })` are called
|
||||
- **THEN** two distinct `Intl.DateTimeFormat` instances are created and cached
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"identical options reuse cached Intl.DateTimeFormat"` AND `packages/i18n/i18n-formatter-date/src/formatter.test.ts::"distinct preset creates distinct cache entries"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel histogram `i18n.format.date.duration_ms`
|
||||
|
||||
The package SHALL emit an OTel histogram named exactly `i18n.format.date.duration_ms` (unit `ms`) on every successful `formatDate` or `formatDateRange` call, tagged with `locale` and `preset` (or `"custom"` when no preset is supplied).
|
||||
|
||||
#### Scenario: histogram emitted on every call
|
||||
|
||||
- **GIVEN** an OTel test exporter capturing histogram observations
|
||||
- **WHEN** `formatDate(d, "en-GB", { preset: "short" })` is called
|
||||
- **THEN** exactly one observation appears under `i18n.format.date.duration_ms` with `locale: "en-GB"` and `preset: "short"` attributes
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/observability.test.ts::"emits i18n.format.date.duration_ms histogram on every call"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel cache-hit counter `i18n.format.cache_hit`
|
||||
|
||||
The package SHALL emit an OTel counter `i18n.format.cache_hit` tagged with `formatter: "date"` on every cache hit. The counter SHALL NOT increment on the first (cache-miss) call.
|
||||
|
||||
#### Scenario: cache-hit counter increments only on second call
|
||||
|
||||
- **GIVEN** an OTel test exporter capturing counter observations and an empty cache
|
||||
- **WHEN** `formatDate(d, "en-GB", { preset: "short" })` is called twice in sequence
|
||||
- **THEN** the counter `i18n.format.cache_hit{formatter="date"}` shows exactly one increment
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/observability.test.ts::"cache-hit counter increments only on second call with same options"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `formatTime` standalone helper
|
||||
|
||||
The package SHALL export a `formatTime(date, locale, long?)` standalone helper that pre-binds `formatDate` with the `time-short` (or `time-long` when `long: true`) preset.
|
||||
|
||||
#### Scenario: short time rendering
|
||||
|
||||
- **WHEN** `formatTime(new Date(2026, 0, 1, 14, 30), "en-GB", false)` is called
|
||||
- **THEN** the result equals a short-style time (e.g. `"14:30"`) and does NOT include seconds
|
||||
|
||||
#### Scenario: long time rendering includes seconds
|
||||
|
||||
- **WHEN** `formatTime(new Date(2026, 0, 1, 14, 30, 45), "en-GB", true)` is called
|
||||
- **THEN** the result includes seconds (e.g. `"14:30:45"`)
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"formatTime short does not include seconds"` AND `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"formatTime long includes seconds"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `formatDateOnly` standalone helper
|
||||
|
||||
The package SHALL export a `formatDateOnly(date, locale, long?)` standalone helper that pre-binds `formatDate` with the `date-short` (or `date-long` when `long: true`) preset.
|
||||
|
||||
#### Scenario: short date-only rendering
|
||||
|
||||
- **WHEN** `formatDateOnly(new Date(2026, 0, 1), "en-GB", false)` is called
|
||||
- **THEN** the result is a numeric short-date (e.g. `"01/01/2026"`) without time
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"formatDateOnly short renders numeric date"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `getDayNames` and `getMonthNames` helpers
|
||||
|
||||
The package SHALL export `getDayNames(locale, style?)` returning 7 day names starting from the locale-defined first day of the week (consults `Intl.Locale.getWeekInfo()` when available; falls back to Monday-start) and `getMonthNames(locale, style?)` returning 12 month names. Both accept `style: "long" | "short" | "narrow"` (default `"long"`).
|
||||
|
||||
#### Scenario: getDayNames returns 7 strings
|
||||
|
||||
- **WHEN** `getDayNames("en-GB", "long")` is called
|
||||
- **THEN** the result has length `7` AND every entry is a non-empty string
|
||||
|
||||
#### Scenario: getMonthNames returns 12 strings
|
||||
|
||||
- **WHEN** `getMonthNames("en-GB", "long")` is called
|
||||
- **THEN** the result has length `12` AND every entry is a non-empty string
|
||||
|
||||
#### Scenario: getDayNames falls back to Monday-start when getWeekInfo() unavailable
|
||||
|
||||
- **GIVEN** `Intl.Locale.prototype.getWeekInfo` is mocked to throw
|
||||
- **WHEN** `getDayNames("en-GB", "long")` is called
|
||||
- **THEN** the first entry is `"Monday"` (locale-correct Monday rendering)
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"getDayNames returns 7 strings"` AND `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"getMonthNames returns 12 strings"` AND `packages/i18n/i18n-formatter-date/src/helpers.test.ts::"getDayNames falls back to Monday-start when getWeekInfo unavailable"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL declare subpath exports in `package.json`:
|
||||
|
||||
- `@suaveplan/i18n-formatter-date` — root barrel
|
||||
- `@suaveplan/i18n-formatter-date/formatter` — `DateFormatter` class + `formatDate` + `formatDateRange` + types + schema only
|
||||
- `@suaveplan/i18n-formatter-date/helpers` — `formatTime`, `formatDateOnly`, `getDayNames`, `getMonthNames` only
|
||||
|
||||
#### Scenario: every documented subpath resolves
|
||||
|
||||
- **WHEN** every documented subpath is imported in a consumer test fixture
|
||||
- **THEN** every import resolves with no `Module not found` errors
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/index.test.ts::"every documented subpath export resolves"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Side-effect-free package
|
||||
|
||||
The package SHALL declare `"sideEffects": false` in `package.json`.
|
||||
|
||||
#### Scenario: package.json declares sideEffects false
|
||||
|
||||
- **WHEN** `package.json` is parsed
|
||||
- **THEN** the `sideEffects` field equals `false`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-date/src/index.test.ts::"package.json declares sideEffects false"`
|
||||
59
openspec/archive/2026-05-09-i18n-formatter-date/tasks.md
Normal file
59
openspec/archive/2026-05-09-i18n-formatter-date/tasks.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Tasks — `@suaveplan/i18n-formatter-date`
|
||||
|
||||
> Reconciled retroactively after Wave 3 shipped under commit `1cb2f412`. Package is built, tested, and validated; this pass folds the spec delta into `openspec/specs/i18n-formatter-date/spec.md` (12 requirements) and archives the change folder. All `**Verified by:**` paths in the spec have been confirmed to exist in `packages/i18n/i18n-formatter-date/src/`.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Cross-cutting design + AGENTS sections read
|
||||
- [x] 1.2 Source-mapping table populated
|
||||
- [x] 1.3 `proposal.md` authored
|
||||
- [x] 1.4 `specs/i18n-formatter-date/spec.md` delta drafted (12 requirements)
|
||||
- [x] 1.5 Every requirement carries a `**Verified by:**` line
|
||||
- [x] 1.6 `openspec change validate i18n-formatter-date` clean
|
||||
|
||||
## 2. Package scaffold
|
||||
|
||||
- [x] 2.1 `packages/i18n/i18n-formatter-date/src/` structure
|
||||
- [x] 2.2 `package.json` — `@suaveplan/i18n-formatter-date@0.0.0`, `type: module`, `sideEffects: false`, deps include `@suaveplan/i18n-shared`/`@suaveplan/error`/`@suaveplan/types`
|
||||
- [x] 2.3 `tsconfig.lib.json` extends shared library config
|
||||
- [x] 2.4 `vite.config.ts` + `vitest.config.ts`
|
||||
- [x] 2.5 `README.md` with Modules table
|
||||
|
||||
## 3. Implementation
|
||||
|
||||
- [x] 3.1 Formatter implementation under `src/` wrapping `Intl` with cache, Zod options boundary, structured `I18nError` mapping (`INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW`)
|
||||
- [x] 3.2 OTel spans + counters + cache-hit histograms
|
||||
- [x] 3.3 Sibling `.md` per `.ts` implementation file
|
||||
- [x] 3.4 Root `src/index.ts` — explicit named re-exports (no `export *`)
|
||||
|
||||
## 4. Tests (runner-neutral imports)
|
||||
|
||||
- [x] 4.1 Unit tests covering every documented scenario in the spec
|
||||
- [x] 4.2 Property/edge-case tests
|
||||
- [x] 4.3 Cache-eviction + overflow tests
|
||||
- [x] 4.4 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [x] 4.5 Runner-neutral imports via `@suaveplan/testing/runner` — CLAUDE.md §11 grep clean (verified)
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 Biome clean
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-formatter-date` green at commit time
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-formatter-date` 100/100/100/100 at commit time
|
||||
- [x] 5.4 Conformance script clean
|
||||
- [x] 5.5 Build green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics threaded — formatter options typed via shared-bundle types
|
||||
- [x] 6.2 Zod schemas — options boundary validated
|
||||
- [x] 6.3 Error codes — `INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW` consumed from `i18n-shared`
|
||||
- [x] 6.4 Strict-mode tested
|
||||
- [x] 6.5 OTel — spans/counters/histograms
|
||||
- [x] 6.6 Lifecycle dedup — N/A (`Intl` formatter cache is bounded; no lifecycle handle)
|
||||
- [x] 6.7 No retroactive tick — boxes ticked during reconciliation pass after honest audit
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Spec delta folded into `openspec/specs/i18n-formatter-date/spec.md` (canonical, status ACTIVE)
|
||||
- [x] 7.2 Folder moved to `openspec/archive/2026-05-09-i18n-formatter-date/`
|
||||
- [x] 7.3 `MANIFEST.md` already lists the package
|
||||
Reference in New Issue
Block a user