Initial Commit
This commit is contained in:
90
openspec/archive/2026-05-18-timing-completion/proposal.md
Normal file
90
openspec/archive/2026-05-18-timing-completion/proposal.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Timing completion — rateLimit / once / times helpers
|
||||
|
||||
**Package**: `@suaveplan/timing`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature
|
||||
|
||||
## Why
|
||||
|
||||
The 2026-04-29 `timing-spec-conformance` change deliberately archived the more elaborate timing constructs (`createHighPrecisionStopwatch`, `createHighConcurrencyScheduler`, `VectorClockManager`) on architectural grounds — those landed in higher-tier packages or were superseded by composable primitives. However, three smaller helpers from the legacy `throttle/throttle.ts` module never got the same treatment: `rateLimit`, `once`, and `times`. They are not architecturally suspect, they are not duplicated elsewhere on genesis, and they fall well within the scope CLAUDE.md describes for `@suaveplan/timing`. They were simply missed.
|
||||
|
||||
The 2026-05-02 audit found no replacement for any of the three:
|
||||
|
||||
- `rateLimit(fn, maxCalls, windowMs)` — different from `throttle` (which spaces calls evenly across an interval). `rateLimit` enforces *N calls per sliding window* and lets callers burst inside the window. This is the contract REST clients, webhook deliverers, and feed pollers need; throttle is the wrong tool. Genesis has no equivalent.
|
||||
- `once(fn)` — wraps a function so it executes exactly once and caches the result. Used by initialization paths where a check-and-set guard is too verbose (e.g. lazy DB connection pool warm-up). Genesis has no equivalent. The closest pattern is wrapping in `Promise.resolve(...)` and stashing the promise, which loses the synchronous return signature for synchronous functions.
|
||||
- `times(fn, maxCalls)` — wraps a function so it runs for the first `n` calls and becomes a no-op afterward. Useful for "log this warning the first 5 times then stop" patterns; the alternative — manual counters threaded through every call site — is clutter genesis already has too much of.
|
||||
|
||||
All three are 15-30 line implementations with a small surface and clear semantics. Adding them to the existing `throttle/` feature module (alongside `throttle` and `debounce`) is the lowest-friction path; renaming the module to `rate-control/` would have the same outcome but disturbs the existing tree-shake boundary. Keep `throttle/` and add the three siblings.
|
||||
|
||||
## What changes
|
||||
|
||||
### `@suaveplan/timing`
|
||||
|
||||
- ADDED: `rateLimit<TArgs, TReturn>(fn, options): (...args: TArgs) => TReturn | RateLimitedSentinel` — bounded calls per window. `options.maxCalls: number`, `options.windowMs: number`, `options.clock?: Clock` (injectable; defaults to system clock). When the window cap is exceeded, the wrapped function returns `RATE_LIMITED` (the exported sentinel) instead of invoking `fn`. The sliding window is implemented as a FIFO of timestamps; entries older than `windowMs` are evicted on every call.
|
||||
- ADDED: `RATE_LIMITED` constant — the sentinel symbol returned by `rateLimit` when the cap is reached. Exported alongside `rateLimit`.
|
||||
- ADDED: `RateLimitedSentinel` type alias — `typeof RATE_LIMITED`.
|
||||
- ADDED: `once<TArgs, TReturn>(fn): (...args: TArgs) => TReturn` — wrapped function that runs `fn` exactly once and caches the result. Subsequent calls return the cached value with the same `TReturn` type (no `| undefined` widening). Throws of `fn` are *not* cached — a thrown call counts as not-yet-run, allowing retry.
|
||||
- ADDED: `times<TArgs, TReturn>(fn, maxCalls): (...args: TArgs) => TReturn | undefined` — wrapped function that runs `fn` for the first `maxCalls` invocations and returns `undefined` thereafter.
|
||||
- ADDED: subpath export `./throttle` already exists; the three helpers live in `src/throttle/rate-limit/rate-limit.ts`, `src/throttle/once/once.ts`, `src/throttle/times/times.ts`. Each gets its own subpath: `./throttle/rate-limit`, `./throttle/once`, `./throttle/times`.
|
||||
- ADDED: OTel counter `timing.rate_limit.rejected` (no labels) incremented every time `rateLimit` returns the sentinel. Counter `timing.once.invocation` and `timing.times.invocation` similarly emitted for observability parity with `throttle` / `debounce`.
|
||||
- ADDED: error code `TIMING.RATE_LIMIT_INVALID_WINDOW` — thrown by the `rateLimit` factory when `maxCalls < 1` or `windowMs <= 0`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **`createHighPrecisionStopwatch`**, **`createHighConcurrencyScheduler`**, **`VectorClockManager`** — these were intentionally archived in `2026-04-29-timing-spec-conformance` and stay archived. This change does not revisit that decision.
|
||||
- **Async `rateLimit` with queue-and-retry semantics.** The legacy implementation drops over-cap calls; genesis matches that behaviour. A queueing variant (await-until-slot-available) is a separate proposal — it has different cancellation, abort-signal, and back-pressure semantics and warrants its own design.
|
||||
- **Distributed rate limiting.** The wrapped function is process-local. Cluster-wide rate limiting goes through `@suaveplan/idempotency` or an external token bucket; this change does not address it.
|
||||
- **Memoization with eviction.** `once` is intentionally simple — no LRU, no TTL, no key-based memoization. Callers that need keyed memoization compose `once` per key OR reach for `@suaveplan/idempotency`.
|
||||
- **Re-introducing `Date.now()` at module scope.** All three helpers accept an injectable `Clock` from `@suaveplan/types`; the default is a `() => Date.now()` clock injected at construction time, not module scope. This complies with CLAUDE.md determinism rules.
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `rateLimit(func, maxCalls, windowMs, _options)` | ✅ | ⬆️ | Enhanced: signature changed to `(fn, options)` (object) for ergonomic parity with `throttle` / `debounce`. Injectable `Clock`. Returns `RATE_LIMITED` sentinel instead of `undefined` for clarity. |
|
||||
| `once(func)` | ✅ | ⬆️ | Enhanced: exception-aware — if `fn` throws, the wrapper re-throws and stays "not yet run" so retries are possible (legacy cached the throw via the side-effecting `called = true; result = func(...)` order, which silently re-returned `undefined` on retry). |
|
||||
| `times(func, maxCalls)` | ✅ | ✅ | Verbatim semantics. |
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/packages/universal/timing/src/throttle/throttle.ts`.
|
||||
|
||||
| Requirement | Source file | Source lines | Notes |
|
||||
|---|---|---|---|
|
||||
| `rateLimit(fn, maxCalls, windowMs)` semantics | `packages/universal/timing/src/throttle/throttle.ts` | L278-L309 | Verbatim semantics; signature reshaped to `(fn, options)` and return value changed to a sentinel. |
|
||||
| `once(fn)` semantics | `packages/universal/timing/src/throttle/throttle.ts` | L314-L328 | Enhanced: throw-and-retry behaviour added (legacy cached the call attempt regardless). |
|
||||
| `times(fn, maxCalls)` semantics | `packages/universal/timing/src/throttle/throttle.ts` | L333-L345 | Verbatim. |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `RATE_LIMITED` sentinel symbol | Genesis-wide preference for explicit sentinels over `undefined` returns — `undefined` ambiguously overlaps with functions whose `TReturn` is `void`. The legacy signature returned `TReturn \| undefined` and could not distinguish "rate-limited" from "function returned undefined". |
|
||||
| Object-shaped `options` parameter for `rateLimit` | Genesis-wide ergonomic rule — every primitive helper in `@suaveplan/timing` (`throttle`, `debounce`) takes `(fn, options)`. `rateLimit` matching that shape is the lowest-friction call site. |
|
||||
| Injectable `Clock` for `rateLimit` window math | Genesis-wide determinism rule — every time-dependent helper accepts a `Clock` from `@suaveplan/types`. |
|
||||
| OTel counters `timing.rate_limit.rejected`, `timing.once.invocation`, `timing.times.invocation` | Genesis-wide observability rule. |
|
||||
| Error code `TIMING.RATE_LIMIT_INVALID_WINDOW` | Genesis-wide policy that every thrown error route through `@suaveplan/error` with a stable code. Legacy threw nothing — it accepted `0` / negative inputs silently and returned a wrapper that always rate-limited. |
|
||||
| Throw-and-retry semantics for `once` | Legacy `called = true` runs before `result = func(...)`, so a throw locks the wrapper out forever. Genesis fixes this — throwing leaves `called` at `false` so a subsequent call retries. Documented in `once.md`. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/throttle/rate-limit/rate-limit.test.ts` — bursts inside window, eviction across window boundary, sentinel returned at cap, injectable clock advances drive eviction.
|
||||
- `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts` — `RATE_LIMITED` is a unique symbol; OTel counter increments on rejection; invalid `maxCalls` / `windowMs` throws `TIMING.RATE_LIMIT_INVALID_WINDOW`.
|
||||
- `src/throttle/once/once.test.ts` — runs once, caches return, throw-and-retry flow.
|
||||
- `src/throttle/once/once.spec-conformance.test.ts` — counter increments on first invocation only; type-level `TReturn` (not `TReturn | undefined`) on the wrapper.
|
||||
- `src/throttle/times/times.test.ts` — runs `n` times, returns `undefined` thereafter, counter behaviour.
|
||||
- `src/throttle/times/times.spec-conformance.test.ts` — counter increments per invocation up to `maxCalls`.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`@suaveplan/timing` currently has zero downstream packages on genesis (verified by `bunx turbo ls --filter=...@suaveplan/timing` returning only the package itself).
|
||||
|
||||
- No dependents (verified by `grep -rln "@suaveplan/timing" /home/ubuntu/web/genesis/packages/ --include=package.json` returning only `packages/timing/timing/package.json`).
|
||||
|
||||
When new consumers land they import `@suaveplan/timing/throttle/rate-limit`, etc. The change is purely additive — no existing public surface is removed or renamed.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. `throttle` and `debounce` already exist at the same subpath structure; the three new helpers slot in alongside them.
|
||||
@@ -0,0 +1,97 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: timing (timing-completion)
|
||||
|
||||
**Package**: `@suaveplan/timing`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Sliding-window rate limit
|
||||
|
||||
The package SHALL expose `rateLimit<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, options: RateLimitOptions): (...args: TArgs) => TReturn | RateLimitedSentinel` where `RateLimitOptions = { maxCalls: number; windowMs: number; clock?: Clock }`. The wrapped function SHALL maintain a FIFO of timestamps; on every call it SHALL evict entries older than `now - windowMs` (where `now` comes from `options.clock` if supplied, otherwise from the system clock injected at construction). When the FIFO size is below `maxCalls`, the wrapped function SHALL push the current timestamp and invoke `fn`. When the size equals `maxCalls`, the wrapper SHALL return the exported sentinel `RATE_LIMITED` instead of invoking `fn`.
|
||||
|
||||
The factory SHALL throw `TimingError` with code `TIMING.RATE_LIMIT_INVALID_WINDOW` when `maxCalls < 1` or `windowMs <= 0`.
|
||||
|
||||
#### Scenario: bursts inside the window are allowed up to the cap
|
||||
|
||||
- **WHEN** a caller wraps a counter-incrementing `fn` with `rateLimit(fn, { maxCalls: 3, windowMs: 1000 })` and invokes the wrapper 3 times within 100ms (under a fake clock)
|
||||
- **THEN** `fn` runs 3 times AND the counter equals 3
|
||||
|
||||
#### Scenario: cap exceeded returns the sentinel
|
||||
|
||||
- **WHEN** the wrapper is invoked a fourth time inside the same window
|
||||
- **THEN** the call returns `RATE_LIMITED` AND `fn` is not invoked
|
||||
|
||||
#### Scenario: window slides
|
||||
|
||||
- **WHEN** the fake clock advances past `windowMs` AND the wrapper is invoked again
|
||||
- **THEN** the oldest FIFO entry is evicted AND `fn` runs (counter = 4)
|
||||
|
||||
#### Scenario: invalid options throw
|
||||
|
||||
- **WHEN** a caller invokes `rateLimit(fn, { maxCalls: 0, windowMs: 1000 })`
|
||||
- **THEN** the factory throws `TimingError` with code `TIMING.RATE_LIMIT_INVALID_WINDOW`
|
||||
|
||||
**Verified by:** `src/throttle/rate-limit/rate-limit.test.ts::"allows bursts up to maxCalls inside a single window"` AND `src/throttle/rate-limit/rate-limit.test.ts::"returns RATE_LIMITED when cap exceeded"` AND `src/throttle/rate-limit/rate-limit.test.ts::"slides the window when clock advances past windowMs"` AND `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts::"throws TIMING.RATE_LIMIT_INVALID_WINDOW for non-positive windowMs"` AND `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts::"increments timing.rate_limit.rejected on cap"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: RATE_LIMITED sentinel symbol
|
||||
|
||||
The package SHALL export a unique symbol `RATE_LIMITED` and a type alias `RateLimitedSentinel = typeof RATE_LIMITED`. Callers MUST be able to disambiguate "rate-limited" from "function returned undefined" via reference equality on the sentinel.
|
||||
|
||||
#### Scenario: sentinel is type-distinct from undefined
|
||||
|
||||
- **WHEN** a caller writes `if (result === RATE_LIMITED)` against a wrapper of `fn: () => void`
|
||||
- **THEN** TypeScript narrows the discriminant correctly AND a wrapper of `fn: () => void` whose call slot is full returns `RATE_LIMITED`, not `undefined`
|
||||
|
||||
**Verified by:** `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts::"RATE_LIMITED narrows the wrapper return type"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: once-only invocation with throw-and-retry
|
||||
|
||||
The package SHALL expose `once<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn`. The wrapper SHALL invoke `fn` on the first successful call and cache its return value. Subsequent calls SHALL return the cached value WITHOUT re-invoking `fn`. Critically, when `fn` throws, the wrapper SHALL re-throw the exception AND remain in the not-yet-run state so the next call retries.
|
||||
|
||||
#### Scenario: first call runs and caches
|
||||
|
||||
- **WHEN** a caller wraps `fn = () => Math.random()` with `once(fn)` and invokes the wrapper twice
|
||||
- **THEN** both invocations return the same value (the cached first result)
|
||||
|
||||
#### Scenario: throw does not lock the wrapper
|
||||
|
||||
- **WHEN** the first invocation of `fn` throws AND the wrapper is invoked a second time with `fn` now succeeding
|
||||
- **THEN** the second invocation runs `fn` AND returns the new value (not `undefined`)
|
||||
|
||||
**Verified by:** `src/throttle/once/once.test.ts::"runs fn once and caches the result"` AND `src/throttle/once/once.test.ts::"re-runs fn after a throw"` AND `src/throttle/once/once.spec-conformance.test.ts::"timing.once.invocation counter increments only on first successful run"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: bounded-times invocation
|
||||
|
||||
The package SHALL expose `times<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, maxCalls: number): (...args: TArgs) => TReturn | undefined`. The wrapper SHALL invoke `fn` for the first `maxCalls` calls and return `undefined` for every subsequent call without invoking `fn`.
|
||||
|
||||
#### Scenario: first n calls run, n+1th returns undefined
|
||||
|
||||
- **WHEN** a caller wraps `fn = (n: number) => n * 2` with `times(fn, 3)` and invokes the wrapper 5 times with values `1, 2, 3, 4, 5`
|
||||
- **THEN** the returned values are `[2, 4, 6, undefined, undefined]`
|
||||
|
||||
**Verified by:** `src/throttle/times/times.test.ts::"runs fn for the first maxCalls invocations and returns undefined thereafter"` AND `src/throttle/times/times.spec-conformance.test.ts::"timing.times.invocation counter increments per invocation up to maxCalls"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports
|
||||
|
||||
The package SHALL expose three new subpath exports in `package.json::exports`: `./throttle/rate-limit`, `./throttle/once`, `./throttle/times`. Each SHALL resolve to its co-located `dist/throttle/<feature>/index.js` plus `.d.ts`. Importing only `once` SHALL NOT pull in `rateLimit` or `times`.
|
||||
|
||||
#### Scenario: subpath imports resolve
|
||||
|
||||
- **WHEN** a caller writes `import { rateLimit, RATE_LIMITED } from "@suaveplan/timing/throttle/rate-limit"` AND `import { once } from "@suaveplan/timing/throttle/once"` AND `import { times } from "@suaveplan/timing/throttle/times"`
|
||||
- **THEN** TypeScript resolves all three imports
|
||||
|
||||
**Verified by:** `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts::"subpath ./throttle/rate-limit resolves and tree-shakes"` AND `src/throttle/once/once.spec-conformance.test.ts::"subpath ./throttle/once resolves and tree-shakes"` AND `src/throttle/times/times.spec-conformance.test.ts::"subpath ./throttle/times resolves and tree-shakes"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
73
openspec/archive/2026-05-18-timing-completion/tasks.md
Normal file
73
openspec/archive/2026-05-18-timing-completion/tasks.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Tasks — Timing completion (rateLimit / once / times)
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened.
|
||||
|
||||
## Audit Note (2026-05-02)
|
||||
|
||||
Status: **DEFERRED**. None of `rateLimit`, `once`, `times`, or the `RATE_LIMITED` sentinel exist in `packages/timing/timing/src/throttle/` (currently only contains `throttle.ts`, `throttle.types.ts`, `throttle.schemas.ts`, plus `debounce.test.ts`). 0% of work is done.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [ ] 1.1 Read `../suaveplan/packages/universal/timing/src/throttle/throttle.ts` (rateLimit / once / times sections, L275-L346)
|
||||
- [ ] 1.2 Read `openspec/specs/timing/spec.md` and the archived `2026-04-29-timing-spec-conformance` proposal
|
||||
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
|
||||
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
|
||||
- [ ] 1.6 Draft `specs/timing/spec.md` from `openspec/templates/spec-delta.md`
|
||||
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 Register error code `TIMING.RATE_LIMIT_INVALID_WINDOW` in `src/errors/`
|
||||
- [ ] 2.2 Create `src/throttle/rate-limit/` feature module with `rate-limit.ts`, `rate-limit.types.ts`, `rate-limit.schemas.ts`, `rate-limit.md`, `index.ts`
|
||||
- [ ] 2.3 Implement `rateLimit(fn, options)` with sliding-window FIFO and injectable `Clock`; export `RATE_LIMITED` sentinel and `RateLimitedSentinel` type alias
|
||||
- [ ] 2.4 Create `src/throttle/once/` feature module with `once.ts`, `once.md`, `index.ts`
|
||||
- [ ] 2.5 Implement `once(fn)` with throw-and-retry semantics
|
||||
- [ ] 2.6 Create `src/throttle/times/` feature module with `times.ts`, `times.md`, `index.ts`
|
||||
- [ ] 2.7 Implement `times(fn, maxCalls)`
|
||||
- [ ] 2.8 Wire OTel counters `timing.rate_limit.rejected`, `timing.once.invocation`, `timing.times.invocation` via `src/otel/`
|
||||
- [ ] 2.9 Add subpath exports `./throttle/rate-limit`, `./throttle/once`, `./throttle/times` to `package.json::exports`
|
||||
- [ ] 2.10 Re-export the three helpers + `RATE_LIMITED` from the root barrel
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 Unit tests at `src/throttle/rate-limit/rate-limit.test.ts` — bursts inside window, eviction across window boundary, sentinel returned at cap, injectable clock drives eviction
|
||||
- [ ] 3.2 Spec-conformance tests at `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts` — `RATE_LIMITED` unique symbol pinned via `expectTypeOf`, counter increments on rejection (real `MeterProvider`), invalid options throw `TIMING.RATE_LIMIT_INVALID_WINDOW`
|
||||
- [ ] 3.3 Unit tests at `src/throttle/once/once.test.ts` — runs once, caches return, throw-and-retry flow
|
||||
- [ ] 3.4 Spec-conformance tests at `src/throttle/once/once.spec-conformance.test.ts` — counter increments on first invocation only, `TReturn` (not `TReturn | undefined`) on wrapper
|
||||
- [ ] 3.5 Unit tests at `src/throttle/times/times.test.ts` — runs `n` times, returns `undefined` thereafter
|
||||
- [ ] 3.6 Spec-conformance tests at `src/throttle/times/times.spec-conformance.test.ts` — counter increments per invocation up to `maxCalls`
|
||||
- [ ] 3.7 Property-based tests via `fast-check` for `rateLimit` window invariant: at any instant, the count of timestamps in the FIFO is at most `maxCalls`
|
||||
- [ ] 3.8 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 `src/throttle/rate-limit/rate-limit.md` — sibling doc (≥200 words, basic + advanced, full API including `RATE_LIMITED` sentinel)
|
||||
- [ ] 4.2 `src/throttle/once/once.md` — sibling doc (≥200 words, document throw-and-retry semantics explicitly, distinguish from legacy)
|
||||
- [ ] 4.3 `src/throttle/times/times.md` — sibling doc (≥200 words)
|
||||
- [ ] 4.4 README links all three `.md` files under `## Modules`
|
||||
- [ ] 4.5 Update `CLAUDE.md` `@suaveplan/timing` entry to include `rateLimit`, `once`, `times`
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/timing` — green
|
||||
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/timing` — 100/100/100/100
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/timing` — exit 0, no `error` findings
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/timing` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md Section 13)
|
||||
|
||||
- [ ] 6.1 Generics threaded through every wrapper; `expectTypeOf` confirms `RATE_LIMITED` is a unique symbol
|
||||
- [ ] 6.2 Zod schemas wired at `rateLimit` boundary (validates `maxCalls >= 1`, `windowMs > 0`)
|
||||
- [ ] 6.3 `TIMING.RATE_LIMIT_INVALID_WINDOW` thrown by at least one production path
|
||||
- [ ] 6.4 Strict-mode behaviors tested (e.g. `once` throw-and-retry vs throw-and-cache contrast)
|
||||
- [ ] 6.5 OTel counters emitted from production paths and asserted with a real in-memory `MeterProvider`
|
||||
- [ ] 6.6 Lifecycle dedup — `once` itself is the dedup primitive; tested directly
|
||||
- [ ] 6.7 No retroactive tick — every box above reflects work done in this change
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold spec delta into `openspec/specs/timing/spec.md`
|
||||
- [ ] 7.2 Move `openspec/changes/timing-completion/` → `openspec/archive/<yyyy-mm-dd>-timing-completion/`
|
||||
- [ ] 7.3 Refresh `MANIFEST.md` if dep set changed (no expected change)
|
||||
Reference in New Issue
Block a user