tech/ directory: Organization & Mental Model for Talos Principle Planning Problems

How the technology files relate to each other, and how to add a new one without breaking what's already there.

Overview

Every technology-assembled Talos problem (e.g. problem-claustro4a.lisp) declares its own leaf object types and instances in a define-types block, then pulls in behavior via a list of (include-tech ...) directives that the stage-time splicer expands in place before translation. Other standalone Wouldwork problems need not use technologies. This document is a map of what lives in tech/, why it's organized the way it is, and a checklist for integrating a new object type so that additions stay consistent with what's already there instead of quietly drifting from it.

The core idea: a role system, not a type hierarchy

A leaf object type (box, gate, jammer, ...) is inert until it acquires roles. A role is one shared composite type plus the single relation that makes it useful — "has a location," "can be held," "can rest on / be rested on," "has a fixed position," "has a height." Each role has exactly one canonical owner file. A technology file's job is almost always one of a small number of things: own a role, own a derived-fact service, or assemble a leaf type by nesting whichever roles it needs and adding whatever is genuinely unique to that object.

Every (either ...) declaration in tech/ is written in leaf-normal form: its members are base types, never other composites. Named composites remain useful as relation, query, update, and action parameter types, but do not serve as shorthand inside another either. This makes the complete membership of each role visible at its declaration and keeps changes to one role from silently extending another. Wouldwork alphabetizes the member names when it synthesizes an inline composite's unique label, so source order does not affect that identity.

Filename visibility is independent of those kinds. A bare filename such as gate.lisp or walkability.lisp is a public, problem-facing technology. A dash-prefixed filename is a private component normally nested by another technology. The dash says nothing about whether that component is a role, default hook interface, geometry derivation, initialization-check companion, solution service, or action-bearing composite; its descriptive name and header state that responsibility. Characterization problems may include a private component directly when they are deliberately testing its contract, but ordinary problem include lists contain only public names.

Initialization semantics follow the same ownership rule. A technology registers a define-init-check over the complete raw define-init form; deliberate authoring failures use fail-init-check, so the staging error names the owning check. Larger clusters keep their Lisp helpers in a nested -*-init-checks.lisp companion. Those helpers are problem-local and are removed at the next stage, just like queries and updates. Optional (:consumes ...) metadata identifies object types consumed only inside untyped list values, keeping inert-type diagnostics accurate without teaching the engine Talos names.

Vocabulary

KindOwnsExamplesRule of thumb
role One composite either-type + its one relation (occasionally one helper query) -location.lispmobile-object/has-location;
-support-occupancy.lispsupport + support-occupant/on/cleartop
Nest it, never hand-copy it. Exactly one owner per role, full stop.
composite substrate 2+ tightly-coupled roles, plus the actions and driver updates that only make sense bundled with them -gears-fan.lisp → mountable floor/wall/angled gears, fixed floor/wall/angled blowers, removable fan mounting, turning/blowing, pickup-fan/put-fan/mount-fan Nest-only like a role — never in a problem's own include-tech list — but permitted to carry actions because the dash marks privacy, not structural simplicity. The exception, not the default shape for a new role; see checklist item 3.
service Capability-specific relations and derived queries built on top of roles, but no object-role composite of its own walkability → walking traversal segments; stairs/ladder/jump → their traversal segments; reachabilityreachable; visibilityvisible Owns the authored edges or sightlines for one capability. Movement services contribute pure providers; central substrate actions apply their results.
assembly A leaf type (declared optional) + whichever roles it nests + its own bespoke relations/queries/actions box, plate, jammer, gun, ladder Where actions live. Roles are ingredients you assemble, not code you retype.
hub An assembly whose derived fact every service also reads gate — its (open gate) fact is read by walkability, reachability, visibility, and every beam file Expect it to be the most cross-referenced file in the directory; changes here ripple widest.
peer-plugin 2+ assemblies sharing one role file that also ships null-object default hooks the peers override beam-direct / beam-relay / beam-crossing over -beam-substrate Use only when 2+ interchangeable variants need to override specific behavior slots — not for a plain shared relation.

The tier picture

Stacking the vocabulary above by what depends on what gives three tiers. Substrate roles sit at the bottom; capability services sit in the middle and consume roles; object & behavior assemblies sit on top and consume both. The one wrinkle: gate is an assembly by ownership, but every service reads its open fact, so there's a real dependency arrow running back up from the service tier into the object tier.

Object & behavior technologies
box, plate, gate, gun, jammer, ladder, repeater, floor-gears, floor-blower, wall-blower, angled-blower, step, beam-direct/relay/crossing — own a leaf type (or composite), plus their own queries and actions
↑↓
Capability services
walkability, reachability, visibility — derive facts from roles; read gate's open and (walkability only) -threat's safe
Private components (nested-only)
-location, -location-coordinates, -holding, -position, -height, -elevation, -support-occupancy, -support-elevation, -placement, -pickup, -gate, -controls, -gears-fan, -floor-blowing, -passability, -stream-passability, -walkability, -walkability-coordinates, -mobility, -mobility-action, -configuration-transition, -reachability, -visibility, -beam-substrate, -beam-los-coordinates, -beam-crossing-coordinates, -threat, and the -recorder-* core/shadow components — mostly roles and internal services. -gears-fan is a composite private component that also bundles actions (see Special cases)

File inventory

What every file in tech/ currently owns, what it nests, and what kind it is. Kept up to date as the directory evolves.

FileOwnsNestsActionsKind
-propagation.lisppropagate-changes! (the fixpoint loop, final — identical in all seven problems that used to transcribe it) and a sentinel propagate-consequences! whose body signals if run. init's install-derived-propagation-driver replaces the sentinel with the order derived from the loaded technologies; a problem that authors its own driver overrides it at load and is left alonerole (nested by all eleven driver-contributing files: -beam-substrate, -gears-fan, -floor-blowing, -threat, angled-blower, beam-crossing, beam-relay, gate, gun, plate, wall-blower — omitted from their Nests columns below, since it is universal among them)
-physical-init-checks.lispRaw initialization consistency for location, holding, support occupancy, position, and on cyclesrole (nested by the four physical role owners)
-stream-passability-init-checks.lispCoordinate-derived air-stream and width consistencyrole (nested by -stream-passability)
-beam-substrate-init-checks.lispRepeater mounting/coordinates, fixed coupling, chroma, corridor, and beam obstacle-list checksrole (nested by -beam-substrate)
-controls-init-checks.lispControls DNF list contents and supported modesrole (nested by -controls)
-beam-relay-init-checks.lispConnector pairing placement, limits, acyclicity, and potential sightlinesrole (nested by beam-relay)
-beam-crossing-init-checks.lispAuthored beam/crossing endpoints, indexes, lists, reverses, sightlines, and gate prefixesrole (nested by beam-crossing)
-recorder-init-checks.lispRecorder mapping, exhaustive loose-cargo copying, located-object completeness, recording-side isolation, and supported-shadow boundary checksrole (nested by recorder)
-recorder-core.lisprecorder, connector, and tray (optional); directional functional recording-copy>; dynamic recording-in-progress (the session flag -recorder-session sets/clears); live/ghost/same-side identity; recorder-aware symmetry coupling; recording-view object presence; and interaction-policy overrides. object-manipulation-allowed and connector-pairing-allowed require recording-in-progress for a ghost actor. support-use-allowed normally isolates mobile supports by side but implements rule 19's directional exception for a live occupant on a ghost-held tray.-location, -holding, -position, -interaction-policy, -recording-shadow-policyrole (identity and isolation core; no apparatus state)
-recorder-plate-shadow.lisprecording-depressed, recording-latched, recording-plate-occupied, and update-recording-plate-status!-recorder-core, -support-occupancyrole (capability-specific recorder shadow)
-recorder-receiver-shadow.lisprecording-active and update-recording-receiver-status!, consuming the beam substrate's recording-arrival hooks-recorder-core, -beam-substraterole (capability-specific recorder shadow)
-recorder-controls-shadow.lisprecording-controller-energized and recording-control-on, aggregating recording plate and receiver state-controls, -recorder-plate-shadow, -recorder-receiver-shadowrole (shared recording control service)
-recorder-jamming-shadow.lisprecording-jammed, filtering shared jamming facts to mapped ghost jammers-recorder-corerole (shared recording jamming service)
-recorder-gate-shadow.lisprecording-open, the recording gate-view hook override, and update-recording-gate-status!-recorder-controls-shadow, -recorder-jamming-shadow, -gaterole (capability-specific recorder shadow)
-recorder-wall-gears-shadow.lisprecording-turning, the recording gears-view hook override, and update-recording-gears-status!-recorder-controls-shadow, -recorder-jamming-shadow, -recording-shadow-policyrole (capability-specific recorder shadow)
-location.lispmobile-object, has-locationrole
-location-coordinates.lisplocation-coords>role
-holding.lispcargo, holdingrole
-position.lispfixed-position-object (pressure-plate/toggle-plate/ladder/gears/fixed-blower leaves/recorder), has-positionrole
-interaction-policy.lispNeutral object-manipulation-allowed, support-use-allowed, and connector-pairing-allowed hooks; -recorder-core overrides them without duplicating actionsrole (hook/interface variant)
-recording-shadow-policy.lispNeutral hooks selecting recording-shadow actors, recording-side object presence, wall-gears turning, and gate openness; ordinary objects use playback state, while -recorder-core and the matching apparatus shadow components override the hooks for ghostsrole (hook/interface variant)
-height.lispheighted-object, has-height (declared $rational; including connector, repeater, and edge), query declared-height (gate/screen/wall default 4, edge 3/2, agent 3/2, box/jammer/connector/repeater 1). Height follows the mounting axis: vertical for a floor repeater, horizontal projection for a wall repeaterrole
-support-occupancy.lispsupport, support-occupant, on, query cleartop-interaction-policyrole
-support-elevation.lispParameter *vertical-reach-limit* (default 1; also reused by jump.lisp); queries support-top-elevation/tray-top-elevation/occupant-elevation/within-agent-vertical-reach-support-occupancy, -location, -position, -height, -elevation, -holdingrole
-configuration-transition.lispDerived (location ground-or-support) agent configurations, a registry for pure support-changing providers, validation/deduplication/canonical selection by destination configuration, and the sole shared support/location mutation-support-occupancy, -location, -propagationrole (explicit state-changing boundary; never transitively closed)
-placement.lispQueries placement-choice-allowed/placement-options (legal same-policy plate/fan/box/ground placements for a carried object; mounting a fan on gears is -gears-fan's own mount-fan instead, and a fan is offered only while floor-mounted — a loose fan is mere cargo, and a wall-mounted fan has no has-location) and placement-elevation; update place-held-object!-support-elevation, -holdingrole
-pickup.lispQuery pickup-clear (policy-compatible actor/object, agent empty-handed, object's location reachable, object's elevation within vertical reach)-placement, -reachabilityrole
-gate.lispgate (optional), open — asserted only by gate.lisp's update-gate-status! — and actor-aware gate-open-for-object, which selects ordinary or recording-shadow state-recording-shadow-policyrole
-controls.lispOptional gate, gears, fixed-blower, receiver, and gun leaves; controls uses a DNF OR-list of AND-lists of receiver/plate controllers over those devices (mode normal | inverted); queries energized and control-on provide the shared aggregate-beam-substraterole
-gears-fan.lispOptional removable fan, mountable floor-gears/wall-gears/angled-gears, and fixed combined floor-blower/wall-blower/angled-blower leaves; gears and blower unions; aimed-at, removable-only mounted-on, and derived turning/blowing; queries blower-drive, blower-present, blower-turning-for-object, blower-active-for-object, blower-elevation, and landing-support; update-blower-status!, relocate-stack!, and land-on-support!. Fixed blowers have no separate cargo fan identity; floor and angled fixed blowers directly expose their flush support surface.-support-occupancy, -location, -position, -elevation, -controls, -placement, -reachability, -pickup, -recording-shadow-policypickup-fan, put-fan, mount-fancomposite substrate (peer substrate for the three mounting technologies)
-floor-blowing.lisplocation-elevation gives undeclared floor-stream destinations hover elevation 10; update-floor-blowing-status! launches occupants from either a mounted fan or a fixed floor-blower support and drops hovering occupants when no matching stream remains-gears-fanrole (shared floor-directed physics; nested only by floor-gears and floor-blower)
-passability.lispOptional screen, ladder, gears, and fixed-blower leaves; queries obstacle-clear/all-clear, with an actor-aware default for stream-obstacle-clear-holding, -gaterole
-stream-passability.lispstream-width (optional per wall-gears or wall-blower override); actor-aware stream-obstacle-clear blocks on either an active mounted fan or a fixed wall blower, and walkability-coordinates-stream-specs gathers both kinds of wall stream-passability, -gears-fan, -walkability-coordinatesrole (nested only under wall-blower)
-walkability.lispwalk-via/walk-via> topology and the canonical antichain algebra for DNF route families that -walkability-coordinates' zone-graph derivation usesrole (walking topology substrate)
-mobility.lispPure traversal-provider registry and canonical breadth-first closure over normalized (mode source witness destination) segments; returns one shortest deterministic route per grounded destinationrole (transparent grounded travel only)
-mobility-action.lispNormalizes transparent grounded routes and singleton support transitions as routes between agent configurations, applies one result through the shared support/location mutation, and records the complete semantic route outside the proposition database-mobility, -configuration-transitionmoverole (central movement action)
-reachability.lispIdentity-default reachable queryrole (hook/interface variant)
-visibility.lispapparatus (transmitter/receiver/floor-repeater/wall-repeater/gun), optional gate/transmitter/receiver/floor-repeater/wall-repeater/gun leaf types, null-default ordinary, beam, and endpoint-elevation-aware visibility queriesrole (hook/interface variant; apparatus are functional points with no has-position or occupancy shortcut)
-threat.lispthreat (either gun; extensible), (threatens threat location) (static, one authored fact per location a threat endangers while lethal), (lethal threat) (dynamic; asserted only by each threat technology's own status update), query safe (true unless some lethal threat threatens the location), update enforce-threat-safety! (the general inconsistent-state backstop: drops the whole state if any agent's current location isn't safe, however it got there -- catches blower launches and remote arming that no precondition can see coming)role (nested by mobility providers and support-changing jump transitions for destination checks, and unconditionally by gun.lisp -- and every future threat technology -- so the backstop update is present whenever any threat exists, regardless of which relocation technologies the problem includes. Deliberately owns driver logic because only a file present under every combination guarantees the invariant)
-beam-occlusion.lispbeam-blocker composite (either agent box jammer connector), elevation-aware ordinary and recording-view blocker queries, and beam-blocker-spans-elevation; the recording view filters blockers through the recording-shadow presence policy-support-elevation, -height, -location, -recording-shadow-policyrole (factored out of beam-direct; also nested by visibility once its sightline occluders gain location entries)
-beam-substrate.lisprepeater/fixed-beam-source/fixed-beam-sink/beam-node leaf-normal composites, directional coupled, beam-via, active, has-chroma, ordinary and recording-shadow receiver-arrival/corridor hooks, update-receiver-status!role (hook/interface variant)
-beam-interpolation.lispDefault beam-elevation-at-location hook: horizontal fixed beams need no coordinates; sloped beams require the coordinate-aware override supplied by public visibility-beam-substraterole
-segment-geometry.lispOptional wall/edge/gate/window/screen, individually keyed segment coordinate facts, ordered boundary-wall, gathering queries, and shared initialization validationrole (shared by walking and LOS coordinate derivation)
-beam-los-coordinates.lispOptional floor-repeater/wall-repeater/jammer/gun, los-endpoint, apparatus functional-point coordinates, coordinate-derived sightlines, and oriented static barrier-crossing records for runtime height checks-location-coordinates, -segment-geometryderive-los-from-segments (init)role (nested under visibility and -beam-crossing-coordinates)
-beam-crossing-coordinates.lispOptional crossing type, left permanently empty; mints one crossing per computed intersection and derives crossings-along-beam>/beam-crossings-before-gate> from LOS geometry and every authored fixed coupled beam, including repeater links-beam-los-coordinatesestablish-beam-coordinates, derive-beam-crossings-before-gaterole (nested only under beam-crossing)
-walkability-coordinates.lispDerives walk-via/walk-via> by region connectivity from the shared individual segment facts and boundary: a coordinate-compressed arrangement of the segments, boundary, and derived air-stream bands; zones flood-filled across open intervals; all subset-minimal door-sets per zone pair as DNF clause families; stream destinations emitted with directional walk-via> ride edges (inbound widened by side-curtain rides, outbound ordinary). Default walkability-coordinates-stream-specs (no streams) is overridden by -stream-passability-walkability, -location-coordinates, -segment-geometryderive-walk-via-from-segments (init)role (nested under walkability and -stream-passability)
-elevation.lispelevated-object (including finite barriers, guns, and repeaters), has-elevation, and anchor queries. Transmitter/receiver/gun anchors default to 1; a floor repeater's base defaults to 0 and its anchor is base + height; a wall repeater's mounting/anchor elevation defaults to 1-heightrole
elevation.lispPublic problem-facing wrapper for -elevation-elevationrole wrapper
walkability.lispwalk-via (symmetric) and walk-via> (directional), both DNF door-clause lists (() direct; else OR over clauses, AND within), walking-traversal-segments, and one-step-walkable-support-occupancy, -location, -passability, -elevation, -walkability, -walkability-coordinates, -threat, -mobility-actionmove (central mobility action)service (walking mobility provider)
stairs.lispSymmetric stairs-via, directional stairs-via>, and stairs-traversal-segments; stairs deliberately allow different endpoint elevations without an ascent bound-passability, -threat, -mobility-actionmove (central mobility action)service (stairs mobility provider)
reachability.lispreach-via, extended reachable, reachable-clear-reachability, -gateservice
visibility.lisplos-to-apparatus, los-to-target, los-to-location, los-barrier-crossings>, ordinary opaque sight, actor-aware height-checked beam and jammer sight, and coordinate-aware elevation interpolation-visibility, -gate, -beam-los-coordinates, -beam-interpolation, -beam-occlusionservice
gate.lispjammer (optional), update-gate-status! (sole asserter of -gate's open; controls/energized now live in nested -controls)-controls, -gatehub
plate.lisppressure-plate/toggle-plate (optional), their plate union, depressed/latched, update-plate-status!-support-occupancyassembly (no actions)
recorder.lispThe problem-facing recorder assembly. It composes identity/isolation, the session lifecycle, plate, receiver, controls, jamming, gate, and wall-gears shadow components with optional recording-prefix pruning, single-cycle solution services, explicit chained-cycle orchestration, and initialization validation. Initialization still refuses capabilities outside that assembled shadow rather than approximating them: beam crossings, floor/angled blowers, threats, movable wall-fan copies, and wall gears controlled by anything but plates-recorder-core, -recorder-controls-shadow, -recorder-jamming-shadow, -recorder-gate-shadow, -recorder-wall-gears-shadow, -recorder-solution, -recorder-session, -recorder-cycle-boundary, -recorder-cycle-chaining, -recorder-init-checksstart-recorder, stop-recorder (via -recorder-session)assembly (composition plus stage-time service registration)
-recorder-solution.lispRecorder path services: optional search-time validate-recorder-recording-prefix pruning; completed-candidate validate-recorder-solution recording/playback validation; build-recorder-report/print-recorder-report two-phase reporting; and the closure queries ghost-stops-recorder, recording-agent-can-close, recording-agent-return-route, and recording-agent-at-recorder. Prefix pruning replays only the recording actions accumulated so far from the captured snapshot. Reads identity, location, position, the mobility closure, and session lifecycle state, but no per-apparatus recording-side relation directly-location, -position, -mobilityrole (nested by recorder; never included directly)
-recorder-session.lispRecording session lifecycle. start-recorder and stop-recorder are real planner actions, not report-only markers: the search itself chooses when a session opens and closes. start-recorder's effect is the fork required by rules 5 and 11 -- every mapped ghost inherits its live counterpart's current has-location, holding, on, and (when the owning tech is included) paired, jamming, and mounted-on state at the exact moment recording starts -- and asserts recording-in-progress, which also gates ghost existence itself (see -recorder-core below). stop-recorder clears the flag. reset-recording-session! clears it again between chained cycles-recorder-core, -recorder-solution, -location, -holding, -support-occupancy, -propagationstart-recorder, stop-recorderrole (nested by recorder; never included directly)
-recorder-cycle-boundary.lispThe boundary contract for chained recorder cycles. recorder-cycle-goal strengthens every cycle goal with ghost-stops-recorder, so report-only return markers can never masquerade as a committed physical state. prepare-recorder-cycle-state copies the accepted integrated playback boundary, runs every capability-owned shadow reset before any seed, and propagates the fresh recording view to consistency-recorder-core, -recorder-solution, -propagationrole (nested by recorder; never included directly)
-recorder-cycle-chaining.lispExplicit one-cycle-per-subgoal orchestration behind the generic interface: solve-subgoal commits one closed intermediate cycle and installs its prepared playback boundary; the following solve commits the original goal as the last closed cycle. Independent history records retain the selected integrated solution, recording/playback report, boundary snapshot, search policy, and per-cycle/cumulative depth, time, and value. ww-undo restores history with the generic session checkpoint-recorder-cycle-boundary; generic goal chainingservice (serial, explicit orchestration; no cross-cycle search)
floor-gears.lispPublic entry point for mountable floor-gears used with removable cargo fans-floor-blowingassembly (removable-fan floor mounting)
floor-blower.lispPublic entry point for fixed combined floor-blower objects-floor-blowingassembly (fixed floor units)
wall-blower.lispupdate-wall-blower-status! sweeps each occupant only when a removable or fixed wall blower is active in that object's playback/recording view; relocation, landing, and stream-height behavior are shared across both representations-gears-fan, -stream-passabilityassembly (wall mounting; stream elevation via blower-elevation, default 1)
angled-blower.lispupdate-angled-blower-status! and arc-occupants-away! launch occupants from either a mounted fan or a fixed angled-blower support along a 45° parabolic arc; land-on-support! then chooses a clear destination support or bare ground. This is a one-shot landing rather than a sustained hover.-gears-fanassembly (angled mounting and fixed angled units; both support surfaces are flush and steppable)
step.lispsteppable-object (either pressure-plate toggle-plate fan floor-blower angled-blower); a fan qualifies only while mounted on gears, while fixed floor and angled blowers expose their own support surfaces; pure local mount/dismount transition provider-mobility-action, -positionmove (central movement action)assembly (flush ground-level support changes)
box.lispplate/box (optional)-placement, -reachability, -pickuppickup-box, put-boxassembly
jump.lispbox/wall (optional), vaultable-object, symmetric jump-via, directed jump-via>, explicit-source elevation/clearance queries, jump-traversal-segments for grounded mobility, and jump-configuration-transitions for support changes-support-elevation, -passability, -threat, -mobility-actionmove (grounded routes and support changes)assembly (barrier-clearing mobility plus explicit support transitions)
jammer.lisptarget includes gates, floor/wall gears, fixed floor/wall blowers, and guns; a jam disables a blower drive regardless of whether its fan is removable or built in-placement, -reachability, -visibility, -pickuppickup-jammer, jam-targetassembly
gun.lispupdate-gun-status! (sole asserter of -threat's lethal for gun instances; lethal <=> (uncontrolled OR control-on) AND NOT jammed, mirroring -gears-fan's turning derivation)-threat, -controlsassembly (no actions and zero physical height; positioned by its apparatus-coords> functional point like a transmitter/receiver, not has-position -- nothing can occupy that point; a jammer targets it through visible/los-to-apparatus)
ladder.lispladder (optional), directed climb-via>, exact source-position and means-membership queries, and ladder-traversal-segments mobility provider-position, -passability, -threat, -mobility-actionmove (central mobility action)service (directed ladder mobility)
beam-direct.lispFixed coupled-corridor clearance, initialization-time wall/edge/gate/boundary crossing records, runtime endpoint-height clearance, direct transmitter→receiver arrival, recording-shadow receiver arrival, and crossing-liveness hooks-beam-substrate, visibilityderive-fixed-beam-barrier-crossings (init)peer-plugin
beam-relay.lisprelay/terminus composites, connector-first paired, derived color, unified update-relay-status!, actor-aware relay visibility/clearance, recording-shadow receiver arrival, and relay/crossing hooks-beam-substrate, -placement, -visibility, -support-elevation, -elevation, -walkability, -reachability, -pickuppickup-connector, put-connector, connect-connectorpeer-plugin (connector actions plus connector/repeater propagation)
repeater.lispPublic fixed-relay assembly and its floor/wall mounting contract; no location or actionsbeam-direct, beam-relay, visibilityassembly
beam-crossing.lisptransmitter (optional), crossing-active, beam-crossing>, current-beam-crossings, get-current-beam-crossings (the query every crossing doall iterates, since the crossing type extension stays empty), update-crossing-status! + hook overrides-beam-substrate, -beam-crossing-coordinates, -gatepeer-plugin

Worked example: box

box.lisp doesn't own mobile-object, cargo, support/support-occupant, heighted-object, fixed floor elevation, or support elevation — it nests the role files that do and adds only box pickup and placement. Jumping is a separate capability in jump.lisp, which may use box tops as landings without making box manipulation own locomotion. That's the pattern to imitate for any new movable, stackable object: figure out which existing roles it needs, nest those files, and write only the bespoke remainder.

Special cases

recorder — explicit mapping, not a naming convention

-recorder-core.lisp identifies live and playback-ghost objects through authored recording-copy> facts. The relation is directional and functional from live to ghost; initialization validation also rejects repeated ghost targets, overlap between the two sides, self-mapping, and cross-category pairs such as an agent mapped to a connector. Every cargo instance must be the live or ghost endpoint of exactly one mapping, whether or not a test happens to use that copy. A fixed combined blower is shared apparatus rather than a cargo fan, as are plates and other fixed objects. Every mobile object carrying a has-location fact must also be mapped. Consequently only mapped objects acquire a recording side, and neither starred symbol names nor problem-local real/ghost composite types carry semantic weight.

Symmetry treats each recording-copy> tuple as one ordered row. Rows with the same complete static role may exchange, but the live and ghost columns never exchange independently and a live-only permutation is invalid. State canonicalization applies one row permutation to both columns and includes every dynamic occurrence, including objects stored in fluent values and recording-shadow facts. Shared apparatus is outside those rows. Goal constants, transition constants, identity-sensitive action parameters, and any static fact that a proposed row exchange would change remove the affected objects from usable symmetry.

Shared actions remain single definitions. They call the neutral hooks from -interaction-policy.lisp; -recorder-core overrides those hooks so pickup, mounting, standing, stacking, jumping, physical landings, and ordinary carried placement keep mapped movable objects on their own layer. Rule 19 supplies one directional support exception during playback: a live occupant may be placed on a ghost tray while a ghost is holding it. A grounded ghost tray remains inert, ordinary ghost mobile supports remain insubstantial to live occupants, and a ghost occupant still cannot depend on a live support introduced during playback. Plates and fixed beam apparatus stay shared. A live connector may select a mapped ghost connector as a terminus during playback, but a ghost connector cannot depend on a live movable connector that was absent during recording. A ghost additionally cannot act at all until recording-in-progress is true (rule 5): a live actor is unrestricted by session timing, but object-manipulation-allowed and connector-pairing-allowed both refuse a ghost actor before the session opens.

The public recorder.lisp assembles private capability-specific components and, at the end, installs the complete recorder solution policy. The core owns only mapping, object presence, and cross-layer interaction policy; plate, receiver, controls, jamming, gate, and wall-gears files each own their recording-side state and derivation. To support another capability, add one focused -recorder-*-shadow.lisp component, give it the matching state/update and actor-aware hook, include it from recorder.lisp, then remove that capability's initialization rejection. Ordinary capability files remain recorder-independent.

Recording begins only when the search itself finds and executes a real start-recorder action. At that moment, every mapped ghost's has-location, holding, and on state -- and, when the owning tech is included, paired, jamming, and mounted-on -- forks from its live counterpart's current state, and recording-in-progress becomes true. A ghost has no state and cannot act before this point (rule 5). stop-recorder simply clears the flag again; neither action restores or seeds anything on its own. Recording and playback are not two separate simulated runs with a rollback between them -- they are two derived views of the one path the search actually found, as validate-recorder-solution shows next.

validate-recorder-solution first replays the actions before the real start-recorder move to reconstruct the snapshot captured at that point. It then extracts the recording sub-path -- start-recorder, every ghost move within the window it opens, and the real stop-recorder move when present -- and replays that sub-path from the reconstructed snapshot through the recording shadow. Thus pre-recording changes to live object locations, holdings, supports, pairings, jamming, and mounting are present when start-recorder forks the ghosts. The legacy focused-test form with no explicit start continues to use its authored, already-open start state directly. The validator requires every mapped ghost agent to finish able to return to a recorder. It then independently replays the complete integrated path, live and ghost moves together in their original order, from the original problem start through the ordinary action rules, so all dependencies are checked at their exact prefixes. A search that reaches the problem's own goal without a real stop-recorder still validates, provided every mapped ghost could still reach a recorder from where it stopped (recording-agent-can-close); a problem that wants the return trip spelled out in the solution path adds ghost-stops-recorder as a goal conjunct instead. Including the public recorder assembly automatically installs candidate validation, the two-phase report, and recorder-aware goal chaining together after all recorder components have been defined. Rejected nominal goals remain searchable states rather than solutions. Focused tests that need recorder mechanics without the complete policy include the relevant private -recorder-* components instead.

Recording-prefix pruning is off by default. A recorder problem enables it with (ww-set *recorder-prefix-pruning* t). When a generated start-recorder, ghost, or stop-recorder move changes the isolated recording sequence, validate-recorder-recording-prefix runs before that state is accepted as a goal or entered into duplicate-state tracking. It reconstructs the same snapshot and replays the recording actions accumulated so far. Ordinary live moves are skipped; their pre-recording effects are captured when start-recorder is checked, and later live moves do not belong to the recording sequence. A failed action or malformed recorder boundary cannot be repaired by appending later actions, so that branch is abandoned immediately. The prefix check deliberately does not require the ghost to be able to close the recording and does not test final playback or the goal: those conditions can still change later. It works in serial, parallel, and backtracking search; ordinary problems and recorder problems that leave the option disabled pay no path-reconstruction or replay cost.

Ghost locomotion and targeting read recording-side gate state. Plate-controlled gates use ghost-only plate state. Receiver-controlled gates use a recording beam view containing fixed apparatus, mapped ghosts, and unmapped fixed objects while excluding mapped live relays and blockers. Jamming remains one shared action fact: ordinary playback responds to every jammer, while recording-jammed admits only mapped ghost jammers into recording gate and wall-gears state. Initial jamming facts must likewise name mapped jammers. Beam crossings, receiver-controlled wall gears, floor and angled blowers, threats, and movable recording-side wall-fan mounting remain outside the supported scope and are rejected during initialization.

build-recorder-report consumes a completed integrated solution and returns a plist containing the unchanged original path plus recording and playback sequences. It classifies each move by the single mapped agent named in the action; connector names and starred-name conventions are irrelevant. In the recording sequence, each contiguous live-action block becomes one (pause) marker, and only ghost actions remain between the recording's open and close. When the searched path contains a real start-recorder/stop-recorder move, that move opens/closes the sequence in place, exactly like any other searched action. When it does not -- a search that reached the problem's own goal without one -- the report synthesizes a bare (start-recorder) and/or (stop-recorder) marker instead, appending each mapped ghost's outstanding return move first; this is the original, pre--recorder-session behavior, kept as a fallback. In playback, the integrated actions remain intact, with (pause) before each live block and (resume) when a following ghost block begins. The public recorder assembly registers print-recorder-report alongside the validator and recorder-aware goal chaining, so the normal integrated solution is printed first and the recording/playback report follows immediately. All three registrations are cleared whenever a problem is staged, preventing recorder policy from leaking into another problem; restaging a recorder problem installs one fresh copy of each service. A synthesized marker and its appended return moves exist only in the report and never enter the planner's actions, cost, successors, or depth; a real start-recorder/stop-recorder move, by contrast, is an ordinary searched action and already carries its own cost and depth.

Chained solving remains an explicit layer above the single-cycle validator while using the ordinary goal-chaining interface. (solve-subgoal goal) searches and commits at most one intermediate recording cycle; the following (solve) searches and commits the original problem goal as the last cycle. An initial (solve), before any subgoal, remains an ordinary whole-problem search. Chained calls strengthen the requested goal with ghost-stops-recorder, so every mapped ghost physically returns to a recorder in the searched path before the integrated playback state can become a boundary. This stronger chained-cycle invariant deliberately excludes the standalone report's weaker option of appending an unapplied return-walk marker.

After an intermediate commit, the accepted integrated playback state is copied as the next baseline and the just-completed recorded program is discarded. Each shadow component resets the recording relations it owns; after all resets, stateful components seed their memory from the committed playback baseline, and ordinary propagation derives the remaining fresh recording view. This supports sequential cycles only: no nested or concurrent recordings, persistent multiple programs, or global planner search over cycle boundaries.

The chain history retains each cycle's subgoal, strengthened goal, selected integrated solution, recording/playback report, immutable boundary state, search policy, and per-cycle plus cumulative depth, elapsed time, and value change. Search counters and minimum-length depth restart for every cycle, so optimization remains local and the combined report makes no claim of global completeness or minimum total length. Chaining requires *threads* = 0. If a later cycle finds no solution, retry it from the prepared baseline or use (ww-undo) to restore the preceding committed session; undo again to roll back another boundary and rerun that earlier cycle under a different subgoal or search policy.

gate — the hub

gate.lisp owns a real leaf type and its own update-gate-status!, which makes it look like an ordinary assembly. Its gate optional type and (open gate) relation are declared once, in -gate.lisp, nested by every file that reads openwalkability (via -passability), reachability, visibility, beam-direct, beam-crossing, and -passability itself — so the declaration is a hard, enforced dependency rather than a hand-copied one. What stays soft is the assertion: only gate.lisp's update-gate-status! ever makes open true, and something must still call it before anything downstream reads a current value. That call used to be the problem's job; since Phase 3 the driver is derived, so including gate is now sufficient on its own. That write-once-read-everywhere shape — not the declaration — is why gate keeps the widest blast radius in the directory. The controller wiring itself (controls and energized) migrated out of gate.lisp into -controls.lisp when blower drives became a second controlled-device family; control-on now evaluates that DNF aggregate once for gates, blower drives, and guns alike, taking the uncontrolled default as an argument, since that was the only thing that differed between the copies (a gate reduces to open <=> jammed, uncontrolled blower drives and guns to running unless jammed — a jam always disables the barrier, opening a gate but stopping a blower drive). Recorder's recording-side aggregate stays a separate query in -recorder-controls-shadow.lisp rather than a view argument, so that the propagation walker sees disjoint read sets for the playback and recording strata.

-gears-fan — a composite substrate

-gears-fan.lisp is nested by -floor-blowing, wall-blower, and angled-blower — never by an ordinary problem's own include-tech list. It owns the mountable gears leaves, the fixed blower leaves, the gears and blower unions, removable-fan actions, and shared driver helpers. The public floor-gears and floor-blower technologies both nest -floor-blowing, which owns the floor-directed physics shared by those distinct object forms. Floor, wall, and angled behavior use the same drive interface whether the source is a mounted fan or a fixed blower, while only removable fans participate in cargo actions. Composite substrate names that structural shape; the dash independently says that the bundle is private.

beam-direct / beam-relay / beam-crossing — peer-plugins

These three share one role file, -beam-substrate.lisp, which is unusual for a role file in that it also ships null-object default queries (e.g. direct-beam-reaches-receiver defaults to always nil). Each peer overrides only the hooks it owns; an absent peer contributes nothing rather than erroring. Use this pattern only when 2+ variants genuinely need to plug into the same interface — a single relation doesn't need it.

beam-direct owns every directional fixed-apparatus corridor: transmitter→receiver, transmitter→repeater, and repeater→repeater/receiver. Each coupled fact has one matching beam-via; fixed couplings are unlimited. It includes public visibility, records every coordinate-known wall, edge, gate, and boundary crossing for each coupling during initialization, and evaluates those finite barriers from the beam's current endpoint elevations at runtime. Equality with a barrier top blocks; the beam must be strictly above it (or below its base). A coordinate-known open gate is transparent, while a closed one can be cleared by height; an authored beam-via gate without crossing geometry keeps the legacy open-only rule. A location in the corridor blocks only when a beam blocker (agent/box/jammer/connector) with has-location there spans the beam's interpolated elevation.

beam-relay propagates one derived color through connectors and repeaters. Connector paired links are structurally undirected and visibility-dependent; repeater coupled links keep their authored direction and normal corridor checks. If differently colored sources reach a relay in the same propagation layer, that relay remains unlit. Both kinds of live link participate in beam crossings.

apparatus-coords> — apparatus functional points

apparatus-coords> is the XY coordinate of an apparatus's functional point: the point where a transmitter, repeater, or gun emits or acts, or where a receiver accepts a beam. It does not create a walkable or occupiable location. Transmitters, receivers, and guns have zero physical height; their functional elevation defaults to 1. A repeater is fixed and likewise has no has-location.

A floor repeater's has-elevation is its base/floor elevation, default 0, and its beam anchor is that base plus declared-height, default 1. A wall repeater's has-elevation is already its mounting and beam-anchor elevation, default 1. Its declared height measures how far it projects horizontally from the wall, so changing that height does not raise or lower the beam anchor. No separate wall identity is needed: apparatus-coords>, mounting subtype, elevation, and height fully determine the functional geometry used by the planner.

-location-coordinates — one role shared across two coordinate-derivation substrates

-location-coordinates.lisp owns nothing but (location-coords> location $rational $rational) — a location's raw 2D placement, independent of any capability that might consume it. Both -beam-los-coordinates (nested under visibility, and transitively under beam-crossing via -beam-crossing-coordinates) and -walkability-coordinates (nested under walkability) nest it for a location's coordinates, so a problem using both capabilities enters each location's position exactly once, and a problem using only one of them never pulls in the other's machinery. This is the direct worked instance of the integration checklist's "new role, 2+ plausible consumers" rule below: the role got its own dash-prefixed file specifically because a second consumer showed up, rather than the second consumer copying the first's relation under a different name or requiring an unrelated capability just to get coordinates.

-walkability-coordinates's own derive-walk-via-from-segments also illustrates that a coordinate-derivation substrate need not use the same algorithm as its sibling: -beam-los-coordinates tests straight-line segment intersection (right for sightlines and beams), while -walkability-coordinates answers a region-connectivity question (right for walking, where the real path detours through a doorway or around a corner rather than following a straight line). It builds a coordinate-compressed arrangement from the segments, the boundary polygon, and each wall fan's derived air-stream band (center line from the solid backstop behind the fan to its aimed-at destination, widened to stream-width, default 3); flood-fills open cells into zones; and computes every subset-minimal door-set between zone pairs, emitted as DNF walk-via clause families. A stream's swept location belongs to the band's interior zone only, so every edge to it carries the gears -- standable exactly while the stream is off; riding is instead the destination's property: from every zone flanking the band across a side curtain, a directional walk-via> edge into the aimed-at destination carries that zone's own family (step laterally into the flow and be carried there while blowing, or walk across the dead band while off -- the unconditional edge is correct in both regimes), while entry against the front curtain stays gears-gated, so the mobility closure crosses a blowing stream only downstream, never against or through it. The derivation is single-layer and elevation-blind: an elevated platform authors its ground-level footprint as walls, keeps its top locations inside, and connects levels only with authored jump-via/climb-via> edges (see problem-claustro-topo's slab). See the file's own header for the full reasoning.

reachability — identity default with an optional extension

-reachability.lisp supplies the baseline reachable query used by manipulation actions: two locations are reachable when they are identical. box, jammer, and beam-relay nest this substrate, so they remain self-contained without authored reach edges. Including the public reachability technology overrides the same query with identity plus reach-via edges and gate checks. As with the beam hooks, nested-include deduplication installs the default once before any override, independent of the problem's include order.

walkability — walking topology and mobility provider

-walkability.lisp owns authored or coordinate-derived walking edges and the canonical DNF-family operations used by the coordinate derivation. Public walkability turns currently enabled edges into normalized walk segments. -mobility.lisp composes those segments with stairs, ladder, and grounded-jump providers into one canonical closure. -configuration-transition.lisp separately retains one support-changing boundary at a time. -mobility-action.lisp normalizes both result kinds as routes between agent configurations and applies either through the single move action. beam-relay consumes only the transparent mobility closure for pairing vantages. Recorder validation uses the same current-state closure after restoring its snapshot.

visibility — null default with an optional extension

-visibility.lisp supplies ordinary, beam, and endpoint-elevation-aware visibility interfaces with null defaults, allowing beam-relay and jammer to compile independently while granting no sight-dependent behavior. Including the public visibility technology adds authored LOS relations and coordinate derivation; beam-direct now includes it for fixed-coupling barrier geometry. Initialization retains every wall, edge, gate, and boundary crossing with its oriented beam parameter, including apparatus-to-apparatus fixed couplings. visible keeps solid barriers opaque; potentially-visible recognizes structural location pairing; beam and elevated-jammer sight interpolate their current endpoint elevations and clear only below a barrier base or strictly above its top. Open gates remain transparent, and actor-aware forms select playback or recording-shadow gate state.

A typed null hook is still installed when one of its optional object types has no objects in the current problem. Calling the hook returns its neutral value (usually NIL); the empty type matters only where an action or quantifier enumerates it, in which case there are no instantiations and therefore no calls. Query/update signatures type Wouldwork object parameters only. Computed Lisp values such as elevations, lists, and hash tables remain untyped parameters.

beam-relay uses potentially-visible across every location in the agent's current mobility-locations closure when selecting pairings, before placing the connector. Traversal obstacles use current passability when forming that closure, but gates and finite barriers on a potential LOS need not already clear. Live lighting and receiver propagation use the beam-specific visibility forms from the connector's exact placed location. jam-target evaluates each legal support at the jammer's resulting top elevation and aims at a gate's vertical midpoint or a point target's functional elevation; a mapped ghost still uses recording-side gate state. The same file owns put-connector, which shares placement rules with connect-connector but creates no pairings.

passability — shared traversal clearance

-passability.lisp owns the action-free rule for clearing a traversal edge's obstacle list: open gates pass, while screens and ladders require an empty-handed agent. walkability, jump, and ladder nest it for walking, jumping, and ladder traversal respectively. Public walkability is therefore needed only for walking behavior.

jump — mobility barrier clearance and support transitions

jump.lisp owns one capability with optional physical path features. An empty feature list is an unobstructed jump. Open gates and passable screens need no clearance; closed gates, non-passable screens, and walls contribute their top elevations, and the agent must clear the highest one. Ground-to-ground edges are pure mobility segments that may compose into a longer move route. Jumps onto, off, or between supports are singleton configuration-transition routes consumed by the same move action, so the closure still stops at every support-state boundary.

Clearing a barrier and rising in elevation are the same physical constraint, both bounded by the identical *vertical-reach-limit* (default 1, owned by -support-elevation.lisp and shared with cargo reach -- see declared-height below) measured from the jump's explicit launch elevation — independent of the jumping agent's own declared height in either case. An agent standing high enough (on a box, a platform, or a prior jump's landing) can clear an arbitrarily tall wall as long as the remaining clearance from that launch point is within the fixed limit; downward and level landings remain unrestricted.

Every produced segment or transition is tagged JUMP or VAULT as its move-type — the same position WALK, STAIRS, and CLIMB occupy in a printed route. The tag is VAULT when some feature genuinely required clearance (again via jump-required-clearance-height's passability filtering, so an already-open gate or an empty-handed-passable screen in the list does not count), else JUMP. This is purely a descriptive label on the same normalized segment; it does not add a new action or change what MOVE can produce.

Case study. Jump clearance needs a launch elevation, which may come from either a support top or a location floor. The feasibility queries therefore receive that elevation explicitly: the configuration provider supplies the source configuration's support top or floor elevation, while the mobility provider supplies each hypothetical intermediate location's floor elevation. The shared -support-elevation.lisp role still owns support-top and occupant elevation without making jumping depend on box manipulation.

declared-height — a consolidated role query

-height.lisp's single declared-height query owns four role defaults: gate, screen, and wall default to 4; edge and agent both default to 3/2; and box, jammer, connector, and repeater default to 1. An explicit has-height always overrides the role default, and both has-height and has-elevation are declared $rational, so a fractional override such as edge's own default is a legal authored fact. Jump clearance and LOS clearance both call this same query, so their physical barrier height cannot drift apart.

connector (from beam-relay.lisp) later joined heighted-object and beam-direct's beam-blocker composite, so a connector left sitting in a direct-beam corridor blocks it exactly like a box or jammer would, defaulting to the unit height of 1 rather than an agent's 3/2. Its own beam anchor is its occupant/base elevation plus that declared height. beam-relay.lisp receives -height through -placement (which nests -support-elevation), so connector pickup and placement can also apply *vertical-reach-limit* via within-agent-vertical-reach.

Boundary segments. A boundary-wall edge has no named object on which to assert facts, so LOS gives it its own fixed base 0 and height 6 directly — distinct from an internal wall's default of 4. Named internal walls and edges may override either base or height through has-elevation/has-height.

Two axes of dependency: hard vs. soft

Hard dependencies are (include-tech ...) nests — resolved by the splicer at every level, so a present file can't quietly omit what it nests. A technology that doesn't exist yet is spliced as a skip-with-warning rather than a hard error, so a problem may be staged before every included technology is written. Soft dependencies are calls into relations or queries owned by another file without nesting it. An unguarded soft dependency requires the problem's own include-tech list to bring in the owning file.

A soft dependency can instead be conditional when every reference is guarded by an optional type. For example, gate calls jamming only inside (exists (?j jammer) ...), and calls depressed only in an and branch guarded by (plate ?controller). When the guarding type is empty, static translation removes that branch before translating the relation call, so the relation's owning technology may be absent. If the problem declares any objects of the guarding type, it must include the owning technology.

Keep conditional references in forms whose static truth the translator understands: quantifiers over the optional type; statically decidable and, or, not, if, and cond branches; or a case selected by a static key. An unreachable reference hidden inside an arbitrary Common Lisp call is not optional. Document both the owner and the guard in the referencing file's REQUIRES header.

A shared hook substrate is the third case. It supplies a complete baseline query, while an optional public technology overrides that query with extended behavior. Consumers nest the substrate and therefore never have a missing function; the problem includes the public technology only when it needs the extension. -walkability/walkability, -reachability/reachability, -visibility/visibility, -beam-substrate/its peer technologies, -recording-shadow-policy/-recorder-core plus the matching apparatus shadow components, and -passability's actor-aware stream-obstacle-clear/-stream-passability use this pattern.

FileCalls (soft)Actually defined inRequirement
gatejammingjammerConditional on nonempty jammer
-recorder-jamming-shadowjammingjammerConditional on nonempty jammer; recording-jammed filters the shared relation to mapped ghosts
-controlsdepressedplateConditional on nonempty plate (read via energized, shared by gate and the blower techs' gears)
-beam-los-coordinateslos-to-apparatus, los-to-target, los-to-locationvisibility declares them; hand-authored by the problem, or derived by -beam-los-coordinates's own derive-los-from-segments when the problem instead asserts wall-segment>/gate-segment> factsConditional on the problem authoring position facts and including visibility

Integration checklist

For a new object type, work through these in order.

  1. Classify it first. Does it just need to participate in existing roles (have a location, be held, rest on/be rested on, have a fixed position, have a height), or does it need a genuinely new relation nobody has? Most new objects are almost entirely the former.
  2. New role, one consumer → declare its type + relation inline in the consuming assembly file (like jump's vaultable-object or jammer's target). Don't create a substrate file for something with exactly one user.
  3. New role, 2+ plausible consumers → give it its own dash-prefixed, private file with only the shared types, relations, and queries needed for that role. No actions or driver logic by default — that's the shape to reach for first. Widen to a composite substrate (see Special cases) only when 2+ sibling assemblies genuinely share tightly-coupled actions that splitting further would fragment, as -gears-fan does for the three blowers; it remains dash-prefixed because it remains private.
  4. Existing role → nest the owning substrate file. If the new leaf type isn't already a member of the role's either union, add it there — the union lives in exactly one place, and nesting alone doesn't extend it. Every either must list base types directly; do not put the role's name inside another composite as shorthand. Never retype the owning either form elsewhere (see the jump case study above); edit the one owning declaration instead. A new object touching several roles (mobile, occupant, heighted, ...) means one small edit per role it needs, in that role's own file — the cost of "exactly one owner per role."
  5. Declare the leaf type optional (define-optional-types) in the new assembly and in every other file that references it as a bare pre-param type. This is idempotent and order-independent, so repeating it is always safe.
  6. Design each relation signature deliberately. A $-prefixed declared type marks a functional/fluent value position. When the same normalized type appears in multiple positions, Wouldwork treats those positions as symmetric and installs their permutations; append > to the relation name when those repeated positions instead have ordered roles. Thus walk-via and jump-via are symmetric, while ladder-via>, jump-via>, and jam-disallowed> are directional.
  7. Identify soft dependencies — which services (reachable/visible/mobility-results) or hub facts (open) the new actions will call. Document them in a REQUIRES header like the existing files do. The owning file must be in the problem's include-tech list unless every reference is guarded by an optional type that is empty for that problem, or the consumer nests a complete default hook substrate.
  8. New derived boolean fact? Declare its relation with define-derived-relations, give it a zero-argument define-update, and nest -propagation. The declaration prevents a problem from authoring either polarity of the computed fact in define-init. A zero-argument update in a technology file is a driver update and is picked up automatically; a parameterized one is a helper whose relevance its callers decide. Do not write a call into propagate-consequences! and do not ask a problem to sequence it — the order is derived from the read/write graph at init. If the new update writes base facts that ordinary actions also write, it is a reaction and the derivation places it after every derivation, which is the one ordering constraint that is enforced rather than merely optimized.
  9. Own initialization semantics. Put a small define-init-check beside the relation it validates, or nest a focused -*-init-checks.lisp companion when several helpers belong together. Use define-init-check-helper for those helpers and fail-init-check for authoring failures. Declare (:consumes ...) when a type is visible only inside an untyped list payload.
  10. Write the actions. This is the one genuinely bespoke step.
  11. Test via REPL, unless the new tech isn't referenced by any problem's include-tech yet, in which case the testing waiver applies.