mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
feat+test+docs: D52 — daily audit rotation (lib/audit.mjs + bin/olp-audit-rotate.mjs) (#29)
Fifth Phase 3 D-day. Adds daily UTC-aware rotation to lib/audit.mjs
per ADR 0008 § 5 + ships an external cron tool. Rotation is
SYNCHRONOUS at v0.3.0 — synchronous design eliminates the race that
an async wrapper would create between date-change-detection and the
append.
lib/audit.mjs EXTENSIONS:
- New _maybeRotateAudit({ olpHome, logEvent }) (sync): probes live
audit.ndjson; if it holds events from past UTC date, renames it
to audit-YYYY-MM-DD.ndjson. Idempotent. If target file exists
(cron beat in-server check), logs warn + skips per § 5.3.
- appendAuditEvent extended: cheap fast-path date check via module-
cached _lastSeenUtcDate. On date change, calls _maybeRotateAudit
synchronously BEFORE appendFileSync — so old-date events land in
the rotated file and new-date events land in the fresh live file.
No event straddles the boundary.
- Why SYNCHRONOUS: an async wrapper would let the sync
appendFileSync race the not-yet-completed renameSync, landing
today's event in the about-to-be-renamed file. Sync rotation is
the only correct ordering at the append-fired-from-many-routes
scale OLP runs. (Test 26b-1 caught this during local run; the
initial async-wrapper implementation failed because the live
file at assertion time didn't exist.)
- New exports: _maybeRotateAudit (sync), getAuditRotateCount,
getAuditRotateFailCount, __resetAuditRotateState,
__setLastSeenUtcDateForTesting.
- First-event-date discovery: when probing the live file's date,
reads only the first ndjson line + parses its ts. Falls back to
file mtime if events absent (corrupt/empty edge).
bin/olp-audit-rotate.mjs (~95 lines): external cron tool per § 5.2.
Calls _maybeRotateAudit once + reports outcome. Exit codes 0
(success or no-op), 1 (bad usage), 2 (rotation failed). Installed
via package.json bin so `npx olp-audit-rotate [--olp-home=<path>]`
works. Example cron line in file header.
CONCURRENT-SAFETY (§ 5.3):
In-process sequential appends after the first date-change detection
short-circuit via the updated _lastSeenUtcDate cache → exactly 1
rename even under N sequential appends. Cross-process (cron + server)
coexistence handled by the "target already exists → skip + warn"
branch.
TESTS — Suite 26, +12 (588 → 600):
26a-1..5: _maybeRotateAudit (no live file / today already /
yesterday→rotate / idempotent re-call / cron-race target-exists
warn)
26b-1: appendAuditEvent past UTC date change triggers sync rotation
+ append lands in fresh live file
26c-1: 10 sequential appendAuditEvent across date change → exactly
1 rotation + all 10 events in new live file
26d-1..4: bin/olp-audit-rotate.mjs CLI (--help / no-live-file /
yesterday-file-rotates / unknown-flag exit 1)
26e-1: rotated files queryable via lib/audit-query.mjs
discoverAuditFiles + readAuditWindow cross-file read
package.json: bin.olp-audit-rotate + scripts.olp-audit-rotate entries
added.
DOCUMENTATION:
- AGENTS.md: lib/audit.mjs marker promoted ✅ (D45 append + D52
rotation both shipped); new bin/olp-audit-rotate.mjs entry.
- CHANGELOG.md: D52 entry under Unreleased per release_kit overlay.
NOT IN D52:
- tried_providers schema fix (D53; D45 P2 deferral)
- E2E + docs polish (D54)
- Phase 3 close → v0.3.0 (D55; maintainer-triggered)
Test count: 588 → 600 (+12). Verified locally via npm test.
AUTHORITY:
- ADR 0008 § 5.1 (first-append-after-UTC-midnight trigger).
- ADR 0008 § 5.2 (external cron alternative).
- ADR 0008 § 5.3 (concurrent-rotation safety + cron-coexistence).
- ADR 0008 § 5.4 (renamed-file query path consumed by D49 lib/
audit-query.mjs).
- CLAUDE.md release_kit overlay phase_rolling_mode — under
Unreleased.
- Standing autopilot grant.
ALIGNMENT.md scope check: extends lib/audit.mjs (a Phase 2 internal
module) + adds new bin/ CLI + small package.json bin/scripts entries.
No provider plugin / entry surface / IR change.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* bin/olp-audit-rotate.mjs — External audit rotation cron tool (Phase 3 / D52)
|
||||
*
|
||||
* Authority: ADR 0008 § 5.2 (external cron alternative to in-server first-
|
||||
* append-after-UTC-midnight trigger).
|
||||
*
|
||||
* Use case: operators who want exact-at-UTC-midnight rotation rather than
|
||||
* "first request after midnight". Invoke from a host cron / launchd job.
|
||||
*
|
||||
* Idempotent + safe to run alongside the in-server check (both detect the
|
||||
* date condition; whichever fires first does the rename; the other no-ops).
|
||||
*
|
||||
* Usage:
|
||||
* olp-audit-rotate [--olp-home=<path>]
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 = success (rotation performed OR no rotation needed)
|
||||
* 1 = bad usage (unknown flag)
|
||||
* 2 = rotation attempted + failed (e.g., EACCES)
|
||||
*
|
||||
* Example cron line (UTC midnight):
|
||||
* 1 0 * * * /usr/local/bin/node /path/to/bin/olp-audit-rotate.mjs >> /var/log/olp-audit-rotate.log 2>&1
|
||||
*/
|
||||
|
||||
import { _maybeRotateAudit } from '../lib/audit.mjs';
|
||||
|
||||
function parseArgv(argv) {
|
||||
const flags = {};
|
||||
for (const arg of argv) {
|
||||
if (arg.startsWith('--')) {
|
||||
const eq = arg.indexOf('=');
|
||||
if (eq > 0) flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
||||
else flags[arg.slice(2)] = true;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
const USAGE = `OLP audit rotation cron tool
|
||||
|
||||
Usage:
|
||||
olp-audit-rotate [--olp-home=<path>]
|
||||
|
||||
Triggers a rotation check: if the live audit.ndjson holds events from a
|
||||
past UTC date, rename it to audit-YYYY-MM-DD.ndjson. Idempotent; safe to
|
||||
run alongside the in-server first-append-after-UTC-midnight trigger.
|
||||
|
||||
Authority: ADR 0008 § 5.2.`;
|
||||
|
||||
export async function runCli(argv, opts = {}) {
|
||||
const ioOut = opts.out ?? (s => process.stdout.write(s));
|
||||
const ioErr = opts.err ?? (s => process.stderr.write(s));
|
||||
|
||||
if (argv.includes('--help') || argv.includes('-h')) {
|
||||
ioOut(USAGE + '\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
const flags = parseArgv(argv);
|
||||
const allowed = new Set(['olp-home', 'help', 'h']);
|
||||
for (const k of Object.keys(flags)) {
|
||||
if (!allowed.has(k)) {
|
||||
ioErr(`Error: unknown flag --${k}\n${USAGE}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const olpHome = typeof flags['olp-home'] === 'string' ? flags['olp-home'] : undefined;
|
||||
|
||||
try {
|
||||
// _maybeRotateAudit is synchronous at v0.3.0 (D52) — rotation must
|
||||
// complete BEFORE the next append so no event straddles the boundary.
|
||||
const result = _maybeRotateAudit({ olpHome });
|
||||
if (result.rotated) {
|
||||
ioOut(`Rotated ${result.fromPath} -> ${result.toPath} (dateUsed=${result.dateUsed}).\n`);
|
||||
} else {
|
||||
ioOut('No rotation needed (live audit is current or absent).\n');
|
||||
}
|
||||
return 0;
|
||||
} catch (err) {
|
||||
ioErr(`Error: rotation failed: ${err?.message ?? err}\n`);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Main guard
|
||||
const isMain = (() => {
|
||||
try { return import.meta.url === `file://${process.argv[1]}`; }
|
||||
catch { return false; }
|
||||
})();
|
||||
if (isMain) {
|
||||
runCli(process.argv.slice(2)).then(code => process.exit(code));
|
||||
}
|
||||
Reference in New Issue
Block a user