# `rotation` _(no submodules)_ ## Purpose Resolves the rotation policy carried by every `createFileAuditSink` instance and implements the rotation primitive itself — the `rename(currentFile → currentFile..log)` step plus the post-rotation state reset. The module owns both dimensions documented in the spec: size-based (`maxSizeBytes`, default 100 MiB) and time-based (`maxAgeMs`, default 24h). Either condition reaching its limit triggers a rotation on the next write. Rotation lives in a separate module so the sink stays focused on queueing + writing. The retention module reads the policy through the same `RotationState` so the operator-facing knobs are documented in exactly one place. ## Features - `resolveRotationPolicy(input?)` — Zod-equivalent validator returning a frozen `RotationPolicy`. Defaults filled from the design doc. - `shouldRotate(state, nowMs)` — pure predicate; size OR age satisfies. - `rotate(fs, basePath, state, clock)` — performs the rename, updates state, returns the rotated path for retention to inspect. - Raises `AUDIT_FILE.ROTATION_FAILED` on rename failure with full `cause` for forensic logging. ## Usage: Basic ```ts import { resolveRotationPolicy, rotate, shouldRotate, } from "@suaveplan/api-audit-file/rotation"; const policy = resolveRotationPolicy({ maxSizeBytes: 1024 * 1024 }); const state = { openedAtMs: Date.now(), bytesWritten: 0, policy }; if (shouldRotate(state, Date.now())) { await rotate(fs, "/var/log/audit.log", state, () => Date.now()); } ``` ## Usage: Advanced Disable a dimension by setting it to `Number.POSITIVE_INFINITY` — useful when an operator wants pure time rotation regardless of size: ```ts const policy = resolveRotationPolicy({ maxSizeBytes: Number.POSITIVE_INFINITY, maxAgeMs: 60 * 60 * 1000, // hourly }); ``` In tests, drive rotation deterministically with a `FakeClock` from `@suaveplan/testing`: ```ts import { FakeClock } from "@suaveplan/testing"; const clock = new FakeClock(0); const state = { openedAtMs: 0, bytesWritten: 0, policy }; clock.advance(61_000); await rotate(fs, basePath, state, clock.read); ``` ## API Reference - `interface RotationPolicy` - `maxSizeBytes: number` — bytes before size rotation; default 100 MiB. - `maxAgeMs: number` — ms before time rotation; default 24h. - `maxFiles: number` — kept rotated files; default 30. - `interface RotationState` - `openedAtMs: number` — wall-clock open time of current segment. - `bytesWritten: number` — running byte count. - `policy: RotationPolicy` — frozen at construction. - `resolveRotationPolicy(input?): RotationPolicy` - `shouldRotate(state, nowMs): boolean` - `rotate(fs, basePath, state, clock): Promise` — returns the rotated file path. ## Implementation Notes The rotated suffix replaces `:` with `-` so the resulting filename is Windows-portable and grep-friendly across shells. State mutation happens AFTER the rename succeeds — a failed rename leaves the original state intact, so the next call retries against the same segment rather than corrupting accounting. `bytesWritten` resets to 0 instead of re-stat'ing the new empty file; the next successful append updates it in lock-step with the on-disk length.