Crucible State Machine — Path Planning #4
joshua-temple
started this conversation in
State Machine
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
PlanPathanswers a simple question: what is the shortest sequence of events that moves an entity from one state to another? It walks the transition graph with breadth-first search and returns the event sequence.Signature & contract
Determinism:
From == To) are skipped during expansion.Purity:
dryRunprimitive.Edge cases:
from == toreturns([]E{}, nil)— a zero-step path is legal.fromortoreturns*ErrUndeclaredState.*ErrNoPathwith the first guard-blocked event and the furthest state reached along the frontier.How it works
The BFS frontier tracks
(state, entity-clone, path)tuples. For each outbound transition, the planner callsdryRun, which clones the entity, evaluates the transition's guards on the clone, and — on success — returns the destination state plus the post-transition clone so the next step's guards see any mutations the path accumulated.flowchart TD Start[from state, entity clone] --> Q{frontier non-empty?} Q -->|dequeue| Cur[current node] Cur --> Hit{state == to?} Hit -->|yes| Done[return path - shortest by edge count] Hit -->|no| Exp[expand outbound transitions<br/>sorted by event name] Exp --> Dry[dryRun each: clone + eval guards] Dry -->|guard passes| Enq[enqueue dest + cloned entity] Dry -->|guard fails| Block[record blocked edge in diagnostics] Enq --> Q Block --> Q Q -->|empty| NoPath[return ErrNoPath with closest + blocked-at]Cloning
The entity is cloned before each simulated transition so the caller's entity is never touched. Because most context types are pointers, the builder author supplies a cheap, domain-specific clone function:
A reflection-based deep copy was rejected (slow, brittle on cyclic graphs and unexported fields); requiring a
Clonerinterface on every entity was rejected (too invasive). The builder-supplied function keeps reflection out of the hot path, lets the author write the cheapest copy, and fails loudly at.Quench()if aPlanPath-using machine forgot to declare it.Why visited-by-state, not visited-by-(state, entity)
Guards in v1 are pure predicates on the entity, and entity shape evolves monotonically along a path (the planner only ever adds setter outputs). So the first time BFS reaches a state via the shortest path, the entity is as filled-in as it can be for that path. Keying the visited-set by state alone keeps the search space small and the result deterministic. The monotonicity assumption is documented and covered by a property test.
ErrNoPath diagnostics
The error distinguishes "structurally unreachable" (no edge exists) from "blocked by a guard" (an edge exists but its guard failed), and names the closest state reached — turning a planning failure into an actionable diagnostic instead of a bare nil.
Use cases
events, _ := Machine.PlanPath(Draft, Published, doc)then fire each in turn.ErrNoPathtells you exactly where it broke.PlanPath, which a user then edits (see the Phase 2 Visualizer discussion).On HSM
PlanPathwalks a graph the builder has already expanded — superstate-level (cross-cutting) transitions are exploded into per-substate edges at build time. The BFS itself stays HSM-unaware, so path planning and the HSM layer compose without either needing to know about the other.Crucible State Machine series: Overview & Roadmap · Kernel Core · HSM · Path Planning · JSON / Mermaid / DOT · Evolution Guide · Conformance · Phase 2
Beta Was this translation helpful? Give feedback.
All reactions