@typeonce/effect-machine 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -1,5 +1,3 @@
1
- This package incubates the Machine work proposed in Effect PR #6429.
2
-
3
1
  Portions are adapted from the Effect project, which is distributed under the
4
2
  MIT License. See https://github.com/Effect-TS/effect and the source history for
5
3
  authorship and provenance.
package/README.md CHANGED
@@ -1,10 +1,6 @@
1
1
  # @typeonce/effect-machine
2
2
 
3
- External home for the schema-first Machine API proposed in
4
- [Effect PR #6429](https://github.com/Effect-TS/effect/pull/6429). During
5
- incubation this repository is the canonical implementation; the synchronization
6
- tool keeps the Effect PR mechanically aligned without a second logic
7
- implementation.
3
+ Schema-first state machines and statecharts for Effect.
8
4
 
9
5
  > Early-release software: APIs may change, and releases are coupled to an exact
10
6
  > Effect beta.
@@ -15,93 +11,512 @@ implementation.
15
11
  pnpm add @typeonce/effect-machine effect@4.0.0-beta.102
16
12
  ```
17
13
 
18
- `effect` is an exact peer dependency, not a bundled runtime dependency. Consumers
19
- must install `effect@4.0.0-beta.102`. Upgrading this package may require upgrading
20
- Effect in lockstep; do not override the peer to another beta.
14
+ `effect` is an exact peer dependency, not a bundled runtime dependency.
15
+ Consumers must install `effect@4.0.0-beta.102`. Upgrading this package may
16
+ require upgrading Effect in lockstep; do not override the peer to another beta.
21
17
 
22
18
  ## Entrypoints
23
19
 
24
20
  ```ts
25
21
  import { Machine } from "@typeonce/effect-machine"
26
- import { AtomMachine } from "@typeonce/effect-machine/reactivity"
27
22
  import { ClusterMachine } from "@typeonce/effect-machine/cluster"
23
+ import { AtomMachine } from "@typeonce/effect-machine/reactivity"
28
24
  ```
29
25
 
30
26
  Each ESM entrypoint is independent and tree-shakeable. Importing the root does
31
27
  not load the reactivity or cluster adapters.
32
28
 
33
- ## Basic machine
29
+ ## First machine
30
+
31
+ Schemas provide runtime decoders and the types used by handlers, targets,
32
+ inputs, outputs, and running references. The public/internal event distinction
33
+ has an additional boundary described below.
34
+
35
+ Effect's `Schema.TaggedUnion` is a compact way to declare cases. Its `cases`
36
+ property contains the individual tagged schemas, and each case has a typed
37
+ `make` constructor.
34
38
 
35
39
  ```ts
36
- import { Effect, Schema } from "effect"
37
40
  import { Machine } from "@typeonce/effect-machine"
41
+ import { Schema } from "effect"
38
42
 
39
- class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
40
- class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {}
41
- class Start extends Schema.TaggedClass<Start>("Start")("Start", {}) {}
43
+ const State = Schema.TaggedUnion({
44
+ Idle: {},
45
+ Running: {}
46
+ })
47
+
48
+ const Event = Schema.TaggedUnion({
49
+ Start: {}
50
+ })
42
51
 
43
- const states = Machine.defineStates({ Idle, Running })
52
+ const States = Machine.defineStates(State.cases)
44
53
 
45
54
  const Counter = Machine.make({
46
55
  id: "Counter",
47
- states: states.states,
48
- events: [Start],
49
- initial: states.initial.Idle(new Idle())
56
+ states: States.states,
57
+ events: [Event.cases.Start],
58
+ initial: () => States.initial.Idle.from()
50
59
  }).handle({
51
60
  Idle: {
52
61
  on: {
53
- Start: ({ target }) => Effect.succeed(target.full.Running(new Running()))
62
+ Start: ({ target }) => target.full.Running.from()
63
+ }
64
+ },
65
+ Running: {}
66
+ })
67
+ ```
68
+
69
+ `initial` is always a function. For a machine with an input schema, the
70
+ initializer receives the decoded input.
71
+
72
+ Builder methods accept an already constructed state value directly, or expose
73
+ `.from` for constructing one safely from the state schema's make input:
74
+
75
+ ```ts
76
+ target.local.Running(decodedRunning)
77
+ target.local.Running.from({ startedAt: event.at })
78
+ ```
79
+
80
+ Use the direct call when a decoded value already exists. Use `.from` when
81
+ entering a state from fields. Construction runs through the schema's
82
+ `makeEffect` while the machine plans the configuration, so constructor
83
+ defaults and tagged-class identity are preserved and failed refinements become
84
+ `MachineSchemaDecodeError` failures instead of synchronous throws. The same
85
+ form is available on initial, local, branch, full, compound, parallel, and
86
+ final builders. A `.from` builder result is therefore a machine construction
87
+ instruction; it becomes a validated public snapshot when planning succeeds.
88
+
89
+ When `{}` is valid constructor input, omit it. Required state fields remain
90
+ required, while compound and parallel states still require their active-child
91
+ callback:
92
+
93
+ ```ts
94
+ States.initial.Idle.from()
95
+ States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
96
+ States.initial.Flow.from((flow) => flow.Idle.from())
97
+ ```
98
+
99
+ Tagged classes are equally valid when cases need class methods or nominal
100
+ identity:
101
+
102
+ ```ts
103
+ class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
104
+ ```
105
+
106
+ ## Public and internal events
107
+
108
+ Declare commands that callers may send in `events`. Declare machine-local
109
+ deliveries, such as invoke results and child emissions, in `internalEvents`:
110
+
111
+ ```ts
112
+ const Command = Schema.TaggedUnion({
113
+ Save: {}
114
+ })
115
+
116
+ const InternalEvent = Schema.TaggedUnion({
117
+ Saved: { id: Schema.String },
118
+ SaveFailed: { message: Schema.String }
119
+ })
120
+
121
+ const machine = Machine.make({
122
+ states: States.states,
123
+ events: [Command.cases.Save],
124
+ internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed],
125
+ initial: () => States.initial.Idle.from()
126
+ })
127
+ ```
128
+
129
+ Handlers and machine logic see the complete union. Local public APIs such as
130
+ `MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
131
+ in TypeScript. The local planner and runtime still share the complete event
132
+ decoder so machine-local deliveries can flow through the same execution
133
+ protocol; bypassing the types with JavaScript or `any` is therefore not a
134
+ runtime authorization boundary. Cluster RPC delivery additionally validates
135
+ incoming payloads against the public `events` schemas.
136
+
137
+ The utility types make the distinction available to application code:
138
+
139
+ ```ts
140
+ type PublicCommand = Machine.Machine.InputEvent<typeof machine>
141
+ type HandledEvent = Machine.Machine.Event<typeof machine>
142
+ ```
143
+
144
+ Tags must be unique within each list, and public and internal tags must be
145
+ disjoint. Reusing a tag is a type error, so a command cannot accidentally
146
+ masquerade as an internal result.
147
+
148
+ ## Statechart structure
149
+
150
+ `Machine.defineStates` accepts atomic, compound, parallel, final, and history
151
+ nodes:
152
+
153
+ ```ts
154
+ const State = Schema.TaggedUnion({
155
+ Form: { draft: Schema.String },
156
+ Editing: {},
157
+ Saving: {},
158
+ Done: {}
159
+ })
160
+
161
+ const States = Machine.defineStates({
162
+ Form: {
163
+ schema: State.cases.Form,
164
+ initial: "Editing",
165
+ states: {
166
+ Editing: State.cases.Editing,
167
+ Saving: State.cases.Saving,
168
+ Done: {
169
+ schema: State.cases.Done,
170
+ type: "final",
171
+ output: Schema.String
172
+ }
54
173
  }
55
174
  }
56
175
  })
57
176
  ```
58
177
 
59
- The exported namespaces preserve the API, type identifiers, service keys,
60
- semantics, and documentation of the Effect proposal.
178
+ Compound states have one active child and declare its initial key. Parallel
179
+ states use `type: "parallel"` and have one active state in every direct region.
180
+ Finality is topology, so declare `type: "final"` only in the state definition.
181
+ Handlers implement behavior and output computation without repeating it:
61
182
 
62
- ## Development and validation
183
+ ```ts
184
+ const machine = Machine.make({
185
+ states: States.states,
186
+ events: [],
187
+ initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
188
+ }).handle({
189
+ Form: {
190
+ states: {
191
+ Done: {
192
+ output: () => "saved"
193
+ }
194
+ }
195
+ }
196
+ })
197
+ ```
63
198
 
64
- Use pnpm 10 and Node.js 20 or newer:
199
+ Every declared output schema must have a matching handler implementation before
200
+ the machine can be planned, started, invoked, or adapted to Atom/Cluster.
201
+ Final children complete their parent; put `onDone` on that compound or parallel
202
+ parent, not on the final leaf.
65
203
 
66
- ```sh
67
- pnpm install --frozen-lockfile
68
- pnpm check
204
+ Put data on the narrowest state where it is valid. If several sibling phases
205
+ share data, prefer storing it on their compound parent instead of copying it
206
+ into every child state.
207
+
208
+ ### History states
209
+
210
+ A history pseudo-state remembers the last active configuration of its parent.
211
+ It has no value schema and never appears in an active snapshot. History is
212
+ shallow by default; use `history: "deep"` to retain the complete descendant
213
+ configuration and its validated values:
214
+
215
+ ```ts
216
+ const States = Machine.defineStates({
217
+ checkout: {
218
+ schema: Checkout,
219
+ initial: "shipping",
220
+ states: {
221
+ shipping: Shipping,
222
+ payment: {
223
+ schema: Payment,
224
+ initial: "cardEntry",
225
+ states: {
226
+ cardEntry: CardEntry,
227
+ verifying: Verifying
228
+ }
229
+ },
230
+ resume: { type: "history", history: "deep" }
231
+ }
232
+ },
233
+ support: Support
234
+ })
69
235
  ```
70
236
 
71
- Individual commands are available for `build`, `test`, `test:types`,
72
- `typecheck`, `format:check`, `test:consumer`, `test:sync`, `sync:check`, and
73
- `pack:check`. Runtime tests use `@effect/vitest`; type tests use TSTyche and
74
- TypeScript 6.0.3.
237
+ Implement a typed default for the first transition before any configuration
238
+ has been remembered, then target history without supplying a state value:
239
+
240
+ ```ts
241
+ machine.handle({
242
+ checkout: {
243
+ history: {
244
+ resume: {
245
+ default: () => initialCheckoutSnapshot
246
+ }
247
+ }
248
+ },
249
+ support: {
250
+ on: {
251
+ Resume: ({ target }) => target.history.checkout.resume()
252
+ }
253
+ }
254
+ })
255
+ ```
75
256
 
76
- ## Synchronizing Effect PR #6429
257
+ Deep history restores every remembered descendant value. Shallow history
258
+ restores the parent and direct-child values, then follows normal initial paths.
259
+ Only compound or parallel states that shallow restoration can enter implicitly
260
+ need an `initial` handler to construct those new child values:
77
261
 
78
- Make logic changes here first and run `pnpm check`. Then generate the mapped
79
- production files, runtime tests, and type tests into a writable Effect checkout:
262
+ ```ts
263
+ payment: {
264
+ initial: ;
265
+ ;(({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" }))
266
+ }
267
+ ```
80
268
 
81
- ```sh
82
- pnpm sync:effect -- /path/to/effect
269
+ Execution APIs remain unavailable until required history defaults and shallow
270
+ initializers have been implemented. History records are part of logical
271
+ snapshots and are schema-validated by `encodeSnapshot` and `decodeSnapshot`.
272
+
273
+ Transition between structurally related tagged states with `Machine.retag`.
274
+ The source `_tag` is discarded, compatible fields are reused, and missing or
275
+ incompatible required fields must be supplied:
276
+
277
+ ```ts
278
+ const saving = Machine.retag(State.cases.Saving, editing)
83
279
  ```
84
280
 
85
- The explicit mapping covers only the eight production files and six test files.
86
- It rewrites package imports to Effect repository-relative `.ts` imports and
87
- restores Effect's private `PipeInspectableProto` boundary. It never copies
88
- package metadata, documentation, release files, or changesets.
281
+ ## Choosing a target builder
282
+
283
+ Transition contexts expose four typed target builders:
284
+
285
+ | Builder | Destination | Configuration behavior |
286
+ | ---------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
287
+ | `target.local` | Inside the source's nearest compound scope | Keeps the compound value, active ancestors, and unrelated parallel regions |
288
+ | `target.branch` | Anywhere under the source's active top-level root | Replaces the selected branch while keeping omitted active ancestor values and parallel regions |
289
+ | `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
290
+ | `target.history` | A declared history pseudo-state | Restores its parent's remembered configuration or runs its typed default |
291
+
292
+ When `target.local` or `target.branch` enters an inactive nested parallel
293
+ state, its callback must select every region, just like `initial` and
294
+ `target.full`. When that parallel state is already active, `target.branch`
295
+ can still update one region directly and preserves the other active regions.
296
+
297
+ The builder controls how the next configuration is assembled; it does not by
298
+ itself decide which invokes restart. The runtime derives exit and entry paths
299
+ from the previous and next active paths. Shared active ancestors remain entered,
300
+ even when `target.full` supplies their values again. Use an event transition
301
+ with `reenter: true` when the source state should explicitly exit and enter
302
+ again:
303
+
304
+ ```ts
305
+ Refresh: {
306
+ reenter: true,
307
+ transition: ({ state, target }) =>
308
+ target.full.Ready(new Ready({ value: state.value }))
309
+ }
310
+ ```
311
+
312
+ `States.get`, `States.getWithParents`, `States.getSnapshot`, and
313
+ `States.matches` accept typed dotted paths. Handler `parents` values are also
314
+ keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
315
+ provides the immediate parent directly and is `undefined` at a root state.
316
+
317
+ ## Planning Effects and staged actions
318
+
319
+ An Effect returned by a transition handler is part of planning. Use it to read
320
+ services, choose a target, raise an event, or emit an event. Wrap external side
321
+ effects in `Machine.action`; actions are staged during planning and run by the
322
+ managed runtime before it publishes the next state.
323
+
324
+ ```ts
325
+ const handlers = {
326
+ Save: ({ target }) => Machine.action(writeAuditLog, target.local.Saving.from())
327
+ }
328
+ ```
329
+
330
+ The one-argument form returns `void` after staging. The two-argument form
331
+ returns its second argument, which avoids a generator when an action and the
332
+ next target are the whole transition.
333
+
334
+ If an action fails, the runtime keeps the previously published state and
335
+ suppresses emissions from that plan.
336
+
337
+ `Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
338
+ `done` is `true`, `output` is the schema-derived structural terminal union;
339
+ while the machine remains active, it is `undefined`. A started machine's
340
+ `join` uses the same terminal union. Output-less structural terminal paths
341
+ contribute `undefined`, while active atomic roots do not.
342
+
343
+ This union is intentionally conservative with respect to handler behavior. For
344
+ example, a root `onDone` transition may make one structurally terminal result
345
+ unreachable even though its schema remains in `Machine.TerminalOutput`.
346
+
347
+ ## State-scoped invokes
348
+
349
+ `Machine.invoke` runs child logic while its owning state is active. Leaving the
350
+ state interrupts the child. For a one-shot Effect, `Machine.invokeEffect` maps
351
+ typed success and failure values directly to internal events:
352
+
353
+ ```ts
354
+ const loading = {
355
+ invoke: ({ state }) =>
356
+ Machine.invokeEffect({
357
+ id: "save",
358
+ effect: save(state),
359
+ onSuccess: (entry) => InternalEvent.cases.Saved.make({ id: entry.id }),
360
+ onFailure: (error) =>
361
+ InternalEvent.cases.SaveFailed.make({
362
+ message: String(error)
363
+ })
364
+ })
365
+ }
366
+ ```
367
+
368
+ Omit `onFailure` when the Effect cannot fail. Defects and interruption remain
369
+ failures rather than being mapped.
370
+
371
+ `Machine.after` creates a cancellable, state-scoped delayed event with the same
372
+ lifetime:
373
+
374
+ ```ts
375
+ invoke: Machine.after("3 seconds", InternalEvent.cases.SaveFailed.make({ message: "Timed out" }), {
376
+ id: "save-timeout"
377
+ })
378
+ ```
379
+
380
+ Provide an explicit id when more than one active timer could deliver the same
381
+ event tag.
382
+
383
+ Use lower-level `Machine.invoke` with `Machine.effect` for custom child logic or
384
+ snapshot mapping. Its `id` is only the state-local lifecycle key. If the parent
385
+ must send events to that invocation, create a typed low-level address with
386
+ `Machine.childAddress<Event>("worker")` and pass it through the explicit
387
+ `address` option; the address protocol is checked against the child logic.
388
+ Lifecycle ids must be unique among simultaneously active invokes owned by the
389
+ same state.
89
390
 
90
- Check an existing checkout without writing:
391
+ Invoke outputs, invoke snapshot events, and invoked-child emissions belong in
392
+ `internalEvents`. They are available to typed handlers but are not accepted by
393
+ the typed public input APIs. Include a child machine's emitted protocol with
394
+ `internalEvents: [...ChildMachine.emits]` when those emissions should be handled
395
+ by the parent.
396
+
397
+ For a child statechart, create one descriptor for `invokeMachine`, `sendTo`,
398
+ and child lookup:
399
+
400
+ ```ts
401
+ const Editor = Machine.child("editor", EditorMachine)
402
+ ```
403
+
404
+ `Machine.child(id, machine)` is the complete statechart descriptor;
405
+ `Machine.childAddress<Event>(id)` is the lower-level event-only address.
406
+ Descriptors are matched by id and machine identity, so independently created
407
+ descriptors for the same pair address the same child without a global cache.
408
+ Exporting one descriptor remains the clearest module boundary.
409
+
410
+ ## Reactivity
411
+
412
+ `AtomMachine.make` creates a lazy bridge backed by one running machine per
413
+ `AtomRegistry`. Mounting or reading one of its atoms starts the machine;
414
+ disposing the registry-owned reference stops it.
415
+
416
+ ```ts
417
+ import { AtomMachine } from "@typeonce/effect-machine/reactivity"
418
+ import { Atom } from "effect/unstable/reactivity"
419
+
420
+ const runtime = Atom.runtime(AppLayer)
421
+ const machines = AtomMachine.bind(runtime)
422
+ const machineAtom = machines.make(Counter)
423
+ ```
424
+
425
+ For applications with a shared runtime, treat
426
+ `AtomMachine.bind(runtime).make(...)` as the canonical form. It keeps runtime
427
+ ownership at the composition boundary so it does not need to be passed through
428
+ every feature. Service-free machines may use `AtomMachine.make(machine)`
429
+ directly.
430
+
431
+ The bridge exposes:
432
+
433
+ - `ref`: the running `MachineRef`
434
+ - `result`: fail-aware logical state, combining startup and post-start runtime
435
+ failures
436
+ - `snapshot`: authoritative runtime lifecycle, including `active`, `done`,
437
+ `error`, and `stopped`
438
+ - `state`: the last logical state, including the retained state after a runtime
439
+ failure
440
+ - `send` and `stop`: writable command atoms
441
+ - `child(descriptor)`: a reactive bridge for a directly owned child
442
+
443
+ Use `AtomMachine.select` and `AtomMachine.matches` for equality-aware root
444
+ derivations. Use `selectChild` and `matchesChild` for child bridges. Selector
445
+ paths and selected value types are inferred directly from the bridge snapshot,
446
+ so these combinators do not need the `DefinedStates` object. They follow normal
447
+ Atom identity semantics and return a new atom on each call, so retain or memoize
448
+ them when constructing them in a component. The `child` method uses Effect's
449
+ `Atom.family` to reuse a live bridge for the same descriptor without maintaining
450
+ a package-level cache.
451
+ `AtomMachine.ChildMachineAtom<typeof Child>` uses `unknown` as its startup-error
452
+ default for general component props.
453
+ `AtomMachine.ChildOf<typeof parentAtom, typeof Child>` preserves the exact
454
+ parent startup-error channel.
455
+
456
+ Child state and snapshot atoms contain `Option.none()` while that child is
457
+ inactive. React applications using `@effect/atom-react` need a
458
+ `RegistryProvider`; see the [Pokémon example](./examples/pokemon).
459
+
460
+ ## Snapshots and persistence
461
+
462
+ `Machine.encodeSnapshot` and `Machine.decodeSnapshot` validate logical
463
+ statechart data for storage or transport. The encoded representation does not
464
+ contain the machine definition, machine version, services, subscriptions, or
465
+ running child processes. Store machine identity and migration/version metadata
466
+ alongside it.
467
+
468
+ `ClusterMachine` provides a separate persisted entity adapter. Its process-local
469
+ restrictions and delivery guarantees are documented on that API.
470
+
471
+ ## Current limits
472
+
473
+ Declarative first-class guards are not part of the current API. Ordinary
474
+ TypeScript conditions implement guards. Use `Machine.after` for a cancellable
475
+ state-scoped delayed event.
476
+
477
+ ## Guidance for agents and contributors
478
+
479
+ The shipped [agent guide](./docs/agent-guide.md) contains the recommended
480
+ definition order, modeling rules, lifecycle invariants, React recipe, common
481
+ compiler errors, and unsupported features.
482
+
483
+ ## Development and validation
484
+
485
+ Use pnpm 10 and Node.js 20 or newer:
91
486
 
92
487
  ```sh
93
- pnpm sync:effect -- --check /path/to/effect
488
+ pnpm install --frozen-lockfile
489
+ pnpm check
94
490
  ```
95
491
 
96
- Check mode exits non-zero on any drift. `pnpm test:sync` exercises generation,
97
- a clean check, and drift detection in a disposable temporary directory. Never
98
- generate into a checkout with work you have not reviewed. After generation,
99
- inspect the Effect diff and run its targeted machine, reactivity, cluster, and
100
- type-test validations followed by `pnpm check`.
492
+ Individual commands are available for `build`, `test`, `test:types`,
493
+ `typecheck`, `format:check`, `test:consumer`, and `pack:check`. Runtime tests use
494
+ `@effect/vitest`; type tests use TSTyche and TypeScript 6.0.3. The consumer check
495
+ packs the package, imports all public entrypoints, and compiles a strict
496
+ TypeScript consumer with `skipLibCheck: false`.
497
+
498
+ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
499
+ requests receive an automated base-versus-head type-instantiation report.
101
500
 
102
- The current source reference is branch `sandro/state-charts` in the Effect
103
- repository. Exact dependency pins and the sync check are the proof boundary;
104
- vendoring the rest of Effect is intentionally unnecessary.
501
+ ## Examples
502
+
503
+ The [platformer statechart example](./examples/platformer) is a playable SVG
504
+ demo centered on a schema-first character machine. It demonstrates nested
505
+ compound locomotion, parallel airborne motion and air-jump regions, independent
506
+ facing and wall-contact regions, a pause/resume flow backed by typed deep
507
+ history, typed protocol events, state-scoped timers, and state-driven SVG
508
+ transforms.
509
+
510
+ The [Pokémon statechart example](./examples/pokemon) is a standalone React and
511
+ Vite project demonstrating compound and parallel states, state-scoped invokes,
512
+ invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
513
+ `file:` dependency on this package while retaining an isolated dependency graph,
514
+ lockfile, build, and CI job.
515
+
516
+ The [playground](./examples/playground) collects focused interactive examples
517
+ for traffic lights, turnstiles, media players, microwaves, and worker-backed
518
+ machines. CI discovers every direct package under `examples/` and runs its
519
+ `check` script automatically.
105
520
 
106
521
  ## Releases
107
522
 
@@ -109,9 +524,5 @@ Add a changeset with `pnpm changeset`. CI validates frozen installation and the
109
524
  complete check suite. The release workflow opens version PRs and publishes with
110
525
  npm provenance through GitHub Actions.
111
526
 
112
- Before the first release, create the `typeonce-dev/effect-machine` GitHub
113
- repository and configure npm trusted publishing for the repository and
114
- `.github/workflows/release.yml` environment. No npm token is intended.
115
-
116
527
  When equivalent Machine modules ship in Effect, this package is intended to
117
528
  become a thin compatibility re-export package before eventual retirement.