Files
2026-05-27 23:29:53 +01:00

4.9 KiB

Tasks: primitives-cron

Change ID: primitives-cron Package: @suaveplan/cron Target path: packages/primitives/cron Prerequisites on Verdaccio: @suaveplan/error, @suaveplan/types


1. Scaffold

  • Create directory tree per proposal.md.
  • package.json at 0.1.0, "sideEffects": false, exports root + ./parse + ./scheduler + ./testing subpaths.
  • Deps: @suaveplan/error workspace:^, @suaveplan/types workspace:^, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build.
  • DevDeps: vitest catalog:testing, @vitest/coverage-istanbul catalog:testing, fast-check catalog:testing, @suaveplan/testing workspace:^.
  • tsconfig.json extending library.json (universal-tier — NO library-node.json).
  • vitest.config.ts using createTestConfig() from @suaveplan/testing/config.
  • Scripts: lint, typecheck, test, test:coverage, build, docs:check.

2. Types + errors

  • src/types/cron.types.ts: CronFields, CronExpression, ParseOptions, CronSchedulerOptions. Zod schemas at the boundary.
  • src/types/cron.types.md (≥200 words, both examples, full API reference).
  • src/error-codes.ts: 5 codes per proposal via defineCodes("CRON", [...]); eager registerCronErrorCodes() call.

3. Field + macro parsing

  • 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.
  • src/parse/macros/macros.ts: expand @yearly/@annually/@monthly/@weekly/@daily/@midnight/@hourly → canonical 5-field expression.
  • 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.
  • Tests for every parser file. Co-located .md for every impl file.

4. Next-run + timezone

  • 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.
  • 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.
  • Tests including known-impossible expressions, leap-day, DST crossings (US America/New_York, EU Europe/London, SH Australia/Sydney).

5. Optional in-process scheduler

  • 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.
  • Test with FakeClock from @suaveplan/testing.

6. OTel surface

  • 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}.
  • Co-located .md.

7. Integration + property + vectors

  • 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).
  • 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.
  • 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

  • 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).
  • bunx turbo lint --filter=@suaveplan/cron — zero errors.
  • bunx turbo typecheck --filter=@suaveplan/cron — zero errors.
  • bunx turbo test --filter=@suaveplan/cron — all green.
  • bunx turbo test:coverage --filter=@suaveplan/cron — 100%.
  • 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/.