73 lines
4.9 KiB
Markdown
73 lines
4.9 KiB
Markdown
# Tasks: primitives-cron
|
|
|
|
**Change ID**: primitives-cron
|
|
**Package**: `@suaveplan/cron`
|
|
**Target path**: `packages/primitives/cron`
|
|
**Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`
|
|
|
|
---
|
|
|
|
## 1. Scaffold
|
|
|
|
- [x] Create directory tree per `proposal.md`.
|
|
- [x] `package.json` at `0.1.0`, `"sideEffects": false`, exports root + `./parse` + `./scheduler` + `./testing` subpaths.
|
|
- [x] Deps: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`.
|
|
- [x] DevDeps: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `@suaveplan/testing workspace:^`.
|
|
- [x] `tsconfig.json` extending `library.json` (universal-tier — NO `library-node.json`).
|
|
- [x] `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`.
|
|
- [x] Scripts: `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`.
|
|
|
|
## 2. Types + errors
|
|
|
|
- [x] `src/types/cron.types.ts`: `CronFields`, `CronExpression`, `ParseOptions`, `CronSchedulerOptions`. Zod schemas at the boundary.
|
|
- [x] `src/types/cron.types.md` (≥200 words, both examples, full API reference).
|
|
- [x] `src/error-codes.ts`: 5 codes per proposal via `defineCodes("CRON", [...])`; eager `registerCronErrorCodes()` call.
|
|
|
|
## 3. Field + macro parsing
|
|
|
|
- [x] `src/parse/cron-fields/cron-fields.ts`: tokenize one field (range / list / step / `*` / numeric alias JAN-DEC + SUN-SAT). Validate per-field range. Throw `CRON.FIELD_OUT_OF_RANGE` / `CRON.UNSUPPORTED_SYNTAX`.
|
|
- [x] `src/parse/macros/macros.ts`: expand `@yearly`/`@annually`/`@monthly`/`@weekly`/`@daily`/`@midnight`/`@hourly` → canonical 5-field expression.
|
|
- [x] `src/parse/cron-expression/cron-expression.ts`: orchestrator. 5/6/7-field detection per `ParseOptions`. `parseCron(expr, options)` returns frozen `CronExpression` with bound `nextRun` / `nextRuns` / `previousRun` / `matches`. `isValidCron` non-throwing variant.
|
|
- [x] Tests for every parser file. Co-located `.md` for every impl file.
|
|
|
|
## 4. Next-run + timezone
|
|
|
|
- [x] `src/next-run/timezone/timezone.ts`: `wallClockComponents(epochMs, tz)` + inverse `epochMsForWallClock(components, tz)` via `Intl.DateTimeFormat` with `timeZone` + `hourCycle: 'h23'`. DST spring-forward = next valid minute. DST fall-back = first occurrence. `CRON.TIMEZONE_INVALID` on bad IANA name.
|
|
- [x] `src/next-run/next-run/next-run.ts`: bit-set field-match algorithm (croner-style). Bounded forward search horizon (default: 8 years per Quartz convention) → `CRON.NEXT_RUN_NOT_FOUND` on exhaustion (e.g. `0 0 31 2 *`). `previousRun` mirror.
|
|
- [x] Tests including known-impossible expressions, leap-day, DST crossings (US `America/New_York`, EU `Europe/London`, SH `Australia/Sydney`).
|
|
|
|
## 5. Optional in-process scheduler
|
|
|
|
- [x] `src/scheduler/cron-scheduler/cron-scheduler.ts`: thin `setTimeout`-based driver (NOT a full job runner). Injectable `Clock`. Handler errors routed to `options.onError` (default: swallow). `start` / `stop` idempotent. `stop()` awaits in-flight handlers.
|
|
- [x] Test with `FakeClock` from `@suaveplan/testing`.
|
|
|
|
## 6. OTel surface
|
|
|
|
- [x] `src/otel/cron-otel.ts`:
|
|
- Histograms: `cron.parse.duration_ms{outcome}`, `cron.next_run.duration_ms{outcome,tz}`.
|
|
- Counters: `cron.parse_total{outcome}`, `cron.next_run_total{outcome,tz}`, `cron.dst_transition_total{kind: 'spring-forward'|'fall-back', tz}`.
|
|
- [x] Co-located `.md`.
|
|
|
|
## 7. Integration + property + vectors
|
|
|
|
- [x] `tests/vectors.test.ts`: ≥30 known (expression, after, tz, expected-nextRun) tuples. Cross-check at least 10 against croner's documented behaviour for confidence (research-only — no runtime dep).
|
|
- [x] `tests/property.test.ts` (fast-check):
|
|
- `nextRun(parseCron(e), t) > t` for all parseable `e` and finite `t`.
|
|
- `nextRuns(parseCron(e), n, t)` is strictly monotonic increasing.
|
|
- `matches(parseCron(e), nextRun(parseCron(e), t))` is true.
|
|
- `previousRun(parseCron(e), nextRun(parseCron(e), t)) <= t` when both finite.
|
|
- [x] `tests/integration.test.ts`: `CronScheduler` over 3 jobs with different intervals under `FakeClock`; verify fire order + counts + stop drain; `{ timeout: 30000 }`.
|
|
|
|
## 8. Docs + verify + publish
|
|
|
|
- [x] Root `README.md`: parse quick-start, `nextRun(tz)` example, `CronScheduler` example, FakeClock test recipe, full API table, comparison-with-croner note (why we wrote our own).
|
|
- [x] `bunx turbo lint --filter=@suaveplan/cron` — zero errors.
|
|
- [x] `bunx turbo typecheck --filter=@suaveplan/cron` — zero errors.
|
|
- [x] `bunx turbo test --filter=@suaveplan/cron` — all green.
|
|
- [x] `bunx turbo test:coverage --filter=@suaveplan/cron` — 100%.
|
|
- [x] `bunx turbo docs:check --filter=@suaveplan/cron` — 100%.
|
|
- [ ] `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md`.
|
|
- [ ] Update `CLAUDE.md` package listing under "Time / sequencing / validation".
|
|
- [ ] `bun scripts/publish.ts --package @suaveplan/cron`.
|
|
- [ ] Archive to `openspec/archive/2026-05-11-primitives-cron/`.
|