How the technology files relate to each other, and how to add a new one without breaking what's already there.
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.
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.
| Kind | Owns | Examples | Rule of thumb |
|---|---|---|---|
| role | One composite either-type + its one relation (occasionally one helper query) |
-location.lisp → mobile-object/has-location;-support-occupancy.lisp → support + support-occupant/on/support-occupied/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; reachability → reachable; visibility → visible |
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. |
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.
open and (walkability only) -threat's safeWhat every file in tech/ currently owns, what it nests, and what kind it is. Kept up to date as the directory evolves.
| File | Owns | Nests | Actions | Kind |
|---|---|---|---|---|
-propagation.lisp | propagate-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 alone | — | — | role (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.lisp | physical-state-init-check validates raw location, holding, support occupancy, position, held-only tray support, and on cycles. cargo-physical-state-init-check requires every live cargo object to be located, held, or mounted when both foundational roles are installed; recorder ghost endpoints may begin absent. Held trays deliberately retain their holder's location so their occupants continue through ordinary location consumers | — | physical-state-init-check, cargo-physical-state-init-check | role (nested by the four physical role owners) |
-stream-passability-init-checks.lisp | Wall-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.lisp | Repeater 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 chroma | — | — | role (nested by -beam-substrate) |
-controls-init-checks.lisp | Controls DNF list contents and supported modes | — | — | role (nested by -controls) |
-beam-relay-init-checks.lisp | Positive connector pairing placement, limits, acyclicity, and potential sightlines; a pending coordinate-derived LOS pass satisfies the topology requirement when raw wall, edge, or boundary geometry activates it, while explicitly absent pairings, locations, and sightlines never do | — | — | role (nested by beam-relay) |
-jammer-init-checks.lisp | Positive initial jamming placement and potential target topology. An active jammer must be located and unheld. When public visibility is installed, a point target needs potential apparatus sightline topology, while a positioned drive may instead share the jammer's location; pending coordinate-derived LOS is accepted before init actions run | — | — | role (nested by jammer) |
-beam-crossing-init-checks.lisp | Positive authored beam/crossing endpoints, indexes, lists, reverses, sightlines, and gate prefixes; explicitly negative records never define or index crossing topology | — | — | role (nested by beam-crossing) |
-recorder-init-checks.lisp | Recorder mapping, exhaustive loose-cargo copying, located-object completeness, recording-side isolation, and supported-shadow boundary checks | — | — | role (nested by recorder) |
-recorder-core.lisp | recorder, 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, recorder-cycle-closed, and the strict recorder-cycle-stopped-by-ghost ending marker; 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. support-occupancy-conflict-p exempts exactly one pair from contending for a support top — a mapped live object and a mapped ghost one, which playback superimposes rather than stacks — so a live box and its ghost copy share the plate they were forked on; anything unmapped keeps the ordinary single-occupant rule. | -location, -holding, -position, -interaction-policy, -recording-shadow-policy | — | role (identity and isolation core; no apparatus state) |
-recorder-plate-shadow.lisp | recording-depressed, recording-latched, recording-plate-occupied, and update-recording-plate-status! | -recorder-core, -support-occupancy | — | role (capability-specific recorder shadow) |
-recorder-switch-shadow.lisp | recording-switched-on plus reset/seed lifecycle, keeping ghost toggles separate from ordinary switch state | -switch, -recorder-core, -propagation | — | role (capability-specific recorder shadow) |
-recorder-receiver-shadow.lisp | recording-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-substrate | — | role (capability-specific recorder shadow) |
-recorder-controls-shadow.lisp | recording-controller-energized and recording-control-on, aggregating recording switch, plate, and receiver state | -controls, -recorder-switch-shadow, -recorder-plate-shadow, -recorder-receiver-shadow | — | role (shared recording control service) |
-recorder-jamming-shadow.lisp | recording-jammed, filtering shared jamming facts to mapped ghost jammers | -recorder-core | — | role (shared recording jamming service) |
-recorder-gate-shadow.lisp | recording-open, the recording gate-view hook override, and update-recording-gate-status! | -recorder-controls-shadow, -recorder-jamming-shadow, -gate | — | role (capability-specific recorder shadow) |
-recorder-blower-shadow.lisp | recording-turning, the recording blower-view hook override, and update-recording-blower-status! for wall drives and fixed floor blowers. The fixed apparatus is not copied; affected ghost objects select this recorded activity state through blower-active-for-object | -recorder-controls-shadow, -recorder-jamming-shadow, -recording-shadow-policy | — | role (capability-specific recorder shadow) |
-location.lisp | mobile-object, has-location | — | — | role |
-location-coordinates.lisp | location-coords> (x, y, and an optional third coordinate: the location's own level, default 0); init check cross-checking that level against has-elevation | — | location-coordinates-init-check | role |
-apparatus-coordinates.lisp | apparatus-coords> for transmitters, receivers, repeaters, guns, and switches (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 0 | — | apparatus-coordinates-init-check | role |
-holding.lisp | cargo, holding | — | — | role |
-plate-types.lisp | pressure-plate/toggle-plate (optional), their plate union | — | — | role (nested by -controls, -position, -support-occupancy, and plate) |
-position.lisp | fixed-position-object (pressure-plate/toggle-plate/ladder/gears/fixed-blower leaves/recorder), has-position | -plate-types | — | role |
-interaction-policy.lisp | Neutral object-manipulation-allowed, support-use-allowed, support-occupancy-conflict-p, connector-pairing-allowed, and connector-location-conflict-p hooks. The two support hooks answer different questions: support-use-allowed is policy (may this occupant use this support at all), support-occupancy-conflict-p is capacity (do two occupants contend for the one top), defaulting to contention for every pair; -recorder-core overrides them without duplicating actions | — | — | role (hook/interface variant) |
-recording-shadow-policy.lisp | Neutral hooks selecting recording-shadow actors, recording-side object presence, blower-drive turning, and gate openness; ordinary objects use playback state, while -recorder-core and the matching apparatus shadow components override the hooks for ghosts | — | — | role (hook/interface variant) |
-height.lisp | heighted-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.lisp | support, support-occupant, on (occupant-keyed, not bijective: a support top holds one occupant per recorder layer), and the two queries that keep the physical and manipulation questions apart — support-occupied (is anything at all resting here, which is what a plate's weight reads) and cleartop (may this occupant take the top, decided by support-occupancy-conflict-p) | -plate-types, -interaction-policy | — | role |
-vertical.lisp | Type vertical-object; parameter *vertical-type-constants* (per-type height default, axis, and base default); queries base/top/fixed-base/object-height/location-elevation/location-level. location-elevation is the fixed, memoized entry point every caller uses and location-level is the overridable rule behind it, split so that an override inherits the memo instead of displacing it; the memo is sound because a location is never on anything, never held, and has no has-location or has-position, so its level is state-independent, and a redefinition of location-level must keep reading only static relations. 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, -holding | — | role |
-support-elevation.lisp | Reach policy only, no geometry: technology-owned *vertical-reach-limit* (default 1; overridable with defparameter after including the parent technology and also reused by jump.lisp), queries within-agent-vertical-reach/within-agent-placement-reach, and a conservative staged-model relevance diagnostic | -vertical | — | role |
-configuration-transition.lisp | Derived (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, -holding | — | role (explicit state-changing boundary; never transitively closed) |
-support-motion.lisp | Shared physical support enumeration, support/holding dependency checks, stack relocation (including nested held trays), and legacy blower landing selection. Same named location is required; no footprint or proximity inference. | -vertical, -location, -position, -holding, -support-occupancy | — | role |
-support-settling.lisp | Event-driven local tray-release settling: snapshot previous base, select the highest eligible surface, report tied maxima explicitly, and assign all landings before propagation. Preserves upper stacks and pairings. No global gravity sweep or recorder-closure change. See scope and tests. | -support-motion | — | role |
-placement.lisp | Queries 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 settles local tray riders before propagation and retains remote unloading. Surface enumeration and dependency traversal belong to -support-motion; release selection belongs to -support-settling. 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-policy | — | role |
-pickup.lisp | Query 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, -reachability | — | role |
-gate.lisp | gate (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-policy | — | role |
-switch.lisp | Optional switch type and persistent switched-on state | -apparatus-coordinates | switch-init-check | role |
-controls.lisp | Optional gate, gears, fixed-blower, receiver, switch, and gun leaves; controls uses a DNF OR-list of AND-lists of receiver/plate/switch controllers over those devices (mode normal | inverted); queries energized and control-on provide the shared aggregate | -plate-types, -beam-substrate, -switch | — | role |
-gears-fan.lisp | Optional 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 update-blower-status!. Shared landing-support, relocate-stack!, and land-on-support! are supplied by -support-motion. 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-registry | gears-fan-init-check, gears-fan-mounting-init-check; pickup-fan, put-fan, mount-fan | composite substrate (peer substrate for the three mounting technologies) |
-floor-blowing.lisp | location-level — -vertical’s overridable seam, not its memoized location-elevation entry point — 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, selecting playback or recorded activity independently for each affected object. 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, -vertical | floor-blowing-init-check | role (shared floor-directed physics; nested only by floor-gears and floor-blower) |
-passability.lisp | Optional screen, ladder, gears, and fixed-blower leaves; queries obstacle-clear/all-clear, with an actor-aware default for stream-obstacle-clear | -holding, -gate | — | role |
-stream-passability.lisp | stream-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-coordinates | — | role (nested only under wall-blower) |
-traversal.lisp | Type 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, which caches by content over traversal-segments-for-source — the computation that iterates modes and destinations and takes the first clause a mode accepts; the cache key is the agent, the source, every parameter a builder registered through register-traversal-cache-parameter, and the state’s projection onto *traversal-state-dependencies*, so nothing needs invalidating. That list is deliberately narrower than the builders’ transitive read set: has-location and on are reached only through base applied to locations, gates, screens and walls, none of which is ever supported, held or located, so those reads are vacuous. *traversal-cache-paranoid* is what holds that argument to account — it recomputes every hit and signals on a difference; 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 | -mobility | traversal-init-check | role (traversal topology substrate) |
-mobility.lisp | Pure traversal-provider registry and canonical breadth-first closure over normalized (mode source witness destination) segments; returns one shortest deterministic route per grounded destination | — | — | role (transparent grounded travel only) |
-mobility-action.lisp | Normalizes 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-transition | move | role (central movement action) |
-reachability.lisp | reach-target (location or switch) and identity-default reachable query | — | — | role (hook/interface variant) |
-visibility.lisp | repeater (floor-repeater/wall-repeater), optional gate/transmitter/receiver/floor-repeater/wall-repeater/gun leaf types, null-default ordinary, beam, and endpoint-elevation-aware visibility queries whose target domains name the point-apparatus leaves directly, and shared initialization helpers for authored or pending coordinate-derived LOS topology | — | — | role (hook/interface variant; point apparatus are functional points with no has-position or occupancy shortcut) |
-threat.lisp | threat (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.lisp | beam-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-policy | — | role (factored out of beam-direct; also nested by visibility once its sightline occluders gain location entries) |
-beam-substrate.lisp | repeater/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.lisp | Default beam-elevation-at-location hook: horizontal fixed beams need no coordinates; sloped beams require the coordinate-aware override supplied by public visibility | -beam-substrate | — | role |
-segment-geometry.lisp | Optional 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 technology-owned *boundary-wall-height* defaults to 6 and may be overridden with defparameter after including the parent technology; it has no named object and therefore no authored base or has-height. | — | segment-geometry-init-check | role (shared by walking, LOS, and vertical geometry) |
-beam-los-coordinates.lisp | Optional floor-repeater/wall-repeater/jammer/gun, los-endpoint, technology-owned geometric tolerance *beam-occlusion-tolerance* (default 1/2; overridable with defparameter after including the parent technology), 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-geometry | derive-los-from-segments (init) | role (nested under visibility and -beam-crossing-coordinates) |
-beam-crossing-coordinates.lisp | Optional 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-coordinates | establish-beam-coordinates, derive-beam-crossings-before-gate | role (nested only under beam-crossing) |
-walkability-coordinates.lisp | Derives 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-geometry | derive-walking-from-segments (init) | role (nested under walkability and -stream-passability) |
-terrain-consistency.lisp | Cross-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, -vertical | — | role (nested by public walkability; never included directly by a problem) |
-elevation.lisp | elevated-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-coordinates | — | role |
elevation.lisp | Public problem-facing wrapper for -elevation | -elevation | — | role wrapper |
walkability.lisp | Registers 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-action | move (central mobility action) | service (walking mobility provider) |
topo-lower-bound.lisp | Optional problem-facing lower-bound service. Its inexpensive finite-domain location/resource query is the only term registered for automatic search pruning; it keeps one location per agent and adds disjoint movement, manipulation, gate-controller, and recorder-session costs. Delete-relaxed topo-relaxed-hmax-bound/topo-relaxed-lm-cut-bound, the analysis-only topo-finite-beam-resource-bound, and the combined topo-lm-cut-resource-bound remain explicit diagnostics rather than automatic fallbacks. A problem may define its own min-steps-remaining? aggregate after including this service. The engine then evaluates the registered finite-resource term first and adaptively samples an unproductive aggregate during serial search; the technology-owned warmup and sampling defaults remain hidden technical settings. The engine also skips every lower-bound evaluation while neither a depth cutoff nor an improvable incumbent exists. *min-steps-pruning-enabled* is a hidden technical kill switch for comparative runs, not a persisted problem parameter. Progress reports distinguish registered-contributor evaluations/prunes from aggregate evaluations and unique prunes. The generated relaxed operator model is built once, backward-sliced from the active goal, and reused by explicit diagnostics with only goal-relevant current facts. The model comes from ordinary types, state, positive conjunctive goal literals, and static traversal/reach/control facts: one MOVE may span the traversal closure; placement ignores clearance, interaction policy, and vertical limits; each wall switch may be turned on by one relaxed toggle; and one beam-affecting action may activate every receiver. Normal controls retain receiver, plate, and switch dependencies. Inverted control, jammer bypass, custom writers, and blower cascades retain permissive one-action fallbacks. Domains with exogenous happenings contribute zero, as do unknown, quantified, disjunctive, negative, and other unsupported goals. Because problem-defined actions may add transitions outside this generated abstraction, it is not certified as a complete unreachability proof. | Engine ww-relaxed-heuristics; reads installed topology and physical-role schemas without nesting another public technology | — | service (optional search pruning) |
stairs.lisp | Registers 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-action | move (central mobility action) | service (stairs mobility provider) |
reachability.lisp | reach-via between reach targets, directional reach-via> from a location, extended reachable, and reachable-clear | -reachability, -gate | — | service |
visibility.lisp | One 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-occlusion | visibility-init-check | service |
gate.lisp | jammer (optional), update-gate-status! (sole asserter of -gate's open; controls/energized now live in nested -controls) | -controls, -gate | — | hub |
plate.lisp | depressed/latched, update-plate-status! (pressure-plate/toggle-plate/plate come from nested -plate-types) | -plate-types, -support-occupancy | — | assembly (no actions) |
recorder.lisp | The problem-facing recorder assembly. It composes identity/isolation, repeatable session transitions, switch, plate, receiver, controls, jamming, gate, and blower-drive 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. Fixed floor blowers have one apparatus identity but side-specific activity effects: live objects read playback state and ghosts read recorded state. Initialization still refuses capabilities outside the assembled shadow rather than approximating them: beam crossings, floor gears, angled blowers, threats, movable wall-fan copies, and receiver-controlled blower drives | -recorder-core, -recorder-controls-shadow, -recorder-jamming-shadow, -recorder-gate-shadow, -recorder-blower-shadow, -recorder-solution, -recorder-session, -recorder-cycle-boundary, -recorder-cycle-chaining, -recorder-init-checks | start-recorder, stop-recorder, cancel-playback (via -recorder-session) | assembly (composition plus stage-time service registration) |
switch.lisp | Wall-mounted switch assembly; toggling requires authored horizontal reach plus vertical reach, permits held cargo, and selects ordinary or recorder-shadow state by actor | -switch, reachability, -support-elevation, -propagation, -recording-shadow-policy | toggle-switch | assembly |
-recorder-solution.lisp | Recorder path services: a state-machine parser for repeated setup/start/window/ending segments and an optional final open window; mandatory STOP/CANCEL 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, -holding | — | role (nested by recorder; never included directly) |
-recorder-session.lisp | Repeatable recording transitions. start-recorder requires a clean closed state, an empty-handed live agent at a recorder, and either an unused slot below *max-recorder-cycles* or an unlimited (nil) setting; 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. cancel-playback instead requires an empty-handed live agent at a recorder during an open cycle; it immediately discards the unfinished ghost playback and every ghost dependency while preserving current live state. Nonphysical links such as connector pairings disappear with their ghost endpoint. | -recorder-cycle-boundary, -location, -holding, -support-occupancy | start-recorder, stop-recorder, cancel-playback | role (nested by recorder; never included directly) |
-recorder-cycle-boundary.lisp | The shared boundary contract for planner transitions and guided chaining. A successful normal stop or cancellation 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. recorder-cycle-ended recognizes either ghost STOP or live cancellation at a ghost-free boundary; ghost-stops-recorder remains the stricter normal-stop query. prepare-recorder-cycle-state copies an ended state and delegates to the same idempotent normalizer. | -recorder-core, -recorder-solution, -propagation | — | role (nested by recorder; never included directly) |
-recorder-cycle-chaining.lisp | Recorder-aware policy behind generic ordered-milestone search. solve-subgoal commits a full-state checkpoint without forcing a recorder boundary, while generic goal chaining retries earlier milestone searches when a later bounded continuation is exhaustively impossible. Every retained alternative is projected into recorder segments and the complete accumulated path is replayed from the original staged state; the following solve appends the original problem goal and retains one cumulative solution. Checkpoint history records each goal, segment solution, saved search policy, cumulative metrics, and open/closed cycle status. Final reporting derives actual cycles from the integrated path, and ww-undo reverses one user command including its automatic recovery. | -recorder-cycle-boundary; generic goal chaining | — | service (serial recorder replay and lifecycle policy) |
floor-gears.lisp | Public entry point for mountable floor-gears used with removable cargo fans | -floor-blowing | — | assembly (removable-fan floor mounting) |
floor-blower.lisp | Public entry point for fixed combined floor-blower objects | -floor-blowing | — | assembly (fixed floor units) |
wall-blower.lisp | update-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-passability | — | assembly (wall mounting; stream elevation via blower-elevation, default 1) |
angled-blower.lisp | update-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-fan | angled-blower-init-check | assembly (angled mounting and fixed angled units; both support surfaces are flush and steppable) |
step.lisp | steppable-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, -position | move (central movement action) | assembly (flush ground-level support changes) |
box.lisp | plate/box (optional) | -placement, -reachability, -pickup | pickup-box, put-box | assembly |
tray.lisp | tray (optional); a carryable, stackable support only while held — grounded, it is inert, and local release settles riders onto the highest unique eligible support no higher than their previous base, or ground; remote release retains ground unloading. 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, -pickup | pickup-tray, put-tray | assembly |
jump.lisp | box/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-action | move (grounded routes and support changes) | assembly (barrier-clearing mobility plus explicit support transitions) |
jammer.lisp | target 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. jammer-init-check rejects a positive initial jamming fact whose jammer is held or unlocated and validates potential physical target topology when public visibility is present. | -vertical, -gears-fan, -placement, -reachability, -visibility, -jammer-init-checks, -pickup, -recorder-fork-registry | jammer-init-check; pickup-jammer, jam-target | assembly |
gun.lisp | update-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, -controls | — | assembly (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.lisp | ladder (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-action | ladder-init-check; move (central mobility action) | service (directed ladder mobility plus supported-source transition) |
beam-direct.lisp | Fixed 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, visibility | derive-fixed-beam-barrier-crossings (init) | peer-plugin |
beam-relay.lisp | relay/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-registry | pickup-connector, put-connector, connect-connector | peer-plugin (connector actions plus connector/repeater propagation) |
repeater.lisp | Public fixed-relay assembly and its floor/wall mounting contract; no location or actions | beam-direct, beam-relay, visibility | — | assembly |
beam-crossing.lisp | transmitter (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, -gate | — | peer-plugin |
Relaxed-heuristic runtime: after goal relevance is compiled, ww-relaxed-heuristics maps the retained facts and operators to integer vectors and builds reusable dependent and achiever indexes. H-max and LM-cut then share those indexes instead of reconstructing per-state hash indexes and allocated justification arcs. analyze-topo-relaxed-lm-cut returns domain-independent records for every cost-partitioning round, including the selected goal and each cut operator's name, supporter, preconditions, and goal-entering effects; report-topo-relaxed-lm-cut-analysis prints a compact operator-family summary for any staged Topo goal.
Tray relaxation: ordinary tray placement supplies the tray's destination, and zero-cost support consequences propagate that destination only through the retained on chain. This preserves joint one-action relocation for actual nested riders without allowing unrelated support occupants to hitchhike.
Finite-domain resource bound: supported cargo-location goals are routed without accumulating deleted agent locations. The bound minimizes over task order, agent assignment, and across-reach vantages, then adds disjoint MOVE, pickup/placement, and recorder-session costs. analyze-topo-finite-resource-bound returns those three components, the supported goals, and retained cargo tasks without changing the production estimate; report-topo-finite-resource-bound-analysis prints the same decomposition for any staged Topo state and goal. The diagnostic-only analyze-topo-control-setup then sets those already-charged action classes to zero and reruns LM-cut in shared- and dedicated-effect modes. Its typed variant replaces the generic action trigger with plate, receiver, and relocation triggers derived from installed capability types and abstract operator families: beam-blocker movement/manipulation can change receivers, plate changes can propagate through gates, and explicit overlap such as placing cargo on a plate remains intact. A final beam-provenance view ignores hue and corridor clearance while following directional coupled and undirected paired links from every transmitter. It reports one only when the permissive route model still needs an unlinked receiver, so standard relay topology requires a separate connector-link action; existing structural links and alternate routes remain free. The view abstains for happenings, unsupported relay providers, missing connectors, and connector manipulation already charged by the finite-resource term. The reporter lists the receiver classification and binary link cut, and topo-finite-beam-resource-bound exposes its sum with the finite term for explicit analysis. Bounded search measurements showed that evaluating this relaxed beam model either on every selected node or through the reactivating aggregate fallback costs more than its extra prunes save, so it is deliberately not registered for production pruning. Carried trays and tray riders retain their specialized movement semantics; propagating relocation and exogenous happenings cause the partial resource term to abstain. Static reach/traversal indexes and the concrete state-fact extraction are shared with the relaxed evaluator.
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.
-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 — and shared means simultaneously: a support top carries one occupant per layer, so start-recorder's fork puts a ghost box on the very plate its live original already stands on, and both stay. Weight remains layer-blind, since ghost objects have weight (rule 15). Lit connectors exclude another placement only on their own recorder layer, so a live connector does not make the corresponding physical location unavailable to a ghost connector or vice versa. 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 blower-drive 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 either the stateful cycle count is below *max-recorder-cycles* (default 1) or that parameter is nil for no limit. The count increments at start even when use is unrestricted, so reports and validation retain exact cycle numbers and 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 and cancel-playback are cost-1 planner actions just like start-recorder. Normal STOP requires every mapped ghost agent at a recorder and empty-handed. It 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. Live cancellation instead requires an empty-handed live agent at a recorder while playback is open; it deliberately permits an unfinished ghost, cargo, and cross-layer support dependencies because the remainder is being discarded. Either ending 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. recorder-cycle-ended recognizes both endings; ghost-stops-recorder additionally requires the strict normal-stop marker. Recording and playback remain two derived views of the one integrated path; each ending 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, ending, then setup again. It accepts STOP or CANCEL as an explicit ending, any number of completed cycles up to the stateful maximum (or without a maximum when the parameter is nil), trailing setup after an ending, and one optional final open cycle. Nested starts, unmatched endings, and starts beyond a finite maximum 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 ending and subsequent setup. It replays that cycle's real start, ghost moves, and optional normal STOP in isolated recording view; live CANCEL is playback-only and is checked by the integrated replay. It 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 ending that created their normalized boundary. Rejected nominal goals remain searchable states rather than solutions.
Every generated stop-recorder or cancel-playback successor receives mandatory cycle-boundary 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 is a technology-owned diagnostic and remains off by default. An experiment may place (defparameter *recorder-prefix-pruning* t) after (include-tech recorder); it then 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 only when the finite 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 supported blower-drive state. Fixed floor blowers are not copied: floor-stream launch and sustain checks select ordinary turning for live objects and recording turning for ghosts, so playback-only activations do not alter recorded ghost motion. Initial jamming facts must likewise name mapped jammers. Beam crossings, receiver-controlled blower drives, floor gears, 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, cancelled, 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 ending when present. Actions after the final ended 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 a normal recording sequence in place. cancel-playback is omitted from recording view and retained at the end of playback view with the live actions that led to it. A final goal reached before a searched ending 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; a positive integer imposes that finite limit and nil permits unrestricted starts. At the default value the closed-boundary dominance frontier is disabled completely; it remains active for eligible unbounded graph searches so equivalent boundaries with needlessly greater cycle histories do not grow the graph without limit. Mandatory completed-cycle validation reconstructs a path only for a generated STOP or CANCEL successor, and rejects invalid or no-progress cycles before ordinary graph insertion. The technology-owned diagnostic *recorder-prefix-pruning* defaults to nil; enabling it spends additional replay work after starts and ghost moves in exchange for abandoning an unplayable open recording earlier. It is not a persisted problem parameter and is omitted from (params). Exact live/ghost interleaving canonicalization remains automatic for serial non-hybrid depth-first search. Raising or removing 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 ending, 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-bound terms and ordering remain problem-specific. The optional topo-lower-bound.lisp service automatically contributes only its finite-resource term; its explicit relaxed h-max and LM-cut diagnostics can model a recorder start and either ending, but deliberately ignore other ghost-object goals. The problem goal does not reveal how many additional cycles are necessary, and the final-open policy means even a closing action is not universally required. A problem may define its own min-steps-remaining? or heuristic? from recording-in-progress, recorder-cycles-used, the live/ghost database, and persistent progress facts. Any added cycle-aware bound must remain admissible for the 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 problem whose rules impose no recorder-use limit may instead set (ww-set *max-recorder-cycles* nil). The total depth cutoff still bounds any finite search run, and eligible graph searches retain recorder-boundary dominance to suppress equivalent histories that merely used more cycles.
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 for the next user-selected milestone without adding a cycle count or closure requirement. A checkpoint may be before a recording starts, inside an open recording, or after one or more boundaries; actual START, STOP, and CANCEL actions determine the cycle structure. If the next milestone exhaustively has no solution, generic goal chaining records that the exact preceding endpoint is unsuitable for that complete requested suffix under its saved cutoff and feasibility-affecting settings. It restarts the preceding milestone search while excluding that contextual endpoint, retries the suffix, and propagates exhaustion backward as necessary. It retains only the active phase solutions and exact nogood keys rather than a frontier or candidate pool. An interrupted, cancelled, capped, out-of-memory, or otherwise incomplete search is unknown: it rejects no checkpoint and leaves the last committed chain intact. The following (solve) searches for the original problem goal and returns one cumulative solution and recorder report. Earlier subgoal formulas remain ordered milestones rather than persistent state constraints unless the user repeats them in a conjunction. An advanced subgoal may request recorder-cycle-ended explicitly when a boundary is intentional.
The generic checkpoint identity includes both planner databases, happenings, time, value, and the staged-problem generation. Recorder supplies only its cycle/resource context and saved Talos search settings. When revisiting a phase, the engine restores its depth cutoff, search algorithm, graph/tree mode, solution type, ordering and random state, symmetry and waiting controls, lower-bound switch, and the registered recorder/Talos settings. A contextual nogood includes the entire later goal suffix and those settings, so changing a later formula or cutoff cannot reuse an invalid stale rejection. Rejected goal states are filtered before graph duplicate handling but remain ordinary searchable successors, allowing a descendant to satisfy the milestone differently. first obtains the next endpoint by restart-and-exclusion; min-length reruns its normal bounded optimization over non-rejected endpoints; a positive solution count remains a transient candidate batch whose unselected members are not retained.
The recorder chain history retains each accepted goal, segment solution and exact endpoint, saved solution policy, cumulative depth, elapsed time and value change, and recorder cycle count and open/closed status. Every tentative retained prefix is replayed exactly from the original staged state and passes validate-recorder-solution; a successful alternative replaces the rejected suffix atomically. Recorder phases and closures are not assigned to checkpoints or stored separately, and final display delegates to the ordinary path-derived recorder report. Search counters and objective optimization restart for every segment, so the combined result makes no claim of global minimum total length. Chaining requires *threads* = 0. ww-undo reverses one explicit solve-subgoal or mid-chain solve command: after automatic recovery it restores the formerly selected prefix in one step, including recorder history, alternatives, and contextual nogoods. Staging or resetting a problem clears the chain, undo history, policy, retained alternatives, and nogoods. An incomplete accepted chain can instead be carried across a restart with export-subgoal-progress and import-subgoal-progress. The human-readable .txt archive contains milestone goals, saved settings, action segments, and symbolic endpoint facts. Import requires a freshly staged matching problem, replays and validates the entire prefix, and builds new stage-local state keys and recorder segments rather than restoring serialized planner objects.
Checkpoint candidate screening is a separate generic layer with three results. impossible requires a sound proof and excludes only the exact state under the original final goal and applicable remaining-budget context; possible requires a concrete replayable continuation; unknown retains the candidate. Engine inconsistency and a relaxed model explicitly certified complete for unreachability are generic proof sources. The optional Topo lower-bound service contributes its explicit-goal finite-resource bound only when the known remaining phase cutoffs form a finite total budget, and rejects only when the admissible bound is strictly greater than that budget. Recorder contributes normalization and cycle-state consistency without moving recorder semantics into the engine. Ordinary heuristic scores, incomplete relaxed models, interrupted or failed bounded probes, timeouts, resource caps, and out-of-memory results remain unknown and cannot remove a checkpoint.
Proof-based screening rejections are stored separately from exhaustive continuation nogoods. Their key includes the exact checkpoint, original final goal, complete later request suffix, and derived remaining-depth budget; changing a goal or cutoff therefore cannot reuse stale screening evidence. Automatic recovery, ww-undo, and staging treat both stores transactionally. Screening ranks or removes only user-requested milestone candidates: it does not generate subgoals or turn uncertain landmarks into mandatory checkpoints.
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 open — walkability (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.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.
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> is the XY coordinate of an apparatus's functional point: the point where a transmitter, repeater, or gun emits or acts, where a receiver accepts a beam, or where a switch is mounted. It does not create a walkable or occupiable location. Transmitters, receivers, switches, and guns have zero physical height; their functional elevation defaults to 1. A switch or 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.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. Co-located gates and screens form one conjunctive clause, so every fixture must pass; a stream curtain remains exclusive because its directional ride behavior is not ordinary conjunctive passability. 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 after one vertical consistency distinction: a gate or screen may share a wall/edge interval when the solid's top is at or below the door's base, in which case the planar interval remains solid and an authored level-change edge handles the upper doorway; a genuine vertical overlap is rejected. Otherwise segment coverage remains 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.lisp supplies the baseline reachable query used by manipulation actions: a target and the actor's location are reachable when they are the same location. Its reach-target role also admits fixed switches. 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; switch manipulation authors an edge from a nearby location directly to the switch apparatus. As with the beam hooks, nested-include deduplication installs the default once before any override, independent of the problem's include order.
-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, which is content-cached on the state’s projection onto the dynamic relations the mode builders read. -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.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.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.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 raised supports are singleton configuration-transition routes consumed by the same move action, so the closure still stops at every support-state boundary. A box offers its top wherever it is located; a tray offers its top only while another agent is holding it. Grounded trays remain inert, and recorder-side policy separately decides whether the jumping agent may use the held tray.
Clearing a barrier and rising in elevation are the same physical constraint, both bounded by the identical technology-owned *vertical-reach-limit* (default 1, defined 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.
-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.
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.
| File | Calls (soft) | Actually defined in | Requirement |
|---|---|---|---|
gate | jamming | jammer | Conditional on nonempty jammer |
-recorder-jamming-shadow | jamming | jammer | Conditional on nonempty jammer; recording-jammed filters the shared relation to mapped ghosts |
-controls | depressed | plate | Conditional on nonempty plate (read via energized, shared by gate and the blower techs' gears) |
-beam-los-coordinates | los-via | visibility 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> facts | Conditional on the problem authoring position facts and including visibility |
For a new object type, work through these in order.
jump's vaultable-object or jammer's target). Don't create a substrate file for something with exactly one user.-gears-fan does for the three blowers; it remains dash-prefixed because it remains private.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."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.$-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.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.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.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.include-tech yet, in which case the testing waiver applies.