From ea1d5e26cc9244135a247ade94213062620c18d4 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 13 Jun 2026 23:08:18 -0400 Subject: [PATCH 1/3] docs: finalize state v1.0.0 changelog for release Signed-off-by: Joshua Temple --- state/CHANGELOG.md | 371 ++++++++++++++++++++++----------------------- 1 file changed, 182 insertions(+), 189 deletions(-) diff --git a/state/CHANGELOG.md b/state/CHANGELOG.md index 4e13be8..af674fe 100644 --- a/state/CHANGELOG.md +++ b/state/CHANGELOG.md @@ -9,100 +9,15 @@ A machine definition is treated as a schema: see the counts as an additive (minor) versus breaking (major) change. Use the `state/evolution` package to classify a machine change and decide the bump. -## [Unreleased] — v1.0.0 release candidate +## [1.0.0] — 2026-06-13 -Freeze-ready, pending human sign-off. This release candidate resolves the v1.0 -freeze-readiness gap analysis: the parallel-region commit path now matches the -documented semantics, the serialized IR round-trips losslessly, and the frozen -public surface is locked. It is intentionally **not tagged**; the data model, -serialized IR, context model, effect envelope, and emission-ordering contract are -ready to freeze on sign-off. The `analysis`, `evolution`, `conformance`, and -`verify` subpackages ship as advisory (not part of the frozen contract). - -### Fixed - -- `Fire` now honors context cancellation at microstep boundaries. It checks the - context before the triggering transition and between every run-to-completion - microstep — the same granularity `WaitFor` polls at — but never mid-microstep, so - a single in-flight cascade always settles before the next boundary check. A context - that is canceled or past its deadline at a boundary aborts the macrostep and - surfaces `context.Canceled` / `context.DeadlineExceeded` on `FireResult.Err` - (matchable with `errors.Is`). The abort routes through the same transactional - rollback as a failed `Fire`, so a canceled `Fire` is a clean no-op: the instance is - left at its pre-Fire configuration and context, and `FireResult.Effects` is nil. - Previously a canceled or expired context still ran the full macrostep. -- A failed `Fire` is now fully transactional on the instance's internal state: when - an action or assign errors or panics partway through a macrostep, the active - configuration, current leaf, context, and recorded history all roll back to their - pre-Fire values, `FireResult.NewState` reports the original state rather than the - abandoned target, and a snapshot taken afterward is identical to one never Fired. - This extends the existing effect-emission transactionality to the configuration — - previously a failed `Fire` left the configuration half-advanced. The successful - path is unchanged: the configuration still advances in place during the macrostep - so entry actions, the done cascade, and the run-to-completion loop observe it. A - partial parallel-region commit (an earlier region committing before a later region - fails) rolls back with the rest of the macrostep. -- A `Raise` declared on a region-internal transition is now delivered instead of - silently dropped (kernel parallel-region commit path). -- A region-internal transition targeting a same-region history pseudostate now - resolves history correctly instead of leaving the pseudostate permanently stuck as - an active leaf. -- A compound nested inside a parallel region now emits its done event and fires - `OnDone` when its leaf reaches final. -- Entering the initial configuration at Cast now runs full entry semantics — - `OnEntry` actions and `OnEntryAssign` reducers, `after` arming, invoke/actor - starts, eventless (`Always`) settling, and an enclosing compound's done / - `OnDone` when initial descent lands on a final leaf — instead of only - recomputing the invoke/actor starts. The effects are buffered and read once - after Cast via `InitialEffects` (full stream) and `StartEffects` (invoke/actor - subset). -- A parallel state that completes inside an enclosing compound now cascades that - compound's `OnDone` (and any guarded completion transition) up the spine, instead - of silently dropping it; the parallel's own `OnDone` still fires exactly once. -- Region transition and entry actions now observe the threaded, exit-assign-folded - context, consistent with the main commit path. -- Eventless (`Always`) transitions now fire in every active parallel region and no - longer collapse a sibling region's configuration. -- Exiting a parallel state now runs every active region leaf's `OnExit` actions - innermost-first, in declaration order; an event handled only by a sibling region - of an outer parallel is now delivered through nested parallels correctly. -- A panicking host action is recovered into a typed `ActionPanicError` instead of - crashing `Fire`. -- An event matched by a guard-failed candidate inside a region now bubbles to the - parallel-state-level handler instead of causing the fire to fail silently. -- The raised-event queue is reset when a macrostep errors, so a queued event no - longer leaks into the next `Fire`. -- Effects are emitted only on a fully successful `Fire`; a failed fire emits no - effects (transactional effect emission on the main commit path). -- `Snapshot`/`Restore` preserves bounded-history (`WithHistory(n)`) retention - instead of reverting to unbounded. -- `SnapshotActors` refuses a non-quiesced actor tree with a typed - `NonQuiescentActorError` instead of silently dropping queued messages. -- `Trace.AssignsApplied` is aggregated across all microsteps of a macrostep instead - of reporting only the triggering microstep's assigns. -- `Region`, `Invocation`, and `IOSpec` IR nodes now preserve unknown JSON fields and - `Meta` losslessly, making the lossless round-trip guarantee true for every - IR-reachable node type. -- `GuardPanicError` and `AssignPanicError` now `Unwrap` to the recovered error, and - `SnapshotError` exposes its cause, so `errors.As`/`errors.Is` can reach a wrapped - sentinel through any of them. -- `SchemaOf` returns an honest `SchemaAny` for interface, func, chan, and complex - types instead of coercing them to `SchemaString`. -- Builder cursor consumers (`When`, `Do`, `Assign`, `Raise`, `GoTo`, and friends) - now panic with an actionable construction-time message when no transition is open, - instead of silently dropping the call. -- `analysis` honors wildcard (`OnAny`) edges and excludes forbidden (`Forbid` / - `ForbidAny`) edges from reachability and dead-end analysis, removing false - unreachable and dead-end findings. -- `evolution` now detects transition order/priority, guard-operator structure, - initial-child, history, context-schema, and eventless-edge changes, and classifies - any unmodeled structural difference as breaking (fail-safe), closing silent - under-reporting of behavior-changing edits. -- `conformance` compares effects order-sensitively and payload-aware, and captures - trace and final context, so it no longer passes a reordering or wrong-payload - regression. -- The pointer-context determinism diagnostic is advisory and is not rejected under - `Quench(Strict())`; pointer context remains a supported escape hatch. +The first stable release. It freezes the data model, the serialized IR, the +context model, the effect envelope, and the emission-ordering contract: from +1.0.0 onward, new capabilities arrive as additive packages, modules, and options +rather than breaking changes. The parallel-region commit path matches the +documented semantics, the serialized IR round-trips losslessly, and the public +surface is locked. The `analysis`, `evolution`, `conformance`, and `verify` +subpackages ship as advisory and are not part of the frozen contract. ### Added @@ -134,96 +49,6 @@ ready to freeze on sign-off. The `analysis`, `evolution`, `conformance`, and `ParamSpec.Examples` via `ParamSpec`. All three are additive and `omitempty`: a descriptor or parameter that declares none serializes byte-identically to before. -### Changed - -- `WaitMode`, `HistoryType`, and `ActorKind` integer wire values are documented as - frozen and append-only; `JournalRandom` is specified to ride - `JournalEntry.Payload`. - -### Documentation - -- Stability banners: `analysis`, `evolution`, `conformance`, `verify`, and - `verify/symbolic` are documented as advisory (not part of the frozen contract); - `expr` documents its guard-expression semantics and deterministic evaluation - environment as part of the v1.0 contract. -- `verify` docs corrected to state that bounded simulation enumerates - configurations (not traces), the covering suite is a structural guarantee, and - only the holding verdict is exact. -- Documented that `Instance` is not safe for concurrent use; that timer absolute - deadlines are a host concern (`ResumeEffects` re-arms at the full declared delay - while `durable` persists deadlines); the guard eval-error asymmetry (eventless - guards fail closed, event-driven guards fail loud); and the v1.0 stability scope. - The initial configuration now runs full entry semantics at Cast, so entering a - compound via initial descent onto a final leaf raises that compound's done - event. -- `Trace.PoliciesEvaluated` documented as reserved (always empty in v1.0). - -### Tests - -- Exhaustive symbolic op-table regression tests over every guard operator (the only - unsoundness-capable code path). -- Seeded property tests for snapshot resume-equivalence and parallel-region - determinism. -- Regression tests porting the parallel-region kernel probes: Raise-in-region, - history-into-region, interior-compound done, sibling-region eventless and exit, - nested-parallel delivery, and a simultaneous-region-completion pin. - -### Deferred to a future release (with reason) - -- The transactional-effects fix is scoped to the main commit path; the region - commit path retains partial-emit-on-error behavior at the freeze. -- `verify.Finding.Reachable` polarity reshape (verify is advisory; additive - kind-specific accessors are provided instead of a breaking field change). -- `conformance` `Assertion.Expected`/`Event` payload reshape and auto-derived effect - assertions in generated goldens (advisory; needs a host entity/codec, which would - be a breaking change). -- Snapshot-carried absolute timer deadlines (`Pending.TimerDeadlines`) — documented - as a host concern in v1.0; additive in a later release. -- Post-v1.0 performance items: lazy hot-path trace-string construction, ref-slice - copy-on-write, trace-history bounding, and random-IR fixpoint and - eventless-termination fuzzing. - -### Deferred additive API surface (post-1.0, non-breaking) - -These are deliberate v1.0 boundaries, not oversights. Each lands later as a purely -additive change — a new builder method, a new option on an existing variadic tail, -or a new optional IR field — so adopting it never breaks a v1 caller. The freeze -locks today's surface precisely so these can be added without a major bump. - -- Payload-carrying `Raise`: the raised internal-event queue is runtime-internal - today, so a transition raises an event without data. A future builder method - carries a payload, backed by an additive optional field on the IR transition; - the existing `Raise []E` slice is unchanged. -- Multi-target transitions: a single transition targets one state via `To`. A - future `.ToAll(...)` builder, backed by an optional `Targets` IR field, expresses - a fan-out; `To` keeps its current single-target meaning. -- `context.Context` on `Verify` / `PlanPath` / `Cast` / `Restore` and the snapshot - codecs: these take no `context.Context` at v1. A future `WithContext` option on - each existing option tail threads cancellation and deadlines without changing a - signature. (`RestoreActors` already takes a `context.Context`.) -- Relaxing `SnapshotActors`' non-quiesced refusal: it refuses to snapshot a - non-quiesced actor system by design. A future `SnapshotActorsOption` lets a host - opt into a defined non-quiesced capture; the strict refusal stays the default. -- Finer Inspector cadence: the Inspector fires once per macrostep — that - once-per-macrostep cadence is the documented v1 contract. A future additive - `InspectKind` exposes per-microstep events for callers that want them; existing - inspectors keep their macrostep cadence. -- A metrics `Meter` seam: there is no metrics hook at v1. A future `WithMeter` - option attaches a meter through the existing option tail, alongside the structured - logger and inspector seams. - -## [1.0.0] - -The first stable release. The 0.2.0 to 1.0.0 step finalizes the breaking changes -listed under Changed below, after which the data model and contracts are fixed: a -machine definition, its serialized IR, the context model, the effect envelope, and -the emission-ordering contract are frozen so that, from 1.0.0 onward, future -capabilities arrive as additive packages, modules, and options rather than breaking -changes. See the "Performance baseline (v1.0.0)" note at the end of this section for -the representative hot-path numbers. - -### Added - - Versioned IR envelope. A definition's serialized form now carries an explicit `schemaVersion` (stamped by `ToJSON`, currently `"1.0"` via `CurrentSchemaVersion`), an optional machine `id` and `version`, opaque @@ -616,6 +441,10 @@ the representative hot-path numbers. ### Changed +- `WaitMode`, `HistoryType`, and `ActorKind` integer wire values are documented as + frozen and append-only; `JournalRandom` is specified to ride + `JournalEntry.Payload`. + - **BREAKING: context is now value-semantic; actions no longer mutate context.** The context model is frozen: a context value `C` flows through the step as data, guards and actions observe it through a read-only projection, and the *only* @@ -670,16 +499,20 @@ the representative hot-path numbers. all struct types with no sentinel vars. `ErrInvalidTransition`, `ErrGuardFailed`, `ErrGuardPanic`, `ErrAssignPanic`, `ErrPolicyDenied`, `ErrUndeclaredState`, `ErrUnboundRef`, `ErrActionFailed`, `ErrMicrostepOverflow`, `ErrNoPath`, - `ErrNoInitialState`, `ErrUnknownBuiltin`, `ErrUnboundActor`, `ErrUnsupportedSchema`, - `ErrUnknownEffectKind`, and `MultiRegionErr` become `InvalidTransitionError`, + `ErrNoInitialState`, `ErrUnknownBuiltin`, `ErrUnboundActor`, `ErrActorPanic`, + `ErrUnsupportedSchema`, `ErrUnknownEffectKind`, and `MultiRegionErr` become + `InvalidTransitionError`, `GuardFailedError`, `GuardPanicError`, `AssignPanicError`, `PolicyDeniedError`, `UndeclaredStateError`, `UnboundRefError`, `ActionFailedError`, `MicrostepOverflowError`, `NoPathError`, `NoInitialStateError`, - `UnknownBuiltinError`, `UnboundActorError`, `UnsupportedSchemaError`, + `UnknownBuiltinError`, `UnboundActorError`, `ActorPanicError`, + `UnsupportedSchemaError`, `UnknownEffectKindError`, and `MultiRegionError`, matching the already-correct `WaitTimeoutError`, `SnapshotError`, `SnapshotVersionError`, and `VerifyError`. - Behavior and fields are unchanged; update the type name at each `errors.As` - target, type switch, and struct literal. + `ActorPanicError` also gains an `Unwrap()` returning the recovered panic value, + so `errors.As`/`errors.Is` reach the underlying cause. Behavior and fields are + otherwise unchanged; update the type name at each `errors.As` target, type + switch, and struct literal. - The determinism and ordering contract is now explicit and frozen: emission order is exit → transition → entry across the cascade, declaration order within a set, fixed parallel-region order, and the run-to-completion interleave for @@ -688,6 +521,91 @@ the representative hot-path numbers. ### Fixed +- `Fire` now honors context cancellation at microstep boundaries. It checks the + context before the triggering transition and between every run-to-completion + microstep — the same granularity `WaitFor` polls at — but never mid-microstep, so + a single in-flight cascade always settles before the next boundary check. A context + that is canceled or past its deadline at a boundary aborts the macrostep and + surfaces `context.Canceled` / `context.DeadlineExceeded` on `FireResult.Err` + (matchable with `errors.Is`). The abort routes through the same transactional + rollback as a failed `Fire`, so a canceled `Fire` is a clean no-op: the instance is + left at its pre-Fire configuration and context, and `FireResult.Effects` is nil. + Previously a canceled or expired context still ran the full macrostep. +- A failed `Fire` is now fully transactional on the instance's internal state: when + an action or assign errors or panics partway through a macrostep, the active + configuration, current leaf, context, and recorded history all roll back to their + pre-Fire values, `FireResult.NewState` reports the original state rather than the + abandoned target, and a snapshot taken afterward is identical to one never Fired. + This extends the existing effect-emission transactionality to the configuration — + previously a failed `Fire` left the configuration half-advanced. The successful + path is unchanged: the configuration still advances in place during the macrostep + so entry actions, the done cascade, and the run-to-completion loop observe it. A + partial parallel-region commit (an earlier region committing before a later region + fails) rolls back with the rest of the macrostep. +- A `Raise` declared on a region-internal transition is now delivered instead of + silently dropped (kernel parallel-region commit path). +- A region-internal transition targeting a same-region history pseudostate now + resolves history correctly instead of leaving the pseudostate permanently stuck as + an active leaf. +- A compound nested inside a parallel region now emits its done event and fires + `OnDone` when its leaf reaches final. +- Entering the initial configuration at Cast now runs full entry semantics — + `OnEntry` actions and `OnEntryAssign` reducers, `after` arming, invoke/actor + starts, eventless (`Always`) settling, and an enclosing compound's done / + `OnDone` when initial descent lands on a final leaf — instead of only + recomputing the invoke/actor starts. The effects are buffered and read once + after Cast via `InitialEffects` (full stream) and `StartEffects` (invoke/actor + subset), and `InitialErr()` surfaces any error raised while entering the initial + configuration so a host can detect an initial-entry failure immediately after + Cast. +- A parallel state that completes inside an enclosing compound now cascades that + compound's `OnDone` (and any guarded completion transition) up the spine, instead + of silently dropping it; the parallel's own `OnDone` still fires exactly once. +- Region transition and entry actions now observe the threaded, exit-assign-folded + context, consistent with the main commit path. +- Eventless (`Always`) transitions now fire in every active parallel region and no + longer collapse a sibling region's configuration. +- Exiting a parallel state now runs every active region leaf's `OnExit` actions + innermost-first, in declaration order; an event handled only by a sibling region + of an outer parallel is now delivered through nested parallels correctly. +- A panicking host action is recovered into a typed `ActionPanicError` instead of + crashing `Fire`. +- An event matched by a guard-failed candidate inside a region now bubbles to the + parallel-state-level handler instead of causing the fire to fail silently. +- The raised-event queue is reset when a macrostep errors, so a queued event no + longer leaks into the next `Fire`. +- Effects are emitted only on a fully successful `Fire`; a failed fire emits no + effects (transactional effect emission on the main commit path). +- `Snapshot`/`Restore` preserves bounded-history (`WithHistory(n)`) retention + instead of reverting to unbounded. +- `SnapshotActors` refuses a non-quiesced actor tree with a typed + `NonQuiescentActorError` instead of silently dropping queued messages. +- `Trace.AssignsApplied` is aggregated across all microsteps of a macrostep instead + of reporting only the triggering microstep's assigns. +- `Region`, `Invocation`, and `IOSpec` IR nodes now preserve unknown JSON fields and + `Meta` losslessly, making the lossless round-trip guarantee true for every + IR-reachable node type. +- `GuardPanicError` and `AssignPanicError` now `Unwrap` to the recovered error, and + `SnapshotError` exposes its cause, so `errors.As`/`errors.Is` can reach a wrapped + sentinel through any of them. +- `SchemaOf` returns an honest `SchemaAny` for interface, func, chan, and complex + types instead of coercing them to `SchemaString`. +- Builder cursor consumers (`When`, `Do`, `Assign`, `Raise`, `GoTo`, and friends) + now panic with an actionable construction-time message when no transition is open, + instead of silently dropping the call. +- `analysis` honors wildcard (`OnAny`) edges and excludes forbidden (`Forbid` / + `ForbidAny`) edges from reachability and dead-end analysis, removing false + unreachable and dead-end findings. +- `evolution` now detects transition order/priority, guard-operator structure, + initial-child, history, context-schema, and eventless-edge changes, and classifies + any unmodeled structural difference as breaking (fail-safe), closing silent + under-reporting of behavior-changing edits. +- `conformance` compares effects order-sensitively and payload-aware, and captures + trace and final context, so it no longer passes a reordering or wrong-payload + regression. +- The pointer-context determinism diagnostic is advisory and is not rejected under + `Quench(Strict())`; pointer context remains a supported escape hatch. + - `Cast` returns the typed `*InvalidTransitionError` consistently for an event that matches no transition, including inside parallel regions, so a caller can distinguish "no transition" from other failures uniformly. @@ -702,6 +620,37 @@ the representative hot-path numbers. (including nested compounds). `Fire` stays pure: the fix emits effect data, it does not run timers/services/actors in the kernel. +### Documentation + +- Stability banners: `analysis`, `evolution`, `conformance`, `verify`, and + `verify/symbolic` are documented as advisory (not part of the frozen contract); + `expr` documents its guard-expression semantics and deterministic evaluation + environment as part of the v1.0 contract. +- `verify` docs corrected to state that bounded simulation enumerates + configurations (not traces), the covering suite is a structural guarantee, and + only the holding verdict is exact. +- Documented that `Instance` is not safe for concurrent use; that timer absolute + deadlines are a host concern (`ResumeEffects` re-arms at the full declared delay + while `durable` persists deadlines); the guard eval-error asymmetry (eventless + guards fail closed, event-driven guards fail loud); and the v1.0 stability scope. + The initial configuration now runs full entry semantics at Cast, so entering a + compound via initial descent onto a final leaf raises that compound's done + event. +- `Trace.PoliciesEvaluated` documented as reserved (always empty in v1.0). + +### Tests + +- Exhaustive symbolic op-table regression tests over every guard operator (the only + unsoundness-capable code path). +- Seeded property tests for snapshot resume-equivalence and parallel-region + determinism. +- Regression tests porting the parallel-region kernel probes: Raise-in-region, + history-into-region, interior-compound done, sibling-region eventless and exit, + nested-parallel delivery, and a simultaneous-region-completion pin. +- A wire/API freeze guard that pins the serialized JSON shapes of the v1 IR and + effect envelopes and the exported public API signatures, so an accidental change + to a frozen wire form or surface fails the build. + ### Performance baseline (v1.0.0) Representative numbers from `go test -run=^$ -bench=. -benchmem ./...` on the @@ -720,6 +669,50 @@ fresh trace and effect set as data. | `SnapshotRestore` | ~28,550 | 15,108 | 121 | snapshot capture + restore | | `E2E_ConnectionLifecycle` | ~46,187 | 59,856 | 657 | end-to-end exemplar over the wired host runtime | +### Deferred to a future release (with reason) + +- The transactional-effects fix is scoped to the main commit path; the region + commit path retains partial-emit-on-error behavior at the freeze. +- `verify.Finding.Reachable` polarity reshape (verify is advisory; additive + kind-specific accessors are provided instead of a breaking field change). +- `conformance` `Assertion.Expected`/`Event` payload reshape and auto-derived effect + assertions in generated goldens (advisory; needs a host entity/codec, which would + be a breaking change). +- Snapshot-carried absolute timer deadlines (`Pending.TimerDeadlines`) — documented + as a host concern in v1.0; additive in a later release. +- Post-v1.0 performance items: lazy hot-path trace-string construction, ref-slice + copy-on-write, trace-history bounding, and random-IR fixpoint and + eventless-termination fuzzing. + +### Deferred additive API surface (post-1.0, non-breaking) + +These are deliberate v1.0 boundaries, not oversights. Each lands later as a purely +additive change — a new builder method, a new option on an existing variadic tail, +or a new optional IR field — so adopting it never breaks a v1 caller. The freeze +locks today's surface precisely so these can be added without a major bump. + +- Payload-carrying `Raise`: the raised internal-event queue is runtime-internal + today, so a transition raises an event without data. A future builder method + carries a payload, backed by an additive optional field on the IR transition; + the existing `Raise []E` slice is unchanged. +- Multi-target transitions: a single transition targets one state via `To`. A + future `.ToAll(...)` builder, backed by an optional `Targets` IR field, expresses + a fan-out; `To` keeps its current single-target meaning. +- `context.Context` on `Verify` / `PlanPath` / `Cast` / `Restore` and the snapshot + codecs: these take no `context.Context` at v1. A future `WithContext` option on + each existing option tail threads cancellation and deadlines without changing a + signature. (`RestoreActors` already takes a `context.Context`.) +- Relaxing `SnapshotActors`' non-quiesced refusal: it refuses to snapshot a + non-quiesced actor system by design. A future `SnapshotActorsOption` lets a host + opt into a defined non-quiesced capture; the strict refusal stays the default. +- Finer Inspector cadence: the Inspector fires once per macrostep — that + once-per-macrostep cadence is the documented v1 contract. A future additive + `InspectKind` exposes per-microstep events for callers that want them; existing + inspectors keep their macrostep cadence. +- A metrics `Meter` seam: there is no metrics hook at v1. A future `WithMeter` + option attaches a meter through the existing option tail, alongside the structured + logger and inspector seams. + ## [0.2.0] - 2026-05-30 ### Added @@ -1008,6 +1001,6 @@ Initial release of the pure state-machine kernel. clock/ID seams for determinism. - Reusable conformance harness with golden scenarios. -[1.0.0]: https://github.com/stablekernel/crucible/compare/state/v0.2.0...state/v1.0.0 +[1.0.0]: https://github.com/stablekernel/crucible/compare/state/v0.3.0...state/v1.0.0 [0.2.0]: https://github.com/stablekernel/crucible/releases/tag/state/v0.2.0 [0.1.0]: https://github.com/stablekernel/crucible/releases/tag/state/v0.1.0 From c40b8053a69638772c14581a8bf8bca04831b540 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 13 Jun 2026 23:25:44 -0400 Subject: [PATCH 2/3] docs: align README and examples for the state v1.0.0 release Signed-off-by: Joshua Temple --- README.md | 45 ++++++++++++------- .../docs/concepts/machine-and-instance.md | 6 +-- .../content/docs/start/getting-started.mdx | 26 ++++------- .../content/docs/start/ingest-drive-emit.md | 23 ++++------ 4 files changed, 49 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 19069f8..48c8ed5 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,14 @@ stability label. | Module | What it is | Status | | --------------------- | -------------------------------------------------------------------------------- | ------------ | -| `state` | Full-featured, domain-agnostic statechart engine. Stdlib-only, no IO. | experimental | -| `state/analysis` | Static model-checking and path enumeration over a machine's IR. | experimental | -| `state/evolution` | Diffs two machine definitions and classifies the SemVer bump. | experimental | -| `state/conformance` | Reusable harness for driving golden scenarios against a machine. | experimental | -| `state/expr` | Rich expression tier: CEL-backed guards type-checked against the context schema. | experimental | +| `state` | Full-featured, domain-agnostic statechart engine. Stdlib-only, no IO. | v1.0.0 | +| `state/analysis` | Static model-checking and path enumeration over a machine's IR. | advisory | +| `state/evolution` | Diffs two machine definitions and classifies the SemVer bump. | advisory | +| `state/conformance` | Reusable harness for driving golden scenarios against a machine. | advisory | +| `state/verify` | Decides behavioral properties of a machine and returns a witness event sequence. | advisory | +| `state/expr` | Rich expression tier: CEL-backed guards type-checked against the context schema. | stable contract (v0.1.0) | +| `gen` | Eject codegen: turn a machine's IR into typed Go stub source and a registry wiring. | experimental | +| `cmd/crucible` | Headless IR CLI: lint, render, diff, validate, and eject a machine's serialized IR. | experimental | | `telemetry` | Vendor-neutral tracing/metrics interface for the IO modules. Stdlib-only. | experimental | | `telemetry/slog` | `log/slog` adapter for the telemetry interface. | experimental | | `telemetry/otel` | OpenTelemetry adapter for the telemetry interface. | experimental | @@ -127,16 +130,24 @@ in-memory `source/memsource` test source, each experimental. ## Status -Early and evolving. `state` is a complete, embeddable statechart engine, covering -hierarchical, parallel, and final states, history, guard combinators, delayed -transitions, invoked services, an actor model, snapshots, and JSON -(de)serialization, backed by its `analysis`, `evolution`, and `conformance` -packages. `telemetry`, `sink`, and `source` (with all their adapters, codecs, and +`state` is released at **v1.0.0**: a complete, embeddable statechart engine +covering hierarchical, parallel, and final states, history, guard combinators, +delayed transitions, invoked services, an actor model, snapshots, and JSON +(de)serialization. Its public contract is frozen under v1 SemVer. The +`analysis`, `evolution`, `conformance`, and `verify` subpackages ship inside +v1.0 but are **advisory**: they produce diagnostics, and their surfaces sit +outside the frozen contract and may change in a minor release. `state/expr` is a +separate module pinned at v0.1.0 whose expression *semantics* are a committed, +stable contract even though the module version is pre-1.0. + +The remaining modules are still evolving and may change before they reach v1: +`telemetry`, `sink`, and `source` (with all their adapters, codecs, and middleware) are released and documented, as are the host-side runtimes over the kernel: `durable` (durable execution), `cluster` (distribution and live migration), `transport` (the gRPC network transport for cluster), and `wasm` -(polyglot behaviors). `broker` is planned. Treat every API as experimental until -it reaches v1. +(polyglot behaviors). The IR tools `gen` (eject codegen) and `cmd/crucible` (the +IR CLI) ship unversioned for now, and `broker` is planned. Treat those modules as +experimental until each reaches its own v1. ## Roadmap @@ -162,12 +173,16 @@ and forcing nothing third-party on the consumer: A small set of tools works the IR directly: +- [x] **IR CLI** (`cmd/crucible`): headless IR tooling for CI. Lint reachability and + nondeterminism, render diagrams, diff and validate, and classify version diffs + straight from a machine's serialized IR, no behavior bound. +- [x] **Eject codegen** (`gen`): turn a machine's IR into typed Go stub source. Each + referenced behavior becomes a panic-bodied stub typed to the exact engine signature, + plus a `Provide` function that wires them against the registry, so the host fills in + bodies against a contract the compiler already checks. - [ ] **Visual editor** _(planned)_: a browser workbench over the IR. Author, simulate, and inspect machines, with reachability and version-diff overlays from the existing `analysis` and `evolution` packages. -- [ ] **IR CLI** _(exploring)_: headless IR tooling for CI. Lint reachability and - nondeterminism, render diagrams, and classify version diffs straight from a machine's - IR. Durable state and event persistence is tracked separately with the `durable` runtime, not here. diff --git a/docs/src/content/docs/concepts/machine-and-instance.md b/docs/src/content/docs/concepts/machine-and-instance.md index e2d5284..4da5278 100644 --- a/docs/src/content/docs/concepts/machine-and-instance.md +++ b/docs/src/content/docs/concepts/machine-and-instance.md @@ -17,9 +17,9 @@ goroutines and reuse for the lifetime of your process. You forge and quench it once: ```go -m := state.Forge[Gate, Signal, Turnstile]("turnstile"). - Initial(Locked). - Transition(Locked).On(Coin).GoTo(Unlocked). +m := state.ForgeFor[Turnstile]("turnstile"). + Initial("Locked"). + Transition("Locked").On("Coin").GoTo("Unlocked"). Quench() // *Machine, immutable, share freely ``` diff --git a/docs/src/content/docs/start/getting-started.mdx b/docs/src/content/docs/start/getting-started.mdx index 6a74d9d..d2dbb55 100644 --- a/docs/src/content/docs/start/getting-started.mdx +++ b/docs/src/content/docs/start/getting-started.mdx @@ -31,27 +31,17 @@ import ( "github.com/stablekernel/crucible/state" ) -type Gate string // S -type Signal string // E type Turnstile struct{ Coins int } // C -const ( - Locked Gate = "Locked" - Unlocked Gate = "Unlocked" -) - -const ( - Coin Signal = "Coin" - Push Signal = "Push" -) - func main() { // Forge a builder, declare states + transitions, then Quench to freeze // the definition into an immutable *Machine. Quench panics on misconfig. - m := state.Forge[Gate, Signal, Turnstile]("turnstile"). - Initial(Locked). - Transition(Locked).On(Coin).GoTo(Unlocked). - Transition(Unlocked).On(Push).GoTo(Locked). + // State and event identifiers here are plain strings, so ForgeFor fixes + // S and E to string and leaves only the context type to spell. + m := state.ForgeFor[Turnstile]("turnstile"). + Initial("Locked"). + Transition("Locked").On("Coin").GoTo("Unlocked"). + Transition("Unlocked").On("Push").GoTo("Locked"). Quench() // Cast an instance around an entity value. @@ -59,10 +49,10 @@ func main() { // Fire advances the instance and returns a FireResult. It performs NO IO. // NewState is the next state, Effects is data for the caller to dispatch. - res := inst.Fire(context.Background(), Coin) + res := inst.Fire(context.Background(), "Coin") fmt.Println(res.NewState) // Unlocked - res = inst.Fire(context.Background(), Push) + res = inst.Fire(context.Background(), "Push") fmt.Println(res.NewState) // Locked } ``` diff --git a/docs/src/content/docs/start/ingest-drive-emit.md b/docs/src/content/docs/start/ingest-drive-emit.md index 330ffa6..98bec25 100644 --- a/docs/src/content/docs/start/ingest-drive-emit.md +++ b/docs/src/content/docs/start/ingest-drive-emit.md @@ -31,26 +31,19 @@ A toy turnstile that emits an `Opened` effect when it unlocks. The effect is pur data; the machine performs no IO. ```go -type Gate string // S -type Signal string // E type Turnstile struct{ Coins int } // C -const ( - Locked Gate = "Locked" - Unlocked Gate = "Unlocked" -) - -const Coin Signal = "Coin" - type Opened struct{ Coins int } -machine := state.Forge[Gate, Signal, Turnstile]("turnstile"). +// State and event identifiers are plain strings, so ForgeFor fixes S and E to +// string and leaves only the context type to spell. +machine := state.ForgeFor[Turnstile]("turnstile"). // An action returns an effect (pure data) for the transition to emit. Action("announceOpen", func(a state.ActionCtx[Turnstile]) (state.Effect, error) { return Opened{Coins: a.Entity.Coins}, nil }). - Initial(Locked). - Transition(Locked).On(Coin).GoTo(Unlocked).Do("announceOpen"). + Initial("Locked"). + Transition("Locked").On("Coin").GoTo("Unlocked").Do("announceOpen"). Quench() ``` @@ -72,11 +65,11 @@ manifold := sink.NewManifold(sink.WithOutlets(sink.OutletFunc( ))) // Durable instance state for the bridge (in-memory for a single process). -store := statemachine.NewMemStore[string, Gate, Signal, Turnstile]() +store := statemachine.NewMemStore[string, string, string, Turnstile]() // Route a message to its instance key and the event to fire. -router := func(m source.Message) (string, Signal, error) { - return m.Headers().Get("turnstile-id"), Coin, nil +router := func(m source.Message) (string, string, error) { + return m.Headers().Get("turnstile-id"), "Coin", nil } handler := statemachine.Drive(machine, store, router, From 2e2a7dce2e8f85dc7665c0db788bcf4436cbf9ba Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 13 Jun 2026 23:31:03 -0400 Subject: [PATCH 3/3] docs: document the crucible CLI and gen, and date the v0.1.0 sibling changelogs Signed-off-by: Joshua Temple --- README.md | 13 ++- cmd/crucible/CHANGELOG.md | 13 ++- docs/astro.config.mjs | 6 ++ docs/src/content/docs/tooling/cli.md | 130 +++++++++++++++++++++++++ docs/src/content/docs/tooling/eject.md | 73 ++++++++++++++ gen/CHANGELOG.md | 2 +- state/expr/CHANGELOG.md | 2 +- 7 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 docs/src/content/docs/tooling/cli.md create mode 100644 docs/src/content/docs/tooling/eject.md diff --git a/README.md b/README.md index 48c8ed5..a7a1c4e 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ stability label. | `state/conformance` | Reusable harness for driving golden scenarios against a machine. | advisory | | `state/verify` | Decides behavioral properties of a machine and returns a witness event sequence. | advisory | | `state/expr` | Rich expression tier: CEL-backed guards type-checked against the context schema. | stable contract (v0.1.0) | -| `gen` | Eject codegen: turn a machine's IR into typed Go stub source and a registry wiring. | experimental | -| `cmd/crucible` | Headless IR CLI: lint, render, diff, validate, and eject a machine's serialized IR. | experimental | +| `gen` | Eject codegen: turn a machine's IR into typed Go stub source and a registry wiring. | v0.1.0 | +| `cmd/crucible` | Headless IR CLI: lint, render, diff, validate, and eject a machine's serialized IR. | v0.1.0 | | `telemetry` | Vendor-neutral tracing/metrics interface for the IO modules. Stdlib-only. | experimental | | `telemetry/slog` | `log/slog` adapter for the telemetry interface. | experimental | | `telemetry/otel` | OpenTelemetry adapter for the telemetry interface. | experimental | @@ -140,14 +140,17 @@ outside the frozen contract and may change in a minor release. `state/expr` is a separate module pinned at v0.1.0 whose expression *semantics* are a committed, stable contract even though the module version is pre-1.0. +The IR tools `gen` (eject codegen) and `cmd/crucible` (the IR CLI) are released +at **v0.1.0**, versioned independently of `state` and free to move at their own +pace. + The remaining modules are still evolving and may change before they reach v1: `telemetry`, `sink`, and `source` (with all their adapters, codecs, and middleware) are released and documented, as are the host-side runtimes over the kernel: `durable` (durable execution), `cluster` (distribution and live migration), `transport` (the gRPC network transport for cluster), and `wasm` -(polyglot behaviors). The IR tools `gen` (eject codegen) and `cmd/crucible` (the -IR CLI) ship unversioned for now, and `broker` is planned. Treat those modules as -experimental until each reaches its own v1. +(polyglot behaviors). `broker` is planned. Treat those modules as experimental +until each reaches its own v1. ## Roadmap diff --git a/cmd/crucible/CHANGELOG.md b/cmd/crucible/CHANGELOG.md index 82d1b7c..55771fd 100644 --- a/cmd/crucible/CHANGELOG.md +++ b/cmd/crucible/CHANGELOG.md @@ -1,12 +1,18 @@ # Changelog -All notable changes to the crucible CLI are documented here. This module is +All notable changes to the crucible CLI are documented here. The format is based +on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this module +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). It is versioned independently of the `state` module. -## 0.1.0 +## [Unreleased] + +## [0.1.0] - 2026-06-13 Initial release. +### Added + - `lint` runs static analysis over an IR and exits non-zero on findings. - `render` emits a Mermaid or DOT diagram of a machine. - `diff` classifies the changes between two IRs and recommends a semver bump. @@ -14,3 +20,6 @@ Initial release. - `eject` generates typed Go behavior stubs from an IR. - `version` (and `-version`) prints the CLI version. - Commands read an IR file path or `-` for stdin. + +[Unreleased]: https://github.com/stablekernel/crucible/compare/cmd/crucible/v0.1.0...HEAD +[0.1.0]: https://github.com/stablekernel/crucible/releases/tag/cmd/crucible/v0.1.0 diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 16453fd..79fa6ac 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -106,6 +106,12 @@ export default defineConfig({ { label: 'Integrating', items: [{ autogenerate: { directory: 'integrating' } }] }, ], }, + { + label: 'Tooling', + // The headless IR CLI and the eject codegen: reason about and + // scaffold from a machine's serialized IR without running it. + items: [{ autogenerate: { directory: 'tooling' } }], + }, { label: 'Reference', // Generated API reference for every module by `tools/docsgen` diff --git a/docs/src/content/docs/tooling/cli.md b/docs/src/content/docs/tooling/cli.md new file mode 100644 index 0000000..439b101 --- /dev/null +++ b/docs/src/content/docs/tooling/cli.md @@ -0,0 +1,130 @@ +--- +title: The crucible CLI +description: A headless command-line tool over a machine's serialized IR — lint, render, diff, validate, and eject, without running any behavior. +sidebar: + order: 1 +--- + +`crucible` is a headless command-line tool over a machine's intermediate +representation (IR). It lints, renders, diffs, validates, and ejects a serialized +IR JSON document without running a single transition. It is the tooling face of +the [serialization split](../../serialization/overview/): structure is data, so a +tool can reason about a machine's shape from the IR alone. + +Every command reads an IR JSON file path, or `-` to read from stdin. So a +machine written in Go becomes a CI gate: emit its IR with `ToJSON`, write it to a +file, and hand that file to the CLI. + +```go +m, err := state.ForgeFor[OrderContext]("order"). + // ... states and transitions ... + Quench() +if err != nil { + return err +} + +ir, err := m.ToJSON() +if err != nil { + return err +} +return os.WriteFile("order.json", ir, 0o644) +``` + +## Install + +The CLI lives in the `cmd/crucible` module, versioned independently of `state`. +Build it from source with the Go toolchain: + +```sh +go install github.com/stablekernel/crucible/cmd/crucible@latest +``` + +Or build a local binary from a checkout: + +```sh +go build -o crucible ./cmd/crucible +``` + +## Behavior-free operation + +A serialized IR carries behavior references by name only; the kernel binds those +names to real implementations when it assembles a machine. The commands that need +an assembled machine (`lint`, `render`, `validate`) do not have the host's real +guards, actions, reducers, and services. They register a deterministic no-op stub +for every referenced name first, so the machine assembles from its structure +alone. The stubs never run — no instance is cast and no event is fired — so the +structural view is exactly what the IR describes. + +## Commands + +### lint + +```sh +crucible lint order.json +``` + +Runs every [static analysis](../../analysis/overview/) check and prints the +findings. Exits non-zero when the analysis reports any finding, so it gates CI. + +### render + +```sh +crucible render order.json -format mermaid +crucible render order.json -format dot | dot -Tsvg -o order.svg +``` + +Renders the machine as a Mermaid `stateDiagram-v2` (the default) or as Graphviz +DOT. `-format` selects between `mermaid` and `dot`; the flag may appear after the +IR path. Output is text — pipe the DOT through Graphviz for an SVG. + +### diff + +```sh +crucible diff order-v1.json order-v2.json +``` + +Classifies the changes between two serialized IRs, prints the recommended semver +bump (`major`, `minor`, or `patch`), and lists the breaking and additive changes +separately. This is the [evolution](../../analysis/evolution/) check on the +command line: treat a machine definition as a schema and let the diff decide the +bump. + +### validate + +```sh +crucible validate order.json +``` + +Confirms the IR loads and assembles cleanly. A malformed JSON document or a +structural defect exits non-zero with a message on stderr; a clean machine prints +`ok: ` and exits zero. It is the well-formedness gate. + +### eject + +```sh +crucible eject order.json -package order -o behaviors.go +``` + +Generates typed Go behavior stubs for every referenced guard, action, reducer, +and service, plus a `Provide` function that registers them against a +`state.Registry`. `-package` sets the generated package name (default `machine`), +and `-o` writes to a file (default stdout). See [Eject](../eject/) for what the +generated file contains and how to fill it in. + +### version + +```sh +crucible version +crucible -version +``` + +Prints the CLI version. + +## Exit codes + +- `0` success +- `1` runtime or load error, and lint findings +- `2` usage error + +A non-zero `lint` or `diff` makes the CLI a drop-in CI gate: a failing analysis +or an unexpected breaking change fails the build. diff --git a/docs/src/content/docs/tooling/eject.md b/docs/src/content/docs/tooling/eject.md new file mode 100644 index 0000000..2dd2785 --- /dev/null +++ b/docs/src/content/docs/tooling/eject.md @@ -0,0 +1,73 @@ +--- +title: Eject codegen +description: Turn a machine's IR into typed Go stubs and a Provide wiring function — proven against the registry before a line of behavior is written. +sidebar: + order: 2 +--- + +The `gen` module turns a machine's IR into typed Go source: one stub per +referenced behavior, plus a `Provide` function that wires them into a registry. +It is the scaffolding step between designing a machine's *shape* and implementing +its *behavior*. The [`crucible eject`](../cli/#eject) command is the CLI front-end +over this module. + +## What it generates + +`gen.Eject` walks a [serialized IR](../../serialization/overview/) and emits a +single gofmt'd Go source file containing: + +- a generated `Context` type synthesized from the IR's context schema — a struct + (one field per schema field, with Go-typed fields and `json` tags) when the + schema declares fields, or a `map[string]any` alias when the schema is absent + or empty; +- one panic-bodied stub per referenced guard, action, assign, and service, each + typed to the exact engine signature with the generated `Context` substituted + for the machine's context type parameter; and +- a `Provide` function that registers every stub against a `state.Registry` by + its original IR name. + +Each stub panics with a TODO until it is implemented, but the file *compiles* and +its `Provide` type-checks against the real registry. The wiring is proven before +any behavior is written: the IR says which behaviors a machine needs, and the +generated file is the typed skeleton a host fills in. + +Output is deterministic. Behavior names are walked across the full state +hierarchy (states, children, regions, transitions, invocations), deduplicated, +and sorted, so ejecting the same IR twice yields byte-identical source. A name +shared across behavior kinds gets a unique, kind-suffixed Go identifier while its +registration string stays the original name. + +## From the command line + +The common path is the CLI, which reads an IR file (or stdin) and writes the +generated source: + +```sh +crucible eject order.json -package order -o behaviors.go +``` + +`-package` sets the generated package clause (default `machine`); `-o` writes to +a file (default stdout). Implement each stub, then call `Provide` to register the +behaviors and assemble the machine. + +## From Go + +`gen.Eject` is the same codegen as a library call. It takes the loaded IR and an +additive tail of options, and returns the formatted source bytes: + +```go +src, err := gen.Eject[string, string, OrderContext](ir, + gen.WithPackageName("order"), + gen.WithContextTypeName("Context"), +) +if err != nil { + return err +} +return os.WriteFile("behaviors.go", src, 0o644) +``` + +The type parameters mirror the machine's `state.IR[S, E, C]`, so a typed +machine's loaded IR passes through without reflection or a wrapper. `WithPackageName` +sets the package clause (default `machine`) and `WithContextTypeName` sets the +generated context type name (default `Context`); both are part of the additive +option tail, so new knobs arrive without breaking the `Eject` signature. diff --git a/gen/CHANGELOG.md b/gen/CHANGELOG.md index 2d0fb4d..0ab1641 100644 --- a/gen/CHANGELOG.md +++ b/gen/CHANGELOG.md @@ -6,7 +6,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## [0.1.0] +## [0.1.0] - 2026-06-13 ### Added diff --git a/state/expr/CHANGELOG.md b/state/expr/CHANGELOG.md index a73060d..7d8347d 100644 --- a/state/expr/CHANGELOG.md +++ b/state/expr/CHANGELOG.md @@ -18,7 +18,7 @@ dependency-free, and `expr` depends on `state`, never the reverse. `Guard[S, C]` directly when the machine's state identifiers are typed (for example enum) values. -## [0.1.0] +## [0.1.0] - 2026-06-13 First release of the rich guard tier. It compiles guard logic written in CEL (the Common Expression Language) against a machine's `ContextSchema`, type-checks