# At most one date library

- **Pattern:** `ab-000229` (`ai-slop-code-drift.tooling-stack-drift.dual-date-library`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000229
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000229)

## Why it matters

Two date libraries mean two timezone-handling strategies, two parsing APIs, and two bundle payloads. date-fns treats dates as immutable UTC-first while dayjs mirrors Moment's mutable-looking API — code that mixes both produces DST-transition bugs that only fire twice a year. Formatting output drifts between components (one says "Jan 5" the other says "1/5"), and locale handling requires loading the right locale pack into the right library or date strings silently fall back to English.

## Severity rationale

Medium because DST and timezone bugs are real correctness issues, but they're bounded to date-display and scheduling features.

## Remediation

Wrap your chosen library in `src/utils/dates.ts` and import only that wrapper from the rest of the codebase.

```ts
// src/utils/dates.ts
import { format, parseISO } from 'date-fns'
export const formatDate = (d: Date) => format(d, 'yyyy-MM-dd')
export const parseDate = (s: string) => parseISO(s)
```

Rewrite every dayjs call site, then `npm uninstall dayjs`. Add an ESLint rule forbidding direct imports of the removed library anywhere outside the wrapper module to prevent regression.

## Detection

- **ID:** `dual-date-library`
- **Severity:** `medium`
- **What to look for:** Apply the three-condition rule against this exact date library allowlist: `date-fns`, `dayjs`, `luxon`, `moment`, `@js-joda/core`, `temporal-polyfill`, `js-time-ago`, `time-fns`. Note: native `Date` and the upcoming `Temporal` API are NOT packages — do not count them. Count all REMAINING libraries that meet all three conditions (at least 1 non-escape-hatch importing file).
- **Pass criteria:** 0 or 1 date libraries from the allowlist actively used. Report even on pass: "Canonical date library: [name or 'native Date'] ([N] importing files)."
- **Fail criteria:** 2 or more date libraries from the allowlist meet all three conditions (at least 1 non-escape-hatch importing file).
- **Skip (N/A) when:** 0 date libraries from the allowlist appear in `RUNTIME_DEPS`.
- **Detail on fail:** `"2 active date libraries: 'date-fns' (14 files) AND 'dayjs' (6 files). Pick one for date handling."`
- **Remediation:** Two date libraries means two timezone behaviors, two formatting APIs, two parsing strategies — and bugs that only show up at DST transitions. Pick one and migrate:

  ```ts
  // Bad: src/utils/format-date.ts uses date-fns, src/utils/parse-date.ts uses dayjs
  // Good: a single src/utils/dates.ts that wraps your chosen library

  // src/utils/dates.ts
  import { format, parseISO } from 'date-fns'
  export const formatDate = (d: Date) => format(d, 'yyyy-MM-dd')
  ```

Taxons: dependency-coherence

HTML version: https://auditbuffet.com/patterns/ab-000229
