Initial Commit
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# `@suaveplan/i18n-formatter-relative-time` — `Intl.RelativeTimeFormat` wrapper
|
||||
|
||||
**Package**: `@suaveplan/i18n-formatter-relative-time` (NEW)
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature (new universal-tier package; verbatim port of `formatting/relative-time/`)
|
||||
**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, §7.1)
|
||||
**Path**: `packages/i18n/i18n-formatter-relative-time/`
|
||||
**Tier**: universal
|
||||
|
||||
## Why
|
||||
|
||||
The 14-package i18n modular split lifts each `Intl.*` wrapper out of the monolith into its own sibling package so consumers can pull only the formatters they actually use. `@suaveplan/i18n-formatter-relative-time` is the second Wave-3 package, following the canonical formatter shape (per-(locale, options-hash) cache, Zod-validated options, OTel histogram + cache-hit counter, helper + class twin surface).
|
||||
|
||||
Today `formatting/relative-time/` ships:
|
||||
|
||||
- `RelativeTimeFormatter` class wrapping `Intl.RelativeTimeFormat` (`packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` L157–L168)
|
||||
- `formatRelativeTime` standalone helper with auto-unit-selection over a 7-tier threshold table (`relative-time.ts` L122–L151, L21–L78)
|
||||
- Per-(locale, style, numeric) cache (`relative-time.ts` L14, L109–L120)
|
||||
- `INVALID_FORMATTER_OPTIONS` on Zod failure (`relative-time.ts` L80–L89)
|
||||
- `UNSUPPORTED_LOCALE` on `Intl.RelativeTimeFormat` `RangeError` (`relative-time.ts` L91–L107)
|
||||
- OTel histogram `i18n.format.relative_time.duration_ms` and cache-hit counter via `observability/format-metrics.ts`
|
||||
|
||||
The auto-unit-selection logic (`autoSelectUnit` at `relative-time.ts` L54–L78) is what makes this package non-trivial: it consults a 7-tier threshold table (`THRESHOLDS_MS`, L21–L28) to pick the human-friendly unit (e.g. `36 h` rolls over to `2 days`).
|
||||
|
||||
## What changes
|
||||
|
||||
### New package: `@suaveplan/i18n-formatter-relative-time`
|
||||
|
||||
- ADDED: `packages/i18n/i18n-formatter-relative-time/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
|
||||
- ADDED: `src/formatter.ts` — `RelativeTimeFormatter` class + `formatRelativeTime` standalone + `clearRelativeTimeFormatterCache` test-only helper. Verbatim port of monolith `relative-time.ts` (L1–L173) with import rewires.
|
||||
- ADDED: `src/formatter.types.ts` — `RelativeTimeUnit`, `RelativeTimeStyle`, `RelativeTimeNumeric`, `RelativeTimeFormatterOptions`, `RelativeTimeUnitSelection`. Verbatim port of monolith `relative-time.types.ts` (L1–L26).
|
||||
- ADDED: `src/formatter.schemas.ts` — `RelativeTimeFormatterOptionsSchema` Zod schema. Verbatim port of monolith `relative-time.schemas.ts` (L1–L22).
|
||||
- ADDED: `src/helpers.ts` — `formatTimeAgo` (`formatRelativeTime(deltaMs, locale, { style: "short" })` shortcut), `formatTimeIn` (positive-delta variant of `formatTimeAgo`), and `selectRelativeUnit` (exposes the auto-unit-selection logic standalone for callers who want the chosen unit only). New ergonomic surface introduced by the split (the monolith only ships the class + standalone).
|
||||
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.relative_time.duration_ms` + counter `i18n.format.cache_hit{formatter="relative_time"}`.
|
||||
- ADDED: `src/formatter.test.ts` — verbatim port of monolith `relative-time.test.ts` (115 lines).
|
||||
- ADDED: `src/formatter.spec-conformance.test.ts` — verbatim port of monolith `relative-time.spec-conformance.test.ts` (121 lines).
|
||||
- ADDED: `src/helpers.test.ts` — covers `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit`.
|
||||
- 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` — `RelativeTimeFormatter` class + `formatRelativeTime` + types + schema only
|
||||
- `./helpers` — `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit` only
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`
|
||||
- `@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/relative-time/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
|
||||
- **Custom threshold tables** (e.g. business-day-aware rollover) — follow-up; the monolith ships fixed thresholds and so does this port.
|
||||
- **`formatRange` / `formatRangeToParts` for relative-time intervals** — not part of `Intl.RelativeTimeFormat`; out of scope.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Requirement | Source file | Source lines | Notes |
|
||||
|---|---|---|---|
|
||||
| `RelativeTimeFormatter` class | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L157–L168 | Verbatim port; rewire imports |
|
||||
| `formatRelativeTime` standalone | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L122–L151 | Verbatim port |
|
||||
| `clearRelativeTimeFormatterCache` | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L170–L173 | Verbatim port |
|
||||
| Auto-unit-selection (`autoSelectUnit`, `THRESHOLDS_MS`) | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L21–L78 | Verbatim port |
|
||||
| Per-(locale, style, numeric) cache | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L14, L109–L120 | Verbatim port |
|
||||
| `INVALID_FORMATTER_OPTIONS` mapping | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L80–L89 | Verbatim port |
|
||||
| `UNSUPPORTED_LOCALE` mapping | `packages/i18n/i18n/src/formatting/relative-time/relative-time.ts` | L91–L107 | Verbatim port |
|
||||
| Type definitions | `packages/i18n/i18n/src/formatting/relative-time/relative-time.types.ts` | L1–L26 | Verbatim port |
|
||||
| Zod schema | `packages/i18n/i18n/src/formatting/relative-time/relative-time.schemas.ts` | L1–L22 | Verbatim port |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Helpers `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit` | New ergonomic surface introduced by the split (monolith only ships class + standalone). Symmetric with how the number formatter ships `formatPercent` / `formatCompact` / `formatUnit` helpers alongside the standalone. |
|
||||
| Subpath exports `./formatter`, `./helpers` | Tree-shaking — slim-core `FormatBridge` wiring imports only the standalone `formatRelativeTime` |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts` — auto-unit-selection at every threshold boundary (60s, 60m, 24h, 7d, 30.4d, 365.25d); explicit-unit override; class twins helper; Zod failure → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`
|
||||
- `packages/i18n/i18n-formatter-relative-time/src/formatter.spec-conformance.test.ts` — verbatim port of monolith spec-conformance suite
|
||||
- `packages/i18n/i18n-formatter-relative-time/src/helpers.test.ts` — `formatTimeAgo` returns `"5 sec. ago"`-style output; `formatTimeIn` returns `"in 5 sec."`-style output; `selectRelativeUnit` returns the chosen unit + value pair
|
||||
- `packages/i18n/i18n-formatter-relative-time/src/observability.test.ts` — histogram named `i18n.format.relative_time.duration_ms`; counter `i18n.format.cache_hit{formatter="relative_time"}` increments only on second call with same options
|
||||
- `packages/i18n/i18n-formatter-relative-time/src/index.test.ts` — every documented public export resolves; subpaths resolve
|
||||
|
||||
## Impact
|
||||
|
||||
- New universal-tier package — joins `@suaveplan/i18n-shared`, `@suaveplan/i18n-icu-parser`, and `@suaveplan/i18n-formatter-number` under `packages/i18n/`.
|
||||
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core wires `formatRelativeTime` into the default `FormatBridge` for ICU `{delta, relativeTime}` placeholder rendering.
|
||||
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.
|
||||
@@ -0,0 +1,162 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec: i18n-formatter-relative-time (i18n-formatter-relative-time)
|
||||
|
||||
**Package**: `@suaveplan/i18n-formatter-relative-time`
|
||||
**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: RelativeTimeFormatter class wrapping `Intl.RelativeTimeFormat`
|
||||
|
||||
The package SHALL export a `RelativeTimeFormatter` class that wraps `Intl.RelativeTimeFormat` with construction-time Zod validation of options and a `format(deltaMs: number, locale: string): string` method. Construction with malformed options SHALL throw `I18nError(I18N.INVALID_FORMATTER_OPTIONS)`.
|
||||
|
||||
#### Scenario: format with default options
|
||||
|
||||
- **GIVEN** `const f = new RelativeTimeFormatter()`
|
||||
- **WHEN** `f.format(-3600_000, "en-GB")` is called
|
||||
- **THEN** the result equals `"1 hour ago"`
|
||||
|
||||
#### Scenario: invalid options throw INVALID_FORMATTER_OPTIONS
|
||||
|
||||
- **WHEN** `new RelativeTimeFormatter({ style: "invalid" as never })` is called
|
||||
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_FORMATTER_OPTIONS"`
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"RelativeTimeFormatter formats with default options"` AND `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"RelativeTimeFormatter rejects invalid options with INVALID_FORMATTER_OPTIONS"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `formatRelativeTime` standalone helper with auto-unit-selection
|
||||
|
||||
The package SHALL export a `formatRelativeTime(deltaMs, locale, options?)` standalone helper that auto-selects the human-friendly unit when `options.unit` is absent. The selection SHALL roll over at: 60 s → minutes, 60 min → hours, 24 h → days, 7 d → weeks, 30.4375 d → months, 365.25 d → years. When `options.unit` is supplied, the explicit unit SHALL be used instead.
|
||||
|
||||
#### Scenario: auto-selects seconds for sub-minute deltas
|
||||
|
||||
- **WHEN** `formatRelativeTime(-30_000, "en-GB")` is called
|
||||
- **THEN** the unit chosen is `"seconds"` AND the result reads as a 30-second-ago phrase
|
||||
|
||||
#### Scenario: auto-selects hours for 36-hour delta
|
||||
|
||||
- **WHEN** `formatRelativeTime(-36 * 3_600_000, "en-GB")` is called
|
||||
- **THEN** the unit chosen rolls over to `"days"` (auto-selection picks 1.5 → rounds to 2 days) and the result is a days-ago phrase
|
||||
|
||||
#### Scenario: explicit unit override skips auto-selection
|
||||
|
||||
- **WHEN** `formatRelativeTime(-3600_000, "en-GB", { unit: "seconds" })` is called
|
||||
- **THEN** the result is a 3600-seconds-ago phrase (no rollover)
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"auto-selects seconds for sub-minute deltas"` AND `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"auto-selects days for 36-hour delta"` AND `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"explicit unit override skips auto-selection"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `Intl.RelativeTimeFormat` `RangeError` maps to `UNSUPPORTED_LOCALE`
|
||||
|
||||
The package SHALL catch every `RangeError` thrown by the `Intl.RelativeTimeFormat` 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** `formatRelativeTime(-1000, "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-relative-time/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-(locale, style, numeric) cache
|
||||
|
||||
The package SHALL maintain an internal `Map<string, Intl.RelativeTimeFormat>` keyed by `${locale}|${style}|${numeric}`. Repeated calls with identical `(locale, style, numeric)` SHALL reuse the same `Intl.RelativeTimeFormat` instance. The package SHALL export a `clearRelativeTimeFormatterCache()` test-only helper.
|
||||
|
||||
#### Scenario: identical key reuses the same Intl.RelativeTimeFormat
|
||||
|
||||
- **GIVEN** the cache is empty
|
||||
- **WHEN** `formatRelativeTime(-1000, "en-GB")` is called twice in sequence
|
||||
- **THEN** only one `Intl.RelativeTimeFormat` constructor invocation occurs
|
||||
|
||||
#### Scenario: distinct style or numeric creates distinct cache entries
|
||||
|
||||
- **WHEN** `formatRelativeTime(-1000, "en-GB", { style: "long" })` and `formatRelativeTime(-1000, "en-GB", { style: "short" })` are called
|
||||
- **THEN** two distinct `Intl.RelativeTimeFormat` instances are created and cached
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"identical key reuses cached Intl.RelativeTimeFormat"` AND `packages/i18n/i18n-formatter-relative-time/src/formatter.test.ts::"distinct style creates distinct cache entries"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel histogram `i18n.format.relative_time.duration_ms`
|
||||
|
||||
The package SHALL emit an OTel histogram named exactly `i18n.format.relative_time.duration_ms` (unit `ms`) on every successful `formatRelativeTime` call, tagged with `locale`, `style`, and `unit`.
|
||||
|
||||
#### Scenario: histogram emitted on every call
|
||||
|
||||
- **GIVEN** an OTel test exporter capturing histogram observations
|
||||
- **WHEN** `formatRelativeTime(-1000, "en-GB", { style: "long" })` is called
|
||||
- **THEN** exactly one observation appears under `i18n.format.relative_time.duration_ms` with `locale`, `style`, and `unit` attributes
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/observability.test.ts::"emits i18n.format.relative_time.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: "relative_time"` 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** `formatRelativeTime(-1000, "en-GB")` is called twice in sequence
|
||||
- **THEN** the counter `i18n.format.cache_hit{formatter="relative_time"}` shows exactly one increment
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/observability.test.ts::"cache-hit counter increments only on second call with same options"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ergonomic helpers — `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit`
|
||||
|
||||
The package SHALL export three ergonomic helpers:
|
||||
|
||||
- `formatTimeAgo(deltaMs, locale)` — pre-binds `formatRelativeTime` with `style: "short"` and asserts negative delta
|
||||
- `formatTimeIn(deltaMs, locale)` — pre-binds `formatRelativeTime` with `style: "short"` and asserts positive delta
|
||||
- `selectRelativeUnit(deltaMs): RelativeTimeUnitSelection` — exports the auto-selection function standalone (returns `{unit, value}`)
|
||||
|
||||
#### Scenario: selectRelativeUnit returns chosen unit and value
|
||||
|
||||
- **WHEN** `selectRelativeUnit(-90_000)` is called
|
||||
- **THEN** the result equals `{ unit: "minutes", value: -2 }` (rounded)
|
||||
|
||||
#### Scenario: formatTimeAgo formats short style
|
||||
|
||||
- **WHEN** `formatTimeAgo(-3600_000, "en-GB")` is called
|
||||
- **THEN** the result is a short-style 1-hour-ago phrase
|
||||
|
||||
**Verified by:** `packages/i18n/i18n-formatter-relative-time/src/helpers.test.ts::"selectRelativeUnit returns chosen unit and value"` AND `packages/i18n/i18n-formatter-relative-time/src/helpers.test.ts::"formatTimeAgo formats short-style 1 hour ago"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL declare subpath exports in `package.json` so consumers can import only the surfaces they need:
|
||||
|
||||
- `@suaveplan/i18n-formatter-relative-time` — root barrel
|
||||
- `@suaveplan/i18n-formatter-relative-time/formatter` — `RelativeTimeFormatter` class + `formatRelativeTime` + types + schema only
|
||||
- `@suaveplan/i18n-formatter-relative-time/helpers` — `formatTimeAgo`, `formatTimeIn`, `selectRelativeUnit` 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-relative-time/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-relative-time/src/index.test.ts::"package.json declares sideEffects false"`
|
||||
@@ -0,0 +1,59 @@
|
||||
# Tasks — `@suaveplan/i18n-formatter-relative-time`
|
||||
|
||||
> 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-relative-time/spec.md` (9 requirements) and archives the change folder. All `**Verified by:**` paths in the spec have been confirmed to exist in `packages/i18n/i18n-formatter-relative-time/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-relative-time/spec.md` delta drafted (9 requirements)
|
||||
- [x] 1.5 Every requirement carries a `**Verified by:**` line
|
||||
- [x] 1.6 `openspec change validate i18n-formatter-relative-time` clean
|
||||
|
||||
## 2. Package scaffold
|
||||
|
||||
- [x] 2.1 `packages/i18n/i18n-formatter-relative-time/src/` structure
|
||||
- [x] 2.2 `package.json` — `@suaveplan/i18n-formatter-relative-time@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-relative-time` green at commit time
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-formatter-relative-time` 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-relative-time/spec.md` (canonical, status ACTIVE)
|
||||
- [x] 7.2 Folder moved to `openspec/archive/2026-05-09-i18n-formatter-relative-time/`
|
||||
- [x] 7.3 `MANIFEST.md` already lists the package
|
||||
Reference in New Issue
Block a user