v0.7.0 — developer experience + swarm:trace audit-chain CLI
Developer experience, test coverage, and one new operator forensics surface. One new opt-in public-surface contract (ReadableSwarmAuditSink) and one new Artisan command (swarm:trace). No migration changes. No breaking changes.
Added
swarm:trace <run_id>audit-chain reconstruction CLI +ReadableSwarmAuditSinkcontract (#44). New read-only Artisan command that walks a single run's audit chain end-to-end by merging three sources into a chronological timeline: sink-side records via the new opt-inBuiltByBerry\LaravelSwarm\Contracts\ReadableSwarmAuditSinkextension contract (forRun(string $runId): iterable<array>), pending and dead-letter rows fromswarm_audit_outboxwith attempt counts andlast_error, and the lifecycle entry from the boundRunHistoryStore. Default output is a human-readable timeline table;--jsonmirrors theswarm:audit:status/swarm:audit:reconcileshape so the same monitoring scrapers can consume it;--include-payloadsattaches the full evidence envelope per record (off by default — payloads can be large);--limit=N(default 1000) bounds sink-side reads so a long-running run cannot exhaust memory in the command's in-memory sort (outbox and history rows are bounded by the run itself and not subject to the limit). The contract is intentionally opt-in: the shippedNoOpSwarmAuditSinkdoes not implement it and existing custom sinks remain valid. When the bound sink isNoOpSwarmAuditSinkor does not implementReadableSwarmAuditSink, the command degrades to outbox + history only and surfaces a cleardegraded: trueflag and per-source note explaining the limitation. Same graceful degradation when the audit outbox is unavailable (cache persistence driver, missing table). The command is read-only and never mutates audit state. The command unseals encrypted-at-rest data on output (last_erroralways; full payload under--include-payloads) — in regulated environments do not redirect to durable storage; seedocs/audit-evidence-contract.md"Security and retention". Registered inSwarmServiceProviderand added todocs/public-surface.mdunder Artisan Commands and Audit Extension Points; full read contract documented indocs/audit-evidence-contract.mdunder a new "Reading the Audit Chain" section.SwarmFakeintercepts for the v0.4 audit-extension contracts (#42). Three new static helpers —SwarmFake::interceptCapturePolicy(),interceptSinkFailureHandler(), andinterceptSwarmAuditSigner()— swap the container binding for the corresponding contract to a recording decorator and return a recorder with first-class assertion methods (assertCaptured,assertCapturedDecision,assertCapturedWith,assertSinkFailureRouted,assertSinkFailureRoutedAs,assertSigned, plusNever*variants). Each decorator wraps an optional delegate so existing policy / handler / signer logic still drives behavior; the recorder only captures inputs, the routed decision, and the resulting payload. Replaces the v0.4.3 workaround-pattern documentation;docs/testing.md"Testing Audit Extension Points" gains a new leading section covering the intercepts and how they preserveSwarmFake's "doesn't touch the dispatcher" design property — recording happens when the real dispatcher resolves the contract from the container during a non-faked run, soSwarmFakeitself never constructs or invokes the dispatcher.RunContext::fake()test helper for ad-hoc test setup (#43). New named constructor on the publicRunContextvalue object that returns a context with sensible test defaults (deterministic run id"fake-run-id", empty input, no actor). Override any slot via the$overridesarray (run_id,input,data,metadata,artifacts,actor— the latter delegates to the existingwithActor()builder and acceptsActor | Authenticatable | "type:id" | "id" string | null). Composes cleanly with the existing fluent builders (->withActor(),->withLabels(),->mergeData(), etc.) so rich test setups stay one fluent chain instead of three. Documented indocs/testing.mdwith a worked example bridgingRunContext::fake()intoSwarmFakeassertions. Pure additive — zero changes to existing public methods.- End-to-end audit-chain test with mid-flight signer rotation (#41). New
tests/Feature/AuditChainEndToEndTest.phpexercises the full chain (enqueue → drain attempt → transient failure → re-attempt → eventual success or dead-letter) through the realSwarmAuditDispatcherandDatabaseAuditOutboxrather than unit-testing the components in isolation. Four parameterized scenarios cover happy-path replay and full chain-to-dead-letter, each in bothencrypt_at_rest=trueand=falsemodes; the K1 signature is asserted to persist across rotation to K2,attemptsprogression is verified,last_errorsealing-at-rest is verified viaSwarmPersistenceCipher::open()for storage-shape neutrality, and the dead-letter transition'sLog::erroris asserted to carry the K1 signature in the final state. Locks down the chain-integrity properties (signature stability, attempt-count progression, retention enforcement) as one cohesive regression story. - Process-concurrency coverage for audit outbox SKIP LOCKED (#40). New
tests/ProcessConcurrency/AuditOutboxConcurrencyTest.phpproves that two parallelDatabaseAuditOutbox::drain()calls each claim a disjoint subset of pending rows and that a single stale reservation is reclaimed by exactly one worker — guarantees the existing SQLite-bound regression tests cannot prove. Taggedskip-locked-real-dbso it skips cleanly on the testbench in-memory SQLite (which honors neither cross-process state norFOR UPDATE SKIP LOCKED) and is exercised against a real MySQL/Postgres connection via the newcomposer test:process-concurrency:real-dblane and a new.github/workflows/tests-real-db.ymlmatrix job. The default CI lane (test:process-concurrency:ci) excludes the group under--fail-on-skippedso SQLite-only CI keeps passing while the real-DB lane enforces the lock contract end-to-end. - Regression coverage for the audit evidence envelope
schema_versionbump rule (#76). Newtests/Unit/Audit/EvidenceSchemaVersionTest.phpasserts that every audit category emitted insrc/(37 categories, excluding telemetry-onlystream.event/broadcast.event) carriesschema_version === EvidenceEnvelope::SCHEMA_VERSIONafter dispatch throughSwarmAuditDispatcher. Additional canaries assert the constant is"2"(guarding against an inadvertent further bump without coordinated CHANGELOG / UPGRADING / docs updates), that the deprecatedSwarmAuditDispatcher::SCHEMA_VERSIONmirror tracks the envelope constant, and that a caller-suppliedschema_versioncannot override the envelope's enriched value. A full audit pass acrosssrc/,tests/,examples/, anddatabase/found zero stale"1"emitters — the codebase was already clean; this commit is pure regression coverage to keep it that way.
Changed
CONTRIBUTING.mdrefresh for v0.5+ patterns (#47). New "Audit pipeline contributions" section covers the bind-a-contract-in-the-container extension pattern across all six audit contracts (ActorResolver,CapturePolicy,SwarmAuditSigner,SinkFailureHandler,SwarmAuditSink,AuditOutbox), the additive-vs-schema_version-bump rule for envelope changes (with the v0.4→v0.5command.*actor-unification as the reference example), and the dispatcher routing contract including theMAX_HANDLER_ITERATIONS = 5runaway guard and cache-driver degrade behavior. New "Stability Surface" section documents the@internalPHPDoc convention, when to ask before extending public surface, and the Pulse component pattern (Recorders are public surface; Livewire/Support are@internal). New "Test Tier Expectations" section codifies theUnit/Feature/ProcessConcurrencylanes — including a "Writingtests/ProcessConcurrency/*worker closures" subsection that documents the two non-obvious traps the package's own audit-outbox concurrency test hit during development: closure scope class (Pest auto-generatedP\Tests\…classes do not exist in child PHP processes spawned by the process driver — define worker closures in free functions, not inline;staticalone is not enough) and child container bootstrap (testbench's child Laravel boots without the package being tested — worker closures that need swarm container bindings must callapp()->register(SwarmServiceProvider::class)and set the minimum config the binding chain requires).docs/public-surface.mdrefresh for v0.5- and v0.6-era audit surface (#75). Listed surface that landed with the v0.5 audit outbox and v0.6 operator commands but had not made it into the public-surface table: all fiveSinkFailureDecisioncases (QueueandDeadLetteradded in v0.5 alongside the audit outbox), theAuditOutboxcontract, theAuditDrainResultresponse value object, theswarm:audit:statusandswarm:audit:reconcilev0.6 operator commands, the--audit/--durablefocus flags onswarm:health, and the--type=auditlane onswarm:relay. v0.7-era additions (swarm:trace,ReadableSwarmAuditSink) were added in #44 itself.
Full entry in the CHANGELOG.