v0.16.0 — Public surface & operator contract
Public surface & operator contract: promote a stable public operator control contract out of @internal DurableSwarmManager (the epic #254 keystone), complete the @internal promotion survey, and finalize the audit contract/envelope shapes (RelayLane split, tolerant schema_version verifier, signature_key_id). Additive and backward-compatible.
Added
- Public operator control contract —
SwarmOperator(#329, epic #254 keystone). Durable-run control is now a stable, container-bound public contract instead of reaching into the@internal DurableSwarmManager. ResolveBuiltByBerry\LaravelSwarm\Contracts\SwarmOperator(app(SwarmOperator::class)) topause(),resume(),cancel(),signal(), andrecover()durable runs from an operator console, HTTP controller, approval workflow, or scheduled maintenance. The contract is control-only: operational reads (status, current step, queue routing, labels) stay on the existing publicSwarmHistory/RunHistoryStorepath — the public read row already carries what an operator console needs, so no read verbs or inspector methods were promoted (DurableRunInspectorand itsupdateLabels/updateDetails/updateQueueRouting/recordProgressmutations stay@internal, as do the engine internalsstart/wait/advance/dispatchChildSwarm). It is implemented by a thin, stateless adapter (DurableSwarmOperator) bound as a shared singleton and delegating to the already-decomposed durable collaborators; the@internal DurableSwarmManagerdeliberately does not implement the contract, preserving the@internalboundary. The contract is authorization-agnostic — verbs take a barerunIdand perform no permission checks (authorizing a caller is the consuming app's responsibility, documented as such) — and every verb fails loud (SwarmException) on an unknown or foreign run, never a silent no-op. - Rich lifecycle result objects —
DurablePauseResult,DurableResumeResult,DurableCancelResult, backed by theDurableLifecycleStatusenum (#329). The control verbs return value objects that report the effective transition instead of a bareboolthat hid it. The effective status is a typedBuiltByBerry\LaravelSwarm\Enums\DurableLifecycleStatusenum (Paused/PauseScheduled/Cancelled/CancelScheduled/Resumed/Waiting), not a raw string — a producer typo is a type error, not a silent breaking change.pause/cancelare step-boundary controls: a run idle at a checkpoint transitions immediately, a mid-step run is marked to transition at its next boundary — the result distinguishesPausedvsPauseScheduledandCancelledvsCancelScheduled(withisImmediate());resumedistinguishesResumedvsWaiting(withisWaiting()) and reportswaitingBoundaryDispatched. Each carries the run id, swarm class, topology, and atoArray().signal()continues to return the existingDurableSignalResult(accepted/duplicate/status), andrecover()continues to return the redispatched run ids (array<string>), both with their existing idempotency guarantees. signature_key_idon signed evidence + theIdentifiesSigningKeyopt-in interface (#49). A signed audit record can now name which key produced its signature, so a sink verifying a tamper-evident chain across key rotation resolves the key deterministically instead of trying each candidate. A signer opts in by implementing the newBuiltByBerry\LaravelSwarm\Contracts\IdentifiesSigningKeycompanion interface (keyId(): ?string);SwarmAuditDispatcherstamps its return onto the envelope as the package-standardizedsignature_key_idfield — exactly once at sign time, dispatcher-owned (any signer-supplied value is cleared first), and omitted when the key id is null/absent so unsigned and legacy records never carry it. The field survives the outbox enqueue→drain→replay path verbatim under the original key (no re-sign on replay), and a throwingkeyId()degrades to an absent field rather than failing an already-verifiable record. It is a non-authoritative routing hint — a sink still verifies the signature and should retain a try-all fallback — so a wrong or absent key id can never turn a valid record invalid. Additive: noschema_versionbump; signers that implement onlysign()are unaffected.- Tolerant
schema_versionverifier for sinks —SinkEnvelopeValidator(#50). A sink that validates the evidence envelope'sschema_versionno longer hard-codes the accepted values or hand-rolls rolling-deploy tolerance.BuiltByBerry\LaravelSwarm\Audit\SinkEnvelopeValidator::acceptsSchemaVersion()returns true for the in-band set (the current value plus the immediately previous value during its two-minor window);supportedVersions()/SUPPORTED_VERSIONSexpose that set. It is computed from a newEvidenceEnvelopeversion history (SCHEMA_VERSION_HISTORY+CURRENT_MINOR), so a futureschema_versionbump automatically widens the accept-list to include the previous value for two minors and then drops it — no hand-edited accept-list, and no silent loss of tolerance on the next bump. Sink-side convenience only and fully opt-in: the dispatcher is unchanged and strict-version sinks remain valid.
Changed
DurableSwarmResponse::pause(),resume(), andcancel()now return rich lifecycle result objects instead ofbool(#329). They previously always returnedtrue-or-throw, which hid whether the run actually transitioned or only scheduled a transition at its next boundary. They now returnDurablePauseResult/DurableResumeResult/DurableCancelResultrespectively. This is a source-level signature change for callers that typed the return asbool; seeUPGRADING.md. Throwing behavior on an invalid transition is unchanged.OutboxDispatchTypesplit intoRelayLane+DurableDispatchType, deprecated (#51). The overloadedOutboxDispatchTypeenum — which mixed durable-job dispatch kinds (Step/Branch/QueuedResume) with anAuditrelay lane that never reaches the durable dispatcher — is split into two cohesive enums:Enums\RelayLane(Durable,Audit) names the lane aswarm:relayinvocation drains, andEnums\DurableDispatchType(Step,Branch,QueuedResume) is the durable dispatch kind persisted inswarm_durable_outbox.dispatch_type.OutboxDispatchTypeis@deprecatedbut retained with all four cases andisAudit(), so existing code keeps working; theswarm:relay --type=step|branch|queued_resume|auditCLI surface and the persisteddispatch_typestring values are byte-identical, so no migration is required. Scheduled for removal in a future major release. SeeUPGRADING.md.
Notes
@internalpromotion survey complete — net promotions: 0 (#52). A deliberate pre-1.0 audit of every@internalmarker insrc/(docs/internal-audit-1.0.md) confirms the public extension surface is already correctly drawn: the two classes the issue named as most-likely promotions — theAuditOutboxcontract and itsAuditDrainResult— were already public (documented indocs/public-surface.mdsince v0.5.0; the issue's@internalpremise was stale). Every other candidate is an engine mechanic, a persistence-driver implementation, a decomposition helper, or a provisional schema-coupled surface that has not been stable for two minors — all kept@internal. The runner collaboratorsRunAuditEmitter,DispatchValidator, andLeaseManagerstay@internal(a user customizes audit and dispatch behavior by binding the already-publicSwarmAuditSink/SinkFailureHandler/AuditOutbox/SwarmAuditSignercontracts, never by replacing these). The two@internalcontractsCausalLogStoreandColdArchiveDriverare recorded as deferred — needs design review (provisional and schema-coupled; the natural next promotion once stable for two minors, not before). No source change; this survey is intentionally a documentation-and-decision pass.