feat: Doctrine persistence infrastructure (Plan 2)#3
Merged
Conversation
Adds the append-only Doctrine entity (implements AuditRecord) and the persist-only store, exercised by an in-memory SQLite round-trip test. Test infra: symfony/cache (dev) for ORMSetup metadata cache, native lazy objects enabled for ORM 3 on PHP 8.5, and a 512M phpstan memory limit since the ORM type graph exceeds the 128M default.
- Cover page>1 offset and assert the reader returns detached DTOs (not the EM-managed entity), killing the pagination and array_map mutants. - Drop the redundant max(0, ...) offset guard: AuditQuery already rejects page/perPage < 1, so the offset is never negative. - Decode JSON via the named flags: argument, removing the json_decode depth literal (an equivalent mutant on 512).
Drives actorId, subjectClass, subjectId, channel and a from+to window through AuditQuery — verifying each is bound to the right column (notably subjectId vs subject_class), that the enum channel filter matches the enumType column, and that from/to are inclusive (>=/<=).
A 64-char cap risks truncating long or composite/external actor identifiers; there is no storage cost to the wider varchar on Postgres.
Changeset values are frozen as JSON, so a non-scalar old/new would silently reshape on read (objects) or throw mid-flush inside the audited transaction (resources/closures). Enforce scalar-or-null at FieldChange::of() so curation discipline fails fast at construction, not at persist time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements Plan 2 — Doctrine persistence infrastructure (
docs/superpowers/plans/2026-05-30-audit-log-doctrine-infrastructure.md).Adds the Doctrine ORM adapters under
Infrastructure/Doctrine/, implementing the core'sRecordStore/RecordReaderports and theAuditRecordread contract. They depend on Core + Doctrine only — deptrac-enforced as independent of any (future) Symfony adapter.What's included
ChangesetType/RenderPayloadType— round-tripChangesetandRenderPayloadas JSON (redaction preserved).AuditRecordEntity— append-only entity (private ctor, singlefromNew()factory, no setters,public private(set)props),Types::GUIDid, indexes on code/occurredAt/actor/subject.DoctrineRecordStore— persist-only (no flush; commits atomically with the surrounding unit of work).DoctrineRecordReader— filter (code/actor/subject/channel/date), newest-first pagination, distinct actors.AuditEntry/AuditActor/AuditQuery/AuditPage) their behavioral coverage.Deviations from the plan text (all necessary, none change scope)
composer.locknot committed — it's no longer tracked onmain(CI usescomposer update).convertToPHPValuenarrowing —(string) $valuefails PHPStan max (cast.string). Replaced with anis_string()guard that rejects a corrupt non-string column value (legitimate integrity behavior).symfony/cache(dev) for ORMSetup's metadata cache,enableNativeLazyObjects(true)for ORM 3 on PHP 8.5, and a 512M phpstan memory limit (the ORM type graph exceeds the 128M default).max(0, …)offset guard (AuditQueryalready rejects page/perPage < 1), added a page-2 pagination test, and assert the reader returns detached DTOs (not the EM-managed entity). One genuinely equivalent mutant remains inChangesetType(public-prop objects serialize byte-identically to the mapped arrays); MSI is 98.77% (≥ 95%).QA (all green locally)
cs-check·phpstan·rector-check·deptrac(0 violations) ·test(23 tests) ·infection(MSI 98.77%)Note (out of scope, flagging)
ADR
0006-framework-free-core.mdstill says framework wiring lives inBridge/{Symfony,Doctrine}; the architecture is nowInfrastructure/{Doctrine,Symfony}. Worth a follow-up doc fix.