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.
| 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/cleartop |
Nest it, never hand-copy it. Exactly one owner per role, full stop. |
| service | Capability-specific relations and derived queries built on top of roles, but no object-role composite of its own | accessibility → accessible; reachability → reachable; visibility → visible |
Owns the authored edges or sightlines for one capability. Usually action-free; accessibility also supplies move. |
| assembly | A leaf type (declared optional) + whichever roles it nests + its own bespoke relations/queries/actions | box, agent, plate, jammer, ladder, barrier |
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 accessibility, 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.
openWhat 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 |
|---|---|---|---|---|
-location.lisp | mobile-object, has-location | — | — | role |
-location-coordinates.lisp | location-position> | — | — | role |
-holding.lisp | cargo, holding | — | — | role |
-position.lisp | fixed-position-object, has-position | — | — | role |
-height.lisp | heighted-object, has-height (box/fence/gate/agent/screen/wall/jammer/connector), query declared-height (shared default-1 fallback for box/agent/jammer/connector; jump clearance uses jump-barrier-height) | — | — | role |
-support-occupancy.lisp | support, support-occupant, on, query cleartop | — | — | role |
-support-elevation.lisp | Queries support-top-elevation/occupant-elevation/within-agent-vertical-reach | -support-occupancy, -location, -position, -height, -elevation | — | role |
-placement.lisp | Query placement-options (legal plate/fan/box/ground placements for a carried object; mounting a fan on gears is -gears-fan's own mount-fan instead, and a wall-mounted fan has no has-location so it is never offered), update place-held-object! | -support-elevation, -holding | — | role |
-pickup.lisp | Query pickup-clear (agent empty-handed, object's location reachable, object's elevation within vertical reach) | -placement, -reachability | — | role |
-gate.lisp | gate (optional), open — asserted only by gate.lisp's update-gate-status! | — | — | role |
-controls.lisp | Optional gate/floor-gears/wall-gears/plate/receiver/mode, controls (DNF OR-list of AND-lists of receiver/plate controllers over (either gate floor-gears wall-gears) — leaf types, since this file splices before -gears-fan installs the gears union; mode normal | inverted), query energized | -beam-substrate | — | role |
-gears-fan.lisp | floor-gears/wall-gears/fan (optional), gears union, aimed-at>, mounted-on (the fan's attachment to its gears; gears are not a support), welded (static; marks a fan/gears pair inseparable — pickup-fan refuses to part them), derived turning/blowing, query gears-elevation (wall stream height, declared or 1; floor elevation for floor gears), update-gears-status! (state only — each mounting tech owns its consequences update), relocate-stack! | -support-occupancy, -location, -position, -elevation, -controls, -placement, -reachability, -pickup | pickup-fan, put-fan, mount-fan | role (peer substrate for floor-blower/wall-blower) |
-passability.lisp | Optional screen/ladder, queries obstacle-clear/all-clear | -holding, -gate | — | role |
-accessibility.lisp | Identity-default accessible query | — | — | role (hook/interface variant) |
-reachability.lisp | Identity-default reachable query | — | — | role (hook/interface variant) |
-visibility.lisp | fixture, transceiver composite, optional fixture leaf types, null-default visible/potentially-visible queries | — | — | role (hook/interface variant) |
-beam-substrate.lisp | hue/transmitter/receiver (optional), active, has-chroma, null-object default hooks, update-receiver-status! | — | — | role (hook/interface variant) |
-beam-los-coordinates.lisp | Optional jammer, beam-endpoint composite (either transmitter/receiver/location), transceiver-position> (transmitter/receiver only — location coordinates come from the nested -location-coordinates instead), wall-segments/gate-segments/boundary-wall (derives los-to-transceiver/los-to-target/los-to-location when asserted, in place of hand-authoring; boundary-wall's closed polygon is folded into the LOS wall list) | -location-coordinates | derive-los-from-segments (init) | role (nested under visibility and -beam-crossing-coordinates) |
-beam-crossing-coordinates.lisp | Optional crossing type (empty default); derives crossings-along-beam>/crossings-before-gate> (declared by beam-crossing.lisp) from the LOS geometry -beam-los-coordinates already derived or the problem hand-authored | -beam-los-coordinates | establish-beam-coordinates, derive-crossings-before-gate (init, in this order, both after -beam-los-coordinates' own derive-los-from-segments) | role (nested only under beam-crossing) |
-accessibility-coordinates.lisp | Optional wall-segments/gate-segments/window-segments (same shape as -beam-los-coordinates's own wall-segments/gate-segments), derives walk-via by classifying each location's side of every wall/gate/window partition line | -location-coordinates | derive-walk-via-from-segments (init) | role (nested only under accessibility) |
-elevation.lisp | elevated-object (location/gate/screen/fence/wall/transmitter/receiver/wall-gears), has-elevation, queries object-elevation/location-elevation/fixture-elevation; omitted location/barrier bases default to 0 while transmitter/receiver beam anchors (and wall-gears streams, via gears-elevation) default to 1 | — | — | role |
elevation.lisp | Public problem-facing wrapper for -elevation | -elevation | — | role wrapper |
accessibility.lisp | walk-via, extended accessible, one-step-accessible | -support-occupancy, -location, -passability, -elevation, -accessibility, -accessibility-coordinates | move | service (the one with an action) |
reachability.lisp | reach-via, extended reachable, reachable-clear | -reachability, -gate | — | service |
visibility.lisp | los-to-transceiver, los-to-target, los-to-location, extended visible/potentially-visible, visible-clear | -visibility, -gate, -beam-los-coordinates | — | 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 | plate (optional), depressed, update-plate-status! | -support-occupancy | — | assembly (no actions) |
floor-blower.lisp | location-elevation override (an undeclared floor-gears blow destination floats at default hover elevation 10), update-floor-blower-status! (launches every occupant of a blowing floor-mounted fan — agent, box, jammer, connector — to the gears' aimed-at> destination, where they hover only while a blowing floor-mounted fan aims there, falling to ground at the gears' location when the stream stops; jamming/pairing facts persist through launch and fall, effects re-derived by propagation; only a fan is toppled to local ground) | -gears-fan | — | assembly (floor mounting; agent mounts the flush fan via step) |
wall-blower.lisp | update-wall-blower-status! (a blowing wall-mounted fan sweeps its gears' faced location at stream level: an occupant standing at s with height h is blown iff s < stream ≤ s + h, torn off its support, relocated to the aimed-at> destination with its riders; jamming/pairing facts persist, effects re-derived by propagation from the destination; zero-thickness fans are never struck; a wall-mounted fan hangs with no has-location, so nothing can rest on it and it is invisible to step/placement) | -gears-fan | — | assembly (wall mounting; stream elevation via gears-elevation, default 1) |
step.lisp | steppable (either plate fan; a fan only while mounted on gears) | -support-occupancy, -location, -position | step-on, step-off | assembly (flush ground-level mount/dismount; jump stays elevation-only) |
box.lisp | plate/box (optional) | -placement, -reachability, -pickup | pickup-box, put-box | assembly |
jump.lisp | box/fence/wall (optional), jump-feature, symmetric jump-via, directed jump-via>, landing and highest-clearance queries | -support-elevation, -passability | jump-to | assembly (unified ordinary and barrier-clearing jumps) |
jammer.lisp | target composite, plate/jammer/box (optional), jamming, jam-disallowed> | -placement, -reachability, -visibility, -pickup | pickup-jammer, jam-target | assembly |
ladder.lisp | ladder (optional), directed climb-via>, query one-way-clear | -support-occupancy, -location, -position, -passability | use-ladder | assembly |
beam-direct.lisp | beam-blocker composite, transmitter/receiver (optional), coupled, beam-via, elevation-aware query beam-clear + hook overrides | -beam-substrate, -location, -support-occupancy, -height, -elevation, -gate | — | peer-plugin |
beam-relay.lisp | terminus composite, several optional types, paired, color, update-connector-status! + hook overrides | -beam-substrate, -placement, -visibility, -accessibility, -reachability, -pickup | pickup-connector, put-connector, connect-connector | peer-plugin (the one with actions) |
beam-crossing.lisp | transmitter (optional), crossing-active, beam-crossing>, update-crossing-status! + hook overrides | -beam-substrate, -beam-crossing-coordinates, -gate | — | peer-plugin |
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.
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 — accessibility (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 the problem's own propagate-consequences! must still call it before anything downstream reads a current value. 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 floor-blower's gears became a second controlled device; each consumer still evaluates its own DNF aggregate in its own update, because the uncontrolled default differs (a gate reduces to open <=> jammed, gears to always-turning).
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 also reads -elevation and -height so fixture elevations set the horizontal beam level. Transmitter and receiver anchors default to elevation 1; gates remain base-elevation objects defaulting to 0. Explicit has-elevation facts override either default. A location in beam-via blocks only when a beam blocker (agent/box/jammer/connector) with has-location there has a base-to-top vertical span that intersects that level.
-location-coordinates.lisp owns nothing but (location-position> location $rational $rational) — a location's raw 2D placement, independent of any capability that might consume it. Both -beam-los-coordinates (nested under visibility, and transitively under beam-crossing via -beam-crossing-coordinates) and -accessibility-coordinates (nested under accessibility) 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.
-accessibility-coordinates's own derive-walk-via-from-segments also illustrates that a coordinate-derivation substrate need not use the same algorithm as its sibling: -beam-los-coordinates tests straight-line segment intersection (right for sightlines and beams), while -accessibility-coordinates classifies each location by which side of every wall/gate/window partition line it falls on (right for walking, where the real path detours through a doorway rather than following a straight line). See that file's own header for the full reasoning.
-reachability.lisp supplies the baseline reachable query used by manipulation actions: two locations are reachable when they are identical. box, jammer, and beam-relay nest this substrate, so they remain self-contained without authored reach edges. Including the public reachability technology overrides the same query with identity plus reach-via edges and gate checks. As with the beam hooks, nested-include deduplication installs the default once before any override, independent of the problem's include order.
-accessibility.lisp supplies a baseline accessible query containing only the starting location. Including the public accessibility technology overrides it with the current passable walking closure and adds the move action. beam-relay consumes this hook for pairing vantages without owning locomotion.
-visibility.lisp supplies visible and potentially-visible 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. visible requires every authored gate occluder to be open; potentially-visible requires only that the LOS relation exists.
beam-relay uses potentially-visible across every location in the agent's current accessible closure when selecting pairings, before placing the connector. Walking obstacles use current passability when forming that closure, but gates listed on a potential LOS need not be open. Live lighting and receiver propagation continue to use visible from the connector's exact placed location. The same file owns put-connector, which shares placement rules with connect-connector but creates no pairings; a separate connector assembly would split one object's lifecycle across sibling technologies.
-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. accessibility, jump, and ladder nest it for walking, jumping, and ladder traversal respectively. Public accessibility 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, fences, and walls contribute their top elevations, and the agent must clear the highest one. The same action generates ground and clear-box landings, so barrier crossing does not duplicate landing semantics in a separate action.
Case study. Jump clearance needs the agent's standing elevation, which may come from either a box or the location floor. Keeping occupant-elevation in the box technology would make jumping depend on box manipulation. The shared -support-elevation.lisp role lets jump.lisp work from raised locations while still supporting box departures and landings when boxes exist.
Until this document was refreshed, box.lisp, agent.lisp, and beam-direct.lisp each defined their own identical default-1 fallback query (box-height, agent-height, beam-blocker-height) over the same has-height relation — three copies of one role-level idiom. They were consolidated into -height.lisp's single declared-height query, and agent.lisp — which existed solely to supply agent-height — was deleted outright, since box.lisp's own nesting of -height already provides declared-height for both a box's own height and an agent's reach bound. No problem needs to (include-tech agent) anymore.
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, jammer, or agent would, defaulting to the same unit height of 1. beam-relay.lisp now receives -height through -placement (which nests -support-elevation), so connector pickup and placement can apply the agent's vertical-reach limit. Relay lighting itself remains visibility/pairing-based and does not inspect a connector's vertical extent; only beam-direct's corridor-blocking check uses that extent.
Why jump-barrier-height stays separate. Fence/gate/screen/wall clearance heights default per kind (fence 2; gate/screen/wall 3), not to the universal default of 1 used for agents and movable objects. Only queries that genuinely share both signature and default belong in the substrate file.
Hard dependencies are (include-tech ...) nests — enforced by the splicer; a file can't be missing what it nests. 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. -accessibility/accessibility, -reachability/reachability, -visibility/visibility, and -beam-substrate/its peer technologies use this pattern.
| File | Calls (soft) | Actually defined in | Requirement |
|---|---|---|---|
gate | jamming | jammer | Conditional on nonempty jammer |
-controls | depressed | plate | Conditional on nonempty plate (read via energized, shared by gate and the blower techs' gears) |
-beam-los-coordinates | los-to-transceiver, los-to-target, los-to-location | 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-segments/gate-segments | Conditional on the problem authoring position facts and including visibility |
For a new object type, work through these in order.
jump's jump-feature or jammer's target). Don't create a substrate file for something with exactly one user.either form by hand (see the jump case study above).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 walk-via and jump-via are symmetric, while ladder-via>, jump-via>, and jam-disallowed> are directional.reachable/visible/accessible) 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-update, and slot a call into propagate-consequences! in the right causal position.include-tech yet, in which case the testing waiver applies.