Skip to content

v0.7.0 — developer experience + swarm:trace audit-chain CLI

Choose a tag to compare

@dberry37388 dberry37388 released this 04 Jun 17:00
· 274 commits to main since this release
10e9b44

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 + ReadableSwarmAuditSink contract (#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-in BuiltByBerry\LaravelSwarm\Contracts\ReadableSwarmAuditSink extension contract (forRun(string $runId): iterable<array>), pending and dead-letter rows from swarm_audit_outbox with attempt counts and last_error, and the lifecycle entry from the bound RunHistoryStore. Default output is a human-readable timeline table; --json mirrors the swarm:audit:status / swarm:audit:reconcile shape so the same monitoring scrapers can consume it; --include-payloads attaches 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 shipped NoOpSwarmAuditSink does not implement it and existing custom sinks remain valid. When the bound sink is NoOpSwarmAuditSink or does not implement ReadableSwarmAuditSink, the command degrades to outbox + history only and surfaces a clear degraded: true flag 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_error always; full payload under --include-payloads) — in regulated environments do not redirect to durable storage; see docs/audit-evidence-contract.md "Security and retention". Registered in SwarmServiceProvider and added to docs/public-surface.md under Artisan Commands and Audit Extension Points; full read contract documented in docs/audit-evidence-contract.md under a new "Reading the Audit Chain" section.
  • SwarmFake intercepts for the v0.4 audit-extension contracts (#42). Three new static helpers — SwarmFake::interceptCapturePolicy(), interceptSinkFailureHandler(), and interceptSwarmAuditSigner() — 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, plus Never* 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 preserve SwarmFake'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, so SwarmFake itself never constructs or invokes the dispatcher.
  • RunContext::fake() test helper for ad-hoc test setup (#43). New named constructor on the public RunContext value object that returns a context with sensible test defaults (deterministic run id "fake-run-id", empty input, no actor). Override any slot via the $overrides array (run_id, input, data, metadata, artifacts, actor — the latter delegates to the existing withActor() builder and accepts Actor | 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 in docs/testing.md with a worked example bridging RunContext::fake() into SwarmFake assertions. Pure additive — zero changes to existing public methods.
  • End-to-end audit-chain test with mid-flight signer rotation (#41). New tests/Feature/AuditChainEndToEndTest.php exercises the full chain (enqueue → drain attempt → transient failure → re-attempt → eventual success or dead-letter) through the real SwarmAuditDispatcher and DatabaseAuditOutbox rather than unit-testing the components in isolation. Four parameterized scenarios cover happy-path replay and full chain-to-dead-letter, each in both encrypt_at_rest=true and =false modes; the K1 signature is asserted to persist across rotation to K2, attempts progression is verified, last_error sealing-at-rest is verified via SwarmPersistenceCipher::open() for storage-shape neutrality, and the dead-letter transition's Log::error is 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.php proves that two parallel DatabaseAuditOutbox::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. Tagged skip-locked-real-db so it skips cleanly on the testbench in-memory SQLite (which honors neither cross-process state nor FOR UPDATE SKIP LOCKED) and is exercised against a real MySQL/Postgres connection via the new composer test:process-concurrency:real-db lane and a new .github/workflows/tests-real-db.yml matrix job. The default CI lane (test:process-concurrency:ci) excludes the group under --fail-on-skipped so 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_version bump rule (#76). New tests/Unit/Audit/EvidenceSchemaVersionTest.php asserts that every audit category emitted in src/ (37 categories, excluding telemetry-only stream.event / broadcast.event) carries schema_version === EvidenceEnvelope::SCHEMA_VERSION after dispatch through SwarmAuditDispatcher. Additional canaries assert the constant is "2" (guarding against an inadvertent further bump without coordinated CHANGELOG / UPGRADING / docs updates), that the deprecated SwarmAuditDispatcher::SCHEMA_VERSION mirror tracks the envelope constant, and that a caller-supplied schema_version cannot override the envelope's enriched value. A full audit pass across src/, tests/, examples/, and database/ found zero stale "1" emitters — the codebase was already clean; this commit is pure regression coverage to keep it that way.

Changed

  • CONTRIBUTING.md refresh 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.5 command.* actor-unification as the reference example), and the dispatcher routing contract including the MAX_HANDLER_ITERATIONS = 5 runaway guard and cache-driver degrade behavior. New "Stability Surface" section documents the @internal PHPDoc 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 the Unit / Feature / ProcessConcurrency lanes — including a "Writing tests/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-generated P\Tests\… classes do not exist in child PHP processes spawned by the process driver — define worker closures in free functions, not inline; static alone 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 call app()->register(SwarmServiceProvider::class) and set the minimum config the binding chain requires).
  • docs/public-surface.md refresh 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 five SinkFailureDecision cases (Queue and DeadLetter added in v0.5 alongside the audit outbox), the AuditOutbox contract, the AuditDrainResult response value object, the swarm:audit:status and swarm:audit:reconcile v0.6 operator commands, the --audit / --durable focus flags on swarm:health, and the --type=audit lane on swarm:relay. v0.7-era additions (swarm:trace, ReadableSwarmAuditSink) were added in #44 itself.

Full entry in the CHANGELOG.