A structural parameter crosses a durable boundary. It can change the States, transitions, contracts, placements, or lifecycle policies that an instance means, so it must be as deliberate and reproducible as the chart source itself.
Three laws keep that boundary honest:
same chartHashnew version boundaryno chartDecode structural inputs before assembly
Put the accepted authoring vocabulary in a Schema and decode external configuration once:
const ApprovalMode = Schema.Literals(["standard", "escalated"]);
type ApprovalMode = typeof ApprovalMode.Type;
const mode = Schema.decodeUnknownSync(ApprovalMode)(deployment.approvalMode);
const ApprovalChart = makeApprovalChart(mode);
makeApprovalChart receives ApprovalMode, not an unchecked string and a default branch. An
unknown value fails before any Component, placement, or chart exists. A literal selected directly
in source already has the same finite TypeScript vocabulary; decoding matters when the value enters
from a real boundary.
The input must be structural. If changing it does not change the canonical declaration, it is probably runtime data wearing the wrong name.
Determinism is stronger than repeatability
A deterministic factory is a pure function of its decoded authoring input. It does not consult a clock, random generator, process environment, filesystem, mutable collection, service, callback, or secret while deciding which topology to build.
Motive lowers the resulting classes to a Schema-owned canonical declaration. Object-key insertion order and JavaScript class identity do not become chart identity; authored sequences remain ordered because their order has meaning. Given the same decoded value, construction must produce the same declaration on another machine, in another process, and in another factory call.
The witnesses below separate reproducible construction, a real structural change, and ambiguous definition ownership:
Make authored variation reproducible
Structural variation must remain deterministic.
A safe structural input can be rebuilt anywhere and still describe the same chart. Changed authored meaning produces a new identity; competing definition authority produces no chart at all.
1 · Decode before assembly
A structural input enters through one finite Schema.
The factory receives ApprovalMode, never an unchecked string plus a fallback. Its result depends only on that decoded value: no services, clocks, randomness, secrets, or mutable process state participate in topology construction.
import { Component, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Approve extends Schema.TaggedClass<Approve>()("Approve", {}) {}
const ApprovalMode = Schema.Literals(["standard", "escalated"]);
type ApprovalMode = typeof ApprovalMode.Type;
const makeApprovalChart = (mode: ApprovalMode) => {
const Approval = Component.make(() => {
class Approved extends State.Atomic<Approved>()("Approved") {}
class Escalated extends State.Atomic<Escalated>()("Escalated") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class StandardPending extends State.Atomic<StandardPending>()("StandardPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class EscalatedPending extends State.Atomic<EscalatedPending>()("EscalatedPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Escalated),
]);
}
class Approval extends State.Compound<Approval>()("Approval") {
static states = States.make(() =>
mode === "escalated"
? [EscalatedPending, Escalated, Approved]
: [StandardPending, Approved],
);
}
return Approval;
});
class OrderFlow extends State.Compound<OrderFlow>()("OrderFlow") {
static states = States.make(() => [OrderApproval]);
}
class OrderApproval extends Approval.as<OrderApproval>()("OrderApproval") {}
return Statechart.make(OrderFlow);
};
const mode = Schema.decodeSync(ApprovalMode)("standard");
export const StandardChart = makeApprovalChart(mode);
2 · Rebuild the same authored world
Fresh class trees can still describe one chart identity.
Both calls construct new JavaScript classes from escalated. Motive lowers them to the same canonical declaration, so they produce the same chartHash. Source object identity and factory call order are not durable identity.
import { Component, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Approve extends Schema.TaggedClass<Approve>()("Approve", {}) {}
const ApprovalMode = Schema.Literals(["standard", "escalated"]);
type ApprovalMode = typeof ApprovalMode.Type;
const makeApprovalChart = (mode: ApprovalMode) => {
const Approval = Component.make(() => {
class Approved extends State.Atomic<Approved>()("Approved") {}
class Escalated extends State.Atomic<Escalated>()("Escalated") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class StandardPending extends State.Atomic<StandardPending>()("StandardPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class EscalatedPending extends State.Atomic<EscalatedPending>()("EscalatedPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Escalated),
]);
}
class Approval extends State.Compound<Approval>()("Approval") {
static states = States.make(() =>
mode === "escalated"
? [EscalatedPending, Escalated, Approved]
: [StandardPending, Approved],
);
}
return Approval;
});
class OrderFlow extends State.Compound<OrderFlow>()("OrderFlow") {
static states = States.make(() => [OrderApproval]);
}
class OrderApproval extends Approval.as<OrderApproval>()("OrderApproval") {}
return Statechart.make(OrderFlow);
};
const mode = Schema.decodeSync(ApprovalMode)("escalated");
export const FirstEscalatedChart = makeApprovalChart(mode);
export const SecondEscalatedChart = makeApprovalChart(mode);
export const sameModeHasSameHash = FirstEscalatedChart.chartHash === SecondEscalatedChart.chartHash;
3 · Change authored meaning
Different geometry creates a new version boundary.
Standard moves directly from Pending to Approved. Escalated inserts a real Escalated state and transition. That authored difference changes chartHash; it does not silently reinterpret instances committed under the earlier chart.
import { Component, State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
export class Approve extends Schema.TaggedClass<Approve>()("Approve", {}) {}
const ApprovalMode = Schema.Literals(["standard", "escalated"]);
type ApprovalMode = typeof ApprovalMode.Type;
const makeApprovalChart = (mode: ApprovalMode) => {
const Approval = Component.make(() => {
class Approved extends State.Atomic<Approved>()("Approved") {}
class Escalated extends State.Atomic<Escalated>()("Escalated") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class StandardPending extends State.Atomic<StandardPending>()("StandardPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Approved),
]);
}
class EscalatedPending extends State.Atomic<EscalatedPending>()("EscalatedPending") {
static transitions = Transitions.make(this, ({ on }) => [
on(Approve, Escalated),
]);
}
class Approval extends State.Compound<Approval>()("Approval") {
static states = States.make(() =>
mode === "escalated"
? [EscalatedPending, Escalated, Approved]
: [StandardPending, Approved],
);
}
return Approval;
});
class OrderFlow extends State.Compound<OrderFlow>()("OrderFlow") {
static states = States.make(() => [OrderApproval]);
}
class OrderApproval extends Approval.as<OrderApproval>()("OrderApproval") {}
return Statechart.make(OrderFlow);
};
export const StandardChart = makeApprovalChart("standard");
export const EscalatedChart = makeApprovalChart("escalated");
export const changedModeChangesHash = StandardChart.chartHash !== EscalatedChart.chartHash;
4 · Keep one definition authority
Equivalent names do not make definitions interchangeable.
Repeated placements must reuse one Component definition. Two separately minted definitions both named Approval are competing authorities, so chart assembly refuses them and reports the two claiming placement paths.
import { State, Statechart, States } from "@motive/motive";
import { Approval as FirstApproval } from "./first-approval.ts";
import { Approval as SecondApproval } from "./second-approval.ts";
class CollisionHost extends State.Compound<CollisionHost>()("CollisionHost") {
static states = States.make(() => [FirstSite, SecondSite]);
}
class FirstSite extends FirstApproval.as<FirstSite>()("FirstSite") {}
class SecondSite extends SecondApproval.as<SecondSite>()("SecondSite") {}
export const duplicateComponentNameDiagnostic = (() => {
try {
// @ts-expect-error ComponentNamesAreChartUnique
Statechart.make(CollisionHost);
return "Unexpectedly accepted";
} catch (cause) {
return cause instanceof Error ? cause.message : "Component name collision refused";
}
})();
Read chartHash as a version boundary
chartHash fingerprints the canonical authored identity: State geometry, transition structure,
accepted data contracts, placements, occurrence policies, Resource topology, lifecycle policy, and
authored State documentation all contribute. The declaration's transport-format stamp does not.
Runtime facts do not belong in that fingerprint. Instance input, current State data, event values, Resource material, service implementations, and the current time may differ between instances that share one chart.
This makes a hash useful operationally. Committed facts can name the exact geometry that interpreted them rather than whichever classes happen to be loaded now. It also means a different hash is only evidence of a new authored version. It does not upgrade or reinterpret an existing instance; the runtime must register, route, and—when state must cross versions—migrate that instance explicitly.
One Component name has one definition authority
Repeated placement reuses one Component value. Each placement gains a different qualified path,
but every path points back to the same definition authority.
Two separately minted definitions are different authorities even when both roots are named
Approval and their current geometry happens to be equivalent. They may be built in separate
charts, where identical canonical declarations can produce the same hash. They may not both appear
in one chart under the same Component name. Statechart.make refuses
ComponentNamesMustBeChartUnique and reports the two claiming placement paths.
That distinction prevents a name from becoming an accidental merge key. To place one definition twice, reuse the same Component. To place two variants together, give them distinct stable names. To select exactly one variant, make the canonical choice before assembly.
Keep runtime variation out of the factory
Do not parameterize topology with information that belongs to a running instance:
- confidential values and large external documents belong behind Resource boundaries;
- user choices and commands belong in decoded events;
- changing business data belongs in State facets or chart input;
- repeated entities belong in keyed Activity, Timer, child, or Resource families;
- executable side effects belong in the runtime Layer.
Moving those values into a factory would manufacture chart versions from ordinary runtime change, make deployments environment-dependent, or leak sensitive distinctions into observable topology.
Test the identity laws
- Decode every accepted structural input and assemble its exact expected topology.
- Build the same input twice from fresh classes and require equal
chartHashvalues. - Change one authored State, transition, contract, placement, or policy and require a new hash.
- Change only runtime input or State data and require the hash to remain unchanged.
- Place one Component twice and require distinct qualified paths under one definition authority.
- Place two separately minted same-name definitions and require the typed and runtime
ComponentNamesMustBeChartUniquerefusal. - Treat every intentionally changed hash as a deployment and migration review boundary.
Next, Run & Observe follows the authored chart into engine selection, committed behavior, and operational observation.