The docs/superpowers/{specs,plans}/ directory was archived to .archive/
in an earlier session (and .archive/ is gitignored). Every md link
into that path is now a broken reference for anyone consuming the
template fresh.
Stripped:
- ADR-011: **Spec:** header line
- ADR-015: **Spec:** + **Plan:** header lines
- ADR-016: **Spec:** + **Plan:** header lines + footer "Spec —"
bullet (the design rationale is captured in the ADR body itself)
- ADR-017: **Spec:** + **Plan:** header lines
- ADR-018: **Spec:** + **Plan:** header lines
- guides/realtime.md: inline "the full spec" link + footer
[Spec] entry (folded its description into the ADR-016 entry)
- guides/events-and-jobs.md: inline "the full spec" link
- architecture/vertical-feature-spec.md: stale "Deleted" subsection
referencing docs/superpowers/plans/*
Updated:
- glossary.md "PRD" entry: clarified status flow now matches the
shipped pnpm work prd-ship lifecycle (draft -> in-review ->
approved -> shipped); removed the parenthetical pointing at
docs/superpowers/specs/ as a definition of "spec"
- glossary.md "spec" flagged-ambiguity: rewritten to reflect that
durable design lives in ADRs (docs/decisions/adr-NNN-*.md) and
implementation seeds live in PRDs (docs/work/prds/*.prd.md) —
"spec" should be avoided in this template
Preserved (legitimate refs to the SuperPowers plugin, not the dir):
- agent-first-workflow-and-conformance.md mentions of
`superpowers:brainstorming` — these reference the external
plugin skill, not a file in the repo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.2 KiB
4.2 KiB
ADR-018 — Audit Logging & DPA Compliance
Status: Accepted Date: 2026-05-11 Companion guide: docs/guides/audit-and-compliance.md
Context
DPA compliance mandates audit logging for every personal-data access event: VIEW/CREATE/UPDATE/DELETE/EXPORT/PERMISSION_CHANGE, with immutable storage, GDPR-deletable path, centralized aggregation, and strict "what NOT to log" boundaries. The interface decisions from ADR-014 (R31-R51) carry over but audit needs its own channel — observability data is sampled and short-retention, audit data is lossless and long-retention with privileged erasure.
Decision (12 points)
AuditLogProtocolincore-shared— must-have universal surface. Features callctx.auditLog?.record(entry)without importing the optional package.AuditEntrytype with closed action enum — VIEW/CREATE/UPDATE/DELETE/ EXPORT/PERMISSION_CHANGE; new actions require explicit type bump. No payload/body/oldValue/newValue fields — type enforces "what NOT to log".@repo/core-auditas 5th optional package — joins realtime, events, trpc, ui. Scaffolded viapnpm turbo gen core-package audit.- Four impls + Recording test double: NoopAuditLog, PayloadAuditLog (local cache), StdoutJsonAuditLog (operator ships via Vector/Fluent Bit), MultiSinkAuditLog (fan-out), RecordingAuditLog (core-testing).
- Append-only Payload collection —
update: () => falseaccess rule is the compliance backbone; erasure path usesoverrideAccess: true. - GDPR erasure — sha256-salted pseudonymization (
erased-{hash[0:16]}) or hard delete. AUDIT_PSEUDONYM_SALT env REQUIRED in production; bind-time validation fails fast. - Erasure trigger surface — admin tRPC procedure (
audit.eraseSubject), PayloadafterDeletehook factory (createAuditErasureHook), auth integration via printed generator next-steps (NOT auto-installed). - OTel correlation bridge —
currentTraceId()helper in core-shared;TraceIdEnrichingAuditLogdecorator at bind time auto-populatesAuditEntry.correlationIdfrom active OTel span. Explicit caller wins. - VIEW capture via BOTH patterns — use-case
record()calls (developer decides per-read-path) ANDcreateAuditAfterReadHookfactory (opt-in per-collection automatic capture). Fire-and-forget for hooks. - IP/UA explicit at call sites — no AsyncLocalStorage. Callers use
truncateIp(raw)(/24 IPv4, /48 IPv6) and pass intorecord({ from: { ... } }). Sentinels for non-HTTP context:"system"/"background-job". - Multi-tenancy: tenant field required —
AuditEntry.scope.tenantnon-optional; single-tenant projects pass"default". Forces multi-tenant thinking from day one. - Six-phase delivery matching established cadence.
Alternatives considered
- Vendor-coupled SDK (Datadog/Grafana direct) — rejected; couples to vendor.
- Payload-only sink — fails compliance (hostile-actor immutability).
- Aggregator-only sink — fails dev ergonomics. Fan-out is the balance.
- AsyncLocalStorage for request context — rejected per user preference; explicit > implicit.
- Optional tenant field — rejected; DPA-aligned scope discipline benefits from forcing the question on every call.
Consequences
Positive:
- DPA-compliant baseline ships with the optional package.
- Vendor-neutral via stdout JSON + log shipper; any aggregator works.
- OTel correlation gives compliance auditors one-click pivot to traces.
- Type-enforced exclusion of "what not to log" prevents categories of mistakes.
Negative:
- Boilerplate at every record() call site (IP/UA explicit).
- core-audit ↔ auth coupling for the user-collection hook is awkward (manual install via generator next-steps).
- StdoutJsonAuditLog's eraseSubject is best-effort (tombstone only; past stdout lines can't be retroactively removed).
Relationship to other ADRs
- ADR-014 (instrumentation interfaces): audit is a parallel channel, not a signal flowing through OTel. The correlationId field is the bridge.
- ADR-015 (events/jobs): no overlap; audit is observational, events are reactive.
- ADR-017 (OTel migration): provides currentTraceId() helper.