House style: source lines <= ~50 cols so they fit phone preview panes. 2-space indent. If nesting gets deep, that is a design smell — flatten or extract, don't widen.
A machine is a pure object:
machine = {
initial, // starting state
decide(s, action), // -> events[]
evolve(s, event), // -> state
react(s, event), // -> effects[] (opt)
resolve(s, uri), // -> value (opt)
render(s), // -> string (opt)
}
Rules:
- decide is the ONLY judge of validity. Reject a bad action by returning []. No throwing.
- evolve is a pure left-fold. Same log => same state, always. No I/O, no Date.now, no random.
- react asks for EFFECTS; it never performs them.
- effects run in core via
processors[type], and whatever they return re-enters as events. - state is a CACHE. The event LOG is truth.
Actions/commands/events must name things that exist IN that archetype's world.
- calculator: KEY -> KeyPressed / Evaluated ...
- store: AddToCart -> ItemAddedToCart ... A "KEY" event in a store is a category error.
Command vs Event:
- ACTION = imperative from the outside (a tap).
- COMMAND = an imperative the system issues to ITSELF (e.g. store: "ReserveInventory"), still goes through decide->events. Model it when an event must trigger further internal intent.
- EVENT = a fact that already happened (past tense names: ItemAddedToCart, not AddItem).
Local: :// calc://value Global: aces:///// aces://ren/calc/main/value World layer (world.js) routes to the instance; the machine resolves the trailing field.
Build <name>.js exporting { machine } to this
contract. Entities own their vocabulary. Events
past-tense. Optional render(state) in the same
ascii-kit spirit as ui.js (atoms -> screen). Keep
every source line <= ~50 cols. Do NOT edit
core.js — if you must, the seam is wrong.