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, -plate-types, -position, -height, -elevation, -support-occupancy, -support-elevation, -placement, -pickup, -gate, -controls, -gears-fan, -floor-blowing, -passability, -stream-passability, -traversal, -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, held-only tray support, and on cyclesrole (nested by the four physical role owners)
-stream-passability-init-checks.lispWall-stream geometry and width consistency: coordinate-driven problems require both endpoint coordinates; whenever coordinates are known they are distinct and axis-aligned. Endpoint levels use their authored value or zero default and must match because wall transport is horizontal; the absolute stream elevation must be strictly above that floor.role (nested by -stream-passability)
-beam-substrate-init-checks.lispRepeater mounting/coordinates, fixed coupling, chroma, corridor, and beam obstacle-list checks; only positive literals supply required beam topology, so an explicit negative cannot masquerade as a coupling, corridor, or chromarole (nested by -beam-substrate)
-controls-init-checks.lispControls DNF list contents and supported modesrole (nested by -controls)
-beam-relay-init-checks.lispPositive connector pairing placement, limits, acyclicity, and potential sightlines; explicitly absent pairings, locations, and sightlines never satisfy or trigger topology checksrole (nested by beam-relay)
-beam-crossing-init-checks.lispPositive authored beam/crossing endpoints, indexes, lists, reverses, sightlines, and gate prefixes; explicitly negative records never define or index crossing topologyrole (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 one-to-one recording-copy> with constant-time static indexes in both directions; dynamic recording-in-progress, recorder-cycles-used, and recorder-cycle-closed; live/ghost/same-side identity and one representative recording-view selector; recorder-aware symmetry coupling; recording-view object presence; and interaction-policy overrides. The cycle count is search state because it changes future start legality. 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!, selecting one shared recording view and computing its relay lighting once per propagation pass before consuming the beam substrate's recording-arrival hooks for every receiver-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> (x, y, and an optional third coordinate: the location's own level, default 0); init check cross-checking that level against has-elevationlocation-coordinates-init-checkrole
-apparatus-coordinates.lispapparatus-coords> (x, y, and an optional third coordinate: the fixture's mounting level, default 1); init check cross-checking that level against has-elevation. A floor-repeater is exempt — it stands on the floor, so its base is has-elevation defaulting to 0apparatus-coordinates-init-checkrole
-holding.lispcargo, holdingrole
-plate-types.lisppressure-plate/toggle-plate (optional), their plate unionrole (nested by -controls, -position, -support-occupancy, and plate)
-position.lispfixed-position-object (pressure-plate/toggle-plate/ladder/gears/fixed-blower leaves/recorder), has-position-plate-typesrole
-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). Relation only — heights are read through -vertical.lisp's object-height, which supplies the per-type default. Height follows the mounting axis: vertical for a floor repeater, horizontal projection for a wall repeater. A wall repeater's height is therefore descriptive only — nothing reads it, and no repeater names the wall it projects from, so there is nothing to cross-check it against (see repeater.lisp)role
-support-occupancy.lispsupport, support-occupant, on, query cleartop-plate-types, -interaction-policyrole
-vertical.lispType vertical-object; parameter *vertical-type-constants* (per-type height default, axis, and base default); queries base/top/fixed-base/object-height/location-elevation. base is structural — it follows on, holding, has-location, then has-position, and otherwise calls fixed-base. That query reads the level coordinate from location-coords>, apparatus-coords>, or a named segment relation where applicable, then has-elevation, then the object type’s base default. top adds the object’s height only when its axis is vertical, so a wall repeater’s horizontal projection never lifts its tip — a height on a non-vertical axis is descriptive only, entering no computation anywhere-height, -elevation, -location-coordinates, -apparatus-coordinates, -segment-geometry, -support-occupancy, -location, -position, -holdingrole
-support-elevation.lispReach policy only, no geometry: managed problem parameter *vertical-reach-limit* (default 1; also reused by jump.lisp), queries within-agent-vertical-reach/within-agent-placement-reach, and the conservative staged-model relevance guard used by parameter display-verticalrole
-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, -propagation, -holdingrole (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!, which unloads a tray when its holder releases it. mounted-on is an intentional soft dependency guarded by optional fan, avoiding a cycle with -gears-fan-vertical, -support-elevation, -support-occupancy, -location, -position, -holding, -interaction-policyrole
-pickup.lispQuery pickup-clear (policy-compatible actor/object, agent empty-handed, object's location reachable, object's elevation within vertical reach); depends only on the foundational geometry, reach-policy, cargo, interaction-policy, and reachability roles, not placement-option or placement-update machinery-vertical, -support-elevation, -holding, -interaction-policy, -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-plate-types, -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!. gears-fan-init-check requires every drive whose floor, wall, or angled mounting physics is installed — removable-fan gears or fixed blower — to have distinct has-position source and aimed-at destination locations; the functional relation keys reject multiples. A substrate-only control or jamming target does not pretend to have an installed stream. gears-fan-mounting-init-check enforces one fan per gears, forbids held or supported mounted fans, requires floor/angled fan location to equal the gears location, and requires wall-mounted fans to have no location. Fixed blowers have no separate cargo fan identity; floor and angled fixed blowers directly expose their flush support surface.-vertical, -support-occupancy, -location, -position, -elevation, -controls, -placement, -reachability, -pickup, -recording-shadow-policy, -recorder-fork-registrygears-fan-init-check, gears-fan-mounting-init-check; pickup-fan, put-fan, mount-fancomposite substrate (peer substrate for the three mounting technologies)
-floor-blowing.lisplocation-elevation gives only undeclared destinations named by a floor drive the hover elevation 10; other undeclared locations, including wall-stream destinations, remain at ground level 0. 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. Shared gears-fan-init-check enforces the source/destination endpoints. Each floor drive must also have its own destination because hover state would otherwise retain no unique drop-back source. floor-blowing-init-check enforces that floor-only rule and requires a coordinate-known stream to have matching source/destination x/y and a strictly higher destination whenever both endpoint levels are authored through coordinates or has-elevation; problems omitting either piece of geometry retain abstract endpoints-gears-fan, -verticalfloor-blowing-init-checkrole (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)
-traversal.lispType traversal-mode (walking, stairway, jumping, climbing); one symmetric traverse-via and one directed traverse-via> keyed by it, replacing the five per-mode relation pairs; the mode registry each technology registers its own clause predicate with; the single traversal-segments mobility provider that iterates modes and destinations and takes the first clause a mode accepts; traversal-init-check, which validates per-mode DNF payloads and rejects same-endpoint edges because mobility is already reflexive; and the canonical antichain algebra for DNF families that -walkability-coordinates' zone-graph derivation also uses. The payload is DNF for every mode now, which is what closed the old hazard of one syntactic shape carrying two readings. reach-via is deliberately not a mode: it authorizes manipulation, not movement-mobilitytraversal-init-checkrole (traversal 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.lisprepeater (floor-repeater/wall-repeater), optional gate/transmitter/receiver/floor-repeater/wall-repeater/gun leaf types, and null-default ordinary, beam, and endpoint-elevation-aware visibility queries whose target domains name the point-apparatus leaves directlyrole (hook/interface variant; point 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), inclusive base-to-top ordinary and recording-view blocker queries, and beam-blocker-spans-elevation; the recording view filters blockers through the recording-shadow presence policy-vertical, -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 (the recording relay hook consumes shared precomputed lighting), 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 whose trailing shared base z is optional and defaults to 0, ordered boundary-wall, gathering queries that retain planar records for horizontal algorithms, and shared initialization validation including agreement with has-elevation. The boundary’s managed *boundary-wall-height* defaults to 6; it has no named object and therefore no authored base or has-height.segment-geometry-init-checkrole (shared by walking, LOS, and vertical geometry)
-beam-los-coordinates.lispOptional floor-repeater/wall-repeater/jammer/gun, los-endpoint, managed problem parameter *beam-occlusion-tolerance* (default 1/2), the shared beam-coordinates-segment-intersection-parameters primitive (the meeting point of two infinite lines, as a parameter along each — every containment and endpoint rule belongs to its caller, which is the only part the two beam substrates ever disagreed about), coordinate-derived los-via sightlines — to apparatus, to another location, and, when a jammer is present, to a gate or gun — and oriented static barrier-crossing records for runtime height checks-location-coordinates, -apparatus-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 proper intersection of projected 2D paths and derives crossings-along-beam>/beam-crossings-before-gate> from LOS geometry and every authored fixed coupled beam, including repeater links. This is intentionally planar: elevation governs barrier and occupant clearance, not beam-beam interference; the sole topology consumer, flat corner-topo, keeps all anchors at the common default height-beam-los-coordinatesestablish-beam-coordinates, derive-beam-crossings-before-gaterole (nested only under beam-crossing)
-walkability-coordinates.lispDerives the walking traversal edges, symmetric and directed, 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 traverse-via> ride edges (inbound widened by side-curtain rides, outbound ordinary). Default walkability-coordinates-stream-specs (no streams) is overridden by -stream-passability, and default terrain-complaints (no complaints) by -terrain-consistency. The arrangement it returns also carries its raw :xs, :ys, and :zones, so a consumer can ask which zones flank a given segment interval-traversal, -location-coordinates, -segment-geometryderive-walking-from-segments (init)role (nested under walkability and -stream-passability)
-terrain-consistency.lispCross-checks authored levels against the walking arrangement. Public walkability nests it and automatically applies the universal invariant through -walkability-coordinates' terrain-complaints seam: an edge's base must equal the lower level of the determinate step it separates and its top the higher. Two stronger connectivity policies are reserved for test-topo, whose complete topology specs require a stepped edge to have something crossing it and every level group in one walking zone to be joined. Focused walking models may deliberately contain a dead elevation-changing walking candidate, so those policies are not universal initialization invariants. A level change is an authored stairway/jumping/climbing edge or a floor drive's lift from its own location to its aimed-at destination. reach-via counts as neither. All three analyses abstain wherever the arrangement cannot determine the relevant levels-walkability-coordinates, -verticalrole (nested by public walkability; never included directly by a problem)
-elevation.lispelevated-object, has-elevation — the authored base of an object with no coordinate relation of its own (floor repeaters, segment fixtures in coordinate-free models, or another elevated object in a problem without coordinates). Relation only — all six former queries are gone: -vertical.lisp's base and top compute base and top for every type from one table, and coordinate-bearing locations, apparatus, and named segments carry their base inline-height, -location-coordinatesrole
elevation.lispPublic problem-facing wrapper for -elevation-elevationrole wrapper
walkability.lispRegisters the walking mode: a clause is a walk when its doors are passable and the two endpoints sit at the same level — the only mode with an elevation-equality test, which is what makes the elevation-blind coordinate derivation safe. Also one-step-walkable, restricted to WALK segments now that one provider returns every mode's, and automatic terrain edge-span validation through nested -terrain-consistency-support-occupancy, -location, -passability, -vertical, -elevation, -traversal, -walkability-coordinates, -terrain-consistency, -threat, -mobility-actionmove (central mobility action)service (walking mobility provider)
stairs.lispRegisters the stairway mode: every means in the clause must be usable, with no elevation test at all — an authored stairway is the answer to a level change, not a consequence of one-passability, -threat, -traversal, -mobility-actionmove (central mobility action)service (stairs mobility provider)
reachability.lispreach-via, extended reachable, reachable-clear-reachability, -gateservice
visibility.lispOne symmetric los-via replacing the three relations that differed only in far-endpoint type — every consumer bound all three in turn and took whichever matched, which is what showed the distinction carried no information. Its two endpoints share one type, so the engine mirrors it: a sightline is symmetric, which the old shape could only express for the location-to-location case. Directed los-barrier-crossings> stays as it was, its records carrying a parameter measured along the beam. One los-clear-for-object answers every sightline question under a policy — :sight (no crossing clears by height, location occluders transparent), :elevation (crossings tested at their interpolated elevation, movable occupants ignored), :beam (the same, plus a location occluder blocks when a beam-blocker there spans the beam) — and visible, elevation-visible-for-object and beam-visible are that query under each. The occluder payload is a flat conjunction, deliberately unlike -traversal's DNF: a traversal may take alternative routes, a sightline is one straight line. visibility-init-check validates only positive facts and rejects same-endpoint lines, repeated occluders, and endpoints mislisted as intervening occluders-visibility, -gate, -beam-los-coordinates, -beam-interpolation, -vertical, -beam-occlusionvisibility-init-checkservice
gate.lispjammer (optional), update-gate-status! (sole asserter of -gate's open; controls/energized now live in nested -controls)-controls, -gatehub
plate.lispdepressed/latched, update-plate-status! (pressure-plate/toggle-plate/plate come from nested -plate-types)-plate-types, -support-occupancyassembly (no actions)
recorder.lispThe problem-facing recorder assembly. It composes identity/isolation, repeatable session transitions, plate, receiver, controls, jamming, gate, and wall-gears shadow components with mandatory completed-cycle validation, optional open-prefix pruning, exact canonical-interleaving pruning, multi-window candidate validation, explicit guided chaining, 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: a state-machine parser for repeated setup/start/window/stop segments and an optional final open window; mandatory stop-successor validation before duplicate tracking; optional search-time validate-recorder-recording-prefix pruning for the current open cycle; exact live/ghost interleaving certification; completed-candidate validate-recorder-solution validation of every cycle plus integrated playback; and path-reconstructed per-cycle reporting with complete totals. Diagnostics identify the failing cycle. Reads identity, location, position, the mobility closure, and session lifecycle state, but no per-apparatus recording-side relation directly-location, -position, -mobility, -holdingrole (nested by recorder; never included directly)
-recorder-session.lispRepeatable recording transitions. start-recorder requires a clean closed state, an empty-handed live agent at a recorder, and an unused slot below *max-recorder-cycles*; it increments the state count, forks every mapped ghost from current live has-location, holding, on, and optional paired, jamming, and mounted-on state, then normalizes the open recording shadow. stop-recorder requires every mapped ghost agent at a recorder and empty-handed and rejects live/ghost support or holding dependencies before atomically closing and normalizing; nonphysical links such as connector pairings disappear with their ghost endpoint.-recorder-cycle-boundary, -location, -holding, -support-occupancystart-recorder, stop-recorderrole (nested by recorder; never included directly)
-recorder-cycle-boundary.lispThe shared boundary contract for planner transitions and guided chaining. A successful stop removes all dynamic ghost references, preserves live and ordinary persistent state, resets every capability-owned shadow before any seed, seeds stateful memory from the committed live baseline, propagates, and leaves an explicit closed marker. ghost-stops-recorder reads that marker and the ghost-free invariant rather than vanished ghost positions. prepare-recorder-cycle-state copies a stopped state and delegates to the same idempotent normalizer.-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, setup/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 when its base is strictly below the active stream and its top reaches that stream; playback/recording view selection, relocation, landing, and stream-height behavior are shared across removable and fixed wall blowers. Coordinate-known endpoints are distinct and axis-aligned; their floor levels match, and the absolute stream elevation is strictly above that floor.-vertical, -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. Coordinate-known endpoints must have horizontal displacement; use a floor drive for a vertical stream. A cyclic destination graph is permitted while inert; an active occupant-transport loop fails to converge, is marked inconsistent by the shared propagation cap, and is discarded.-gears-fanangled-blower-init-checkassembly (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
tray.lisptray (optional); a carryable, stackable support only while held — grounded, it is inert, and releasing it unloads its rider onto the ground at the tray's current location. It keeps its has-location fact even while held so an occupant placed on a held tray stays trackable, its location kept synced to the holder's by -configuration-transition's relocation cascade as it moves-placement, -reachability, -pickuppickup-tray, put-trayassembly
jump.lispbox/wall (optional), vaultable-object, the explicit-source elevation and clearance queries, the jumping mode registration, and jump-configuration-transitions for support changes — which reads the same JUMPING edges but cannot go through the shared provider, because a transition's endpoints are (location place) configurations rather than locations. JUMP and VAULT are labels a segment earns in the state it is evaluated in, not two modes-vertical, -support-elevation, -passability, -threat, -traversal, -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. Blower aiming delegates to the same blower-elevation used by fan manipulation and stream physics.-vertical, -gears-fan, -placement, -reachability, -visibility, -pickup, -recorder-fork-registrypickup-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 over its los-via sightline)
ladder.lispladder (optional), the source-position and means-membership queries, and the climbing mode registration — authored as traverse-via>, since a ladder that carries an agent up need not carry it down. A climbing clause reads in two registers at once: its ladders are candidates, one of which must stand at the source, while every member including those ladders must also clear. Grounded climbs remain transparent mobility; a supported agent instead receives one explicit configuration transition from its current support to destination ground, so leaving the support cannot compose with farther travel in the same move. ladder-init-check rejects symmetric climbs and any alternative clause that has no listed ladder fixed at its source, because either edge would be permanently unusable-position, -passability, -threat, -traversal, -mobility-actionladder-init-check; move (central mobility action)service (directed ladder mobility plus supported-source transition)
beam-direct.lispFixed coupled-corridor clearance, initialization-time wall/edge/gate/boundary crossing records, runtime endpoint-height clearance, direct transmitter→receiver arrival, single-view 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 from shared precomputed lighting, relay/crossing hooks, and the default *max-connector-pairings* value of 3 when the problem supplies no override-beam-substrate, -placement, -visibility, -vertical, -mobility, -reachability, -pickup, -beam-relay-init-checks, -recorder-fork-registrypickup-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 one-to-one from live to ghost, with static indexes supporting constant-time lookup from either endpoint; initialization validation also rejects 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 executes a real start-recorder action while no cycle is open, its live agent is at a recorder and empty-handed, the closed state contains no stale ghost references, and the stateful cycle count is below *max-recorder-cycles* (default 1). The count increments at start, so a final cycle that reaches the problem goal while still open is counted. 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, recording-in-progress becomes true, and the recording shadow is reset, seeded, and propagated from that snapshot. A ghost has no dynamic state and cannot act before this point (rule 5).

stop-recorder is a cost-1 planner action just like start-recorder. It requires every mapped ghost agent at a recorder and empty-handed. Closure is rejected when a live and ghost object are still connected by holding or on, because deleting either relation would leave the surviving object's physical state undefined. A connector pairing is only a nonphysical link, so it may cross the boundary and simply disappears with its ghost endpoint. A successful stop preserves live objects and ordinary persistent state such as toggle latches, removes every dynamic ghost reference (including locations, holdings, supports, pairings, jamming, and mounting), resets all recording-shadow components, seeds stateful memory from the committed live playback baseline, propagates consequences, and asserts recorder-cycle-closed. Recording and playback remain two derived views of the one integrated path; stop is a repeatable primitive boundary rather than a nested planner invocation.

parse-recorder-path walks the integrated path as a state machine: closed setup, start, open window, stop, then setup again. It accepts any number of completed cycles up to the stateful maximum, trailing setup after a stop, and one optional final open cycle. Nested starts, unmatched stops, and excess starts are rejected with the cycle number. Authored focused-test states that begin with recording-in-progress retain one implicit legacy window.

validate-recorder-solution reconstructs each cycle's snapshot by replaying the complete integrated prefix through every preceding normalized stop and subsequent setup. It replays that cycle's real start, ghost moves, and optional real stop in isolation, then independently replays the complete integrated path and checks the problem goal. A final goal reached in an open cycle remains allowed when its isolated recording is executable; intermediate cycles must contain the physical stop that created their normalized boundary. Rejected nominal goals remain searchable states rather than solutions.

Every generated stop-recorder successor receives mandatory isolated-cycle validation before goal handling or graph duplicate tracking. The validator has a newest-move trigger, so recorder problems do not reconstruct paths for unrelated successors. An invalid completed recording therefore never occupies the closed list and cannot suppress a valid route to the same normalized state. The same boundary check rejects a completed cycle whose ending state differs from its starting boundary only in the consumed-cycle count. The closure marker, every ordinary fact, and every recording-shadow fact remain part of this comparison, so closure itself may still be meaningful. A strict improvement to the active time/value objective is also progress. When exogenous happenings exist, the check conservatively retains the cycle because elapsed time may advance toward an event. Diagnostics carry :cycle alongside the failing phase and action or :reason :no-persistent-progress. Optional recording-prefix pruning remains off by default; when enabled with (ww-set *recorder-prefix-pruning* t), it additionally replays the currently open cycle after starts and ghost moves so an irreversible failure is discarded earlier.

Graph identity remains the complete planner database. In an open cycle it includes recording-in-progress, the stateful cycle count, all current live and forked ghost state, and every recording-shadow component. The pre-start snapshot is therefore replay history needed to validate actions already taken, not hidden context that changes the future of two equal accepted open states. At ghost-free boundaries, a separate recorder Pareto frontier projects out only recorder-cycles-used and uses the engine's exact concrete or symmetry-canonical comparison. An equal boundary reached at no greater depth, time, or objective cost with strictly fewer cycles used dominates the more-used state; a cheaper more-used boundary and a more-expensive fewer-used boundary both remain. Same-count duplicates still go through the ordinary graph open/closed machinery. The frontier is reset for each search, synchronized in parallel search, inactive when the maximum is one, and disabled for tree/backtracking, hybrid path enumeration, exogenous happenings, and solution-enumeration modes where dropping a resource-dominated path would change enumeration semantics.

Serial, non-hybrid depth-first recorder search automatically canonicalizes independent live/ghost interleavings. For each adjacent ghost-before-live pair, it conservatively replays the live action first and then the ghost action. Both swapped states must pass ordinary validity and every non-recorder prefix policy, and the complete resulting state must exactly equal the original ghost-before-live result. Otherwise the original ordering is retained. A certified inversion is discarded in favor of its live-before-ghost equivalent. The original successor has already passed recording-prefix validation, and swapping one adjacent live/ghost pair leaves the recording-only action sequence unchanged, so certification reuses that result rather than reconstructing and replaying the same recording prefix. No user setting or diagnostic output is involved. Parallel, hybrid, and backtracking search are unchanged.

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 reconstructs it from *start-state*, returning a plist containing the unchanged original path, an ordered :cycles list, trailing setup, and complete-solution totals. Each cycle contains its number, its cycle-local integrated path, setup, recording and playback sequences, explicit or synthesized closure status, and local depth, elapsed-time, and value-change metrics. The local integrated path begins with that cycle's setup and ends with its searched stop when present. Actions after the final stopped cycle remain under :trailing-setup with their own metrics. A legacy path with no explicit boundary is represented as one synthesized report cycle. For compatibility and convenient display, a one-cycle report also exposes its setup, recording, and playback sequences at the top level.

Within each recording sequence, every contiguous live-action block becomes one (pause) marker and only ghost actions remain between the recording's open and close. Searched start-recorder/stop-recorder actions open and close the sequence in place. A final goal reached before a searched stop instead receives a report-only (stop-recorder) marker after any available mapped-ghost return moves. This synthesized closure records the presentation implied by the existing final-open-cycle policy; it does not add actions to the accepted planner path or claim different physical validation. Playback retains only in-window moves, placing (pause) before each live block and (resume) before a following ghost block. All metrics exclude synthesized markers. Report construction replays every searched segment, so an inconsistent accepted path surfaces as an error rather than producing misleading local values.

Multi-window paths are valid candidates for one planner search, so the report's complete totals and the planner's depth, time, and value optimization cover every setup, start, integrated window, stop, and trailing action in the accepted path. Single-cycle printing keeps the compact setup/recording/playback presentation. Multi-cycle printing adds one numbered section per cycle, followed by trailing setup when present and complete totals. The public recorder assembly registers this printer alongside validation and recorder-aware goal chaining; all three registrations are cleared whenever a different problem is staged.

Recorder performance controls are deliberately layered. *max-recorder-cycles* defaults to one, preserving the original search space; at that value the closed-boundary dominance frontier is disabled completely. Mandatory completed-cycle validation reconstructs a path only for a generated stop-recorder successor, and rejects invalid or no-progress cycles before ordinary graph insertion. Optional *recorder-prefix-pruning* spends additional replay work only after starts and ghost moves, in exchange for abandoning an unplayable open recording earlier. Exact live/ghost interleaving canonicalization remains automatic for serial non-hybrid depth-first search. Raising the cycle maximum adds legal starts and therefore search states even when the selected solution still uses one cycle.

*depth-cutoff* is the total planner-native bound: setup, every start, every integrated window, every explicit stop, and trailing setup all consume it. There is intentionally no generic per-cycle depth parameter. An open cycle's remaining local allowance would affect its future and would therefore have to be represented in graph identity; computing it only from a node's parent path would make equal databases merge unsafely. A problem needing a hard local limit should model the relevant progress in its own state. Guided chaining can apply a separate total cutoff to each search, but those locally bounded searches do not optimize one complete multi-cycle path.

Cycle-aware lower bounds and ordering are likewise problem-specific. A problem's existing min-steps-remaining? or heuristic? can read recording-in-progress, recorder-cycles-used, the live/ghost database, and its own persistent progress facts. Recorder supplies no domain-independent positive lower bound: the problem goal does not reveal how many additional cycles are necessary, and the final-open policy means even a closing stop is not universally required. Any authored lower bound must remain admissible for the problem's actual goal and permitted final closure policy.

For first searches, an inexpensive practical escalation is to try one cycle and only retry with two when the first search finds nothing:

(ww-set *max-recorder-cycles* 1)
(solve)
;; If no solution was found:
(ww-set *max-recorder-cycles* 2)
(solve)

This establishes only that a solution was found at the first attempted cycle limit that succeeded. It cannot establish global minimum length: a two-cycle solution may be shorter than every one-cycle solution, so stopping after the one-cycle success would miss it. To optimize the complete path, run min-length with the final cycle maximum and a suitable total depth cutoff.

A focused serial graph-search measurement with first and fixed total cutoffs characterized the current cost without running Rumin. The one-cycle snapshot fixture used 12 states and 8 search cycles at maximum one, versus 13 states and 9 search cycles at maximum two; both found the same depth-3 solution. The required-two-cycle fixture used 9 states and 8 search cycles before exhausting maximum one, versus 24 states and 18 search cycles to find its depth-7 solution at maximum two. Measured wall times were approximately 1.020 ms, 0.838 ms, 0.747 ms, and 2.090 ms respectively; at this scale timing order is dominated by measurement noise, so state counts are the useful comparison. These figures characterize the present fixtures and action order rather than predicting a large problem's branching factor.

problem-recorder-two-cycle-test.lisp is the focused non-flattenability proof. In its first cycle a ghost creates a transient priming condition that a live action consumes to latch a persistent plate; neither side can create the accepted boundary alone, and the priming condition itself does not survive. The second cycle's ghost can use the resulting recording-side gate only after stop normalization seeds the new snapshot. Moving both advances into one window fails isolated recording replay because the live latch action is absent there. With a seven-action depth bound the fixture has no solution when *max-recorder-cycles* is one, while one ordinary solve finds and reports the explicit two-cycle solution when the maximum is two.

Guided chaining remains a convenience rather than the implementation of planner-native cycles. (solve-subgoal goal) searches and commits one closed intermediate cycle; the following (solve) searches and commits the original problem goal as the last cycle. Each guided call strengthens its requested goal with both the exact next recorder-cycles-used value and ghost-stops-recorder, and dynamically narrows the search maximum to that next value. A call therefore cannot silently use multiple native cycles or commit an already-satisfied zero-cycle boundary. The configured *max-recorder-cycles* still limits the complete guided history and must be at least the intended number of calls.

The chain history retains each cycle's subgoal, strengthened goal, selected integrated solution, shared path-derived cycle report, immutable boundary state, search policy, and per-cycle plus cumulative depth, elapsed time, and value change. Its phase display delegates to the same cycle-report printer as an ordinary planner-native result, while its extra output is limited to guided-goal metadata and cumulative chain totals. Its accepted boundary likewise uses the primitive stop's normalizer, so ghost removal, shadow reset/seed/propagation, and cycle reporting each have one implementation. Search counters and minimum-length depth restart for every guided 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 its top — that base plus its height, default 1. A wall repeater's base is apparatus-coords>'s third coordinate, default 1; its height measures how far it projects horizontally from the wall, so its axis is horizontal and its top equals its base however large that height is. There is one anchor concept now, not four: a beam anchor is always the anchoring object's top. No separate wall identity is needed: apparatus-coords>, mounting subtype, 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 $rational) — a location's raw placement, the third coordinate optional, 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-walking-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 walking 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 walking 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 jumping/climbing edges; even two locations sharing one x/y point remain separate endpoints whose raw derived edge one-step-walkable rejects when their elevations differ (see problem-claustro-topo's slab and problem-phobia-topo's ground/loft pair). 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

-traversal.lisp owns the symmetric traverse-via and directed traverse-via> relations shared by walking, stairs, jumping, and climbing, along with their common DNF-family operations and the single traversal-segments provider. -walkability-coordinates.lisp derives the walking facts from geometry. Public walkability registers the walking predicate that requires equal endpoint elevations and a passable clause, turning currently enabled walking facts into normalized walk segments. -mobility.lisp composes those with the segments registered by stairs, ladder, and grounded jump into one canonical closure. -configuration-transition.lisp separately retains one support-changing boundary at a time, including a ladder climb that starts on a support. -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 managed problem parameter *vertical-reach-limit* (default 1, consumed by -support-elevation.lisp and shared with cargo reach -- see object-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's top or a location's base. 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. -vertical.lisp owns both geometric values through base and top; -support-elevation.lisp owns only the shared vertical-reach policy. Neither role makes jumping depend on box manipulation.

object-height — one height table

-vertical.lisp's object-height query reads an explicit has-height fact for a heighted-object, or falls back to the type's entry in *vertical-type-constants* for every vertical-object: gate, screen, and wall 4; edge and agent 3/2; box, jammer, connector, floor repeater, and wall repeater 1; and location, tray, fan, pressure plate, toggle plate, floor blower, angled blower, transmitter, receiver, and gun 0. Types outside heighted-object intentionally keep that table value. Wall gears and wall blowers are intentionally outside the table because their has-elevation denotes stream height rather than a solid object's base. 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 reads the resulting top, while LOS clearance, beam occlusion, and jammer aiming use the same vertical model, so their physical barrier heights cannot drift apart. Gate aiming averages base and top; hypothetical jammer placement adds object-height to placement-elevation because the jammer is still held while the action evaluates its options, making its current structural top inapplicable. An object outside the table signals an error rather than reading as zero.

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 beam anchor is its top: structural base plus that height. beam-relay.lisp directly nests -vertical for this geometry; its -placement dependency supplies -support-elevation's reach policy for connector pickup and placement.

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 author base as their segment relation’s trailing coordinate and may override height through 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. -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-viavisibility 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 traverse-via is symmetric — its repeated type is location, so the leading mode argument does not disturb the mirrored pair — while traverse-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.