Skip to content

v0.16.0 — Public surface & operator contract

Choose a tag to compare

@dberry37388 dberry37388 released this 02 Jul 15:52
3563f8b

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. Resolve BuiltByBerry\LaravelSwarm\Contracts\SwarmOperator (app(SwarmOperator::class)) to pause(), resume(), cancel(), signal(), and recover() 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 public SwarmHistory / RunHistoryStore path — the public read row already carries what an operator console needs, so no read verbs or inspector methods were promoted (DurableRunInspector and its updateLabels/updateDetails/updateQueueRouting/recordProgress mutations stay @internal, as do the engine internals start/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 DurableSwarmManager deliberately does not implement the contract, preserving the @internal boundary. The contract is authorization-agnostic — verbs take a bare runId and 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 the DurableLifecycleStatus enum (#329). The control verbs return value objects that report the effective transition instead of a bare bool that hid it. The effective status is a typed BuiltByBerry\LaravelSwarm\Enums\DurableLifecycleStatus enum (Paused/PauseScheduled/Cancelled/CancelScheduled/Resumed/Waiting), not a raw string — a producer typo is a type error, not a silent breaking change. pause/cancel are 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 distinguishes Paused vs PauseScheduled and Cancelled vs CancelScheduled (with isImmediate()); resume distinguishes Resumed vs Waiting (with isWaiting()) and reports waitingBoundaryDispatched. Each carries the run id, swarm class, topology, and a toArray(). signal() continues to return the existing DurableSignalResult (accepted/duplicate/status), and recover() continues to return the redispatched run ids (array<string>), both with their existing idempotency guarantees.
  • signature_key_id on signed evidence + the IdentifiesSigningKey opt-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 new BuiltByBerry\LaravelSwarm\Contracts\IdentifiesSigningKey companion interface (keyId(): ?string); SwarmAuditDispatcher stamps its return onto the envelope as the package-standardized signature_key_id field — 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 throwing keyId() 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: no schema_version bump; signers that implement only sign() are unaffected.
  • Tolerant schema_version verifier for sinks — SinkEnvelopeValidator (#50). A sink that validates the evidence envelope's schema_version no 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_VERSIONS expose that set. It is computed from a new EvidenceEnvelope version history (SCHEMA_VERSION_HISTORY + CURRENT_MINOR), so a future schema_version bump 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(), and cancel() now return rich lifecycle result objects instead of bool (#329). They previously always returned true-or-throw, which hid whether the run actually transitioned or only scheduled a transition at its next boundary. They now return DurablePauseResult / DurableResumeResult / DurableCancelResult respectively. This is a source-level signature change for callers that typed the return as bool; see UPGRADING.md. Throwing behavior on an invalid transition is unchanged.
  • OutboxDispatchType split into RelayLane + DurableDispatchType, deprecated (#51). The overloaded OutboxDispatchType enum — which mixed durable-job dispatch kinds (Step/Branch/QueuedResume) with an Audit relay lane that never reaches the durable dispatcher — is split into two cohesive enums: Enums\RelayLane (Durable, Audit) names the lane a swarm:relay invocation drains, and Enums\DurableDispatchType (Step, Branch, QueuedResume) is the durable dispatch kind persisted in swarm_durable_outbox.dispatch_type. OutboxDispatchType is @deprecated but retained with all four cases and isAudit(), so existing code keeps working; the swarm:relay --type=step|branch|queued_resume|audit CLI surface and the persisted dispatch_type string values are byte-identical, so no migration is required. Scheduled for removal in a future major release. See UPGRADING.md.

Notes

  • @internal promotion survey complete — net promotions: 0 (#52). A deliberate pre-1.0 audit of every @internal marker in src/ (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 — the AuditOutbox contract and its AuditDrainResult — were already public (documented in docs/public-surface.md since v0.5.0; the issue's @internal premise 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 collaborators RunAuditEmitter, DispatchValidator, and LeaseManager stay @internal (a user customizes audit and dispatch behavior by binding the already-public SwarmAuditSink / SinkFailureHandler / AuditOutbox / SwarmAuditSigner contracts, never by replacing these). The two @internal contracts CausalLogStore and ColdArchiveDriver are 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.