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 |
-holding.lisp | cargo, holding | — | — | role |
-position.lisp | fixed-position-object, has-position | — | — | role |
-height.lisp | heighted-object, has-height (box/fence/gate/agent/screen/jammer/connector), query declared-height (shared default-1 fallback for box/agent/jammer/connector; barrier vaulting keeps its own kind-specific barrier-height) | — | — | role |
-support-occupancy.lisp | support, support-occupant, on, query cleartop | — | — | role |
-support-elevation.lisp | Queries support-top-elevation/occupant-elevation | -support-occupancy, -location, -position, -height, -elevation | — | role |
-passability.lisp | Optional gate/screen/ladder, open, queries obstacle-clear/all-clear | -holding | — | role |
-reachability.lisp | Identity-default reachable query | — | — | role (hook/interface variant) |
-visibility.lisp | fixture, optional fixture leaf types, null-default visible query | — | — | 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-coordinates.lisp | Optional crossing type, beam-position>, queries beam-coordinates-potential-beams/beam-coordinates-endpoint-positions (derives crossings-along-beam> from beam-position> + LOS facts) | — | establish-beam-coordinates (init) | role (nested only under beam-crossing) |
-elevation.lisp | elevated-object (location/gate/screen/fence/transmitter/receiver), has-elevation, queries object-elevation/location-elevation/fixture-elevation | — | — | role |
elevation.lisp | Public problem-facing wrapper for -elevation | -elevation | — | role wrapper |
accessibility.lisp | walk-via, accessible, one-step-accessible | -support-occupancy, -location, -passability, -elevation | move | service (the one with an action) |
reachability.lisp | gate (optional), reachable-via, extended reachable, reachable-clear | -reachability | — | service |
visibility.lisp | los-to-fixture, los-to-location, extended visible, visible-clear | -visibility | — | service |
gate.lisp | gate/plate/jammer/mode/receiver (optional), open, controls, query energized, update-gate-status! | -beam-substrate | — | hub |
plate.lisp | plate (optional), depressed, update-plate-status! | -support-occupancy | — | assembly (no actions) |
box.lisp | plate/box (optional), jump-via | -support-elevation, -reachability, -passability | pickup-box, put-box, jump-to | assembly (fullest example) |
jammer.lisp | target composite, plate/jammer/box (optional), jamming, jam-disallowed> | -holding, -support-elevation, -reachability, -visibility | pickup-jammer, jam-target | assembly |
ladder.lisp | ladder (optional), traversable>, query one-way-clear | -support-occupancy, -location, -position, -passability | use-ladder | assembly |
barrier.lisp | vaultable-barrier composite, box/fence (optional), traversable/traversable>, queries barrier-height/vaultable-barrier-list/vault-clearance-height | -support-elevation | vault-over | assembly (composite-behavior variant — see below) |
beam-direct.lisp | beam-blocker composite, gate/transmitter/receiver (optional), coupled, beam-via, elevation-aware query beam-clear + hook overrides | -beam-substrate, -location, -support-occupancy, -height, -elevation | — | peer-plugin |
beam-relay.lisp | terminus composite, several optional types, paired, color, update-connector-status! + hook overrides | -beam-substrate, -holding, -location, -support-occupancy, -position, -visibility, -reachability | pickup-connector, connect-connector | peer-plugin (the one with actions) |
beam-crossing.lisp | gate/transmitter (optional), crossing-active, beam-crossing>, update-crossing-status! + hook overrides | -beam-substrate, -beam-coordinates | — | 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 what's genuinely its own: jump-via and its three actions. 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. But its (open gate) fact is read by accessibility, reachability, visibility, and every beam file. Any change to how open is computed or what it depends on has the widest blast radius in the directory.
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 authored fixture elevations can set the horizontal beam level. 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.
-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 reachable-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.
-visibility.lisp supplies the visible interface with a null default, allowing beam-relay and jammer to compile independently while granting no sight-dependent behavior. Including the public visibility technology adds authored LOS relations and overrides visible with the operational implementation.
-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, box, and ladder nest it for walking, jumping, and one-way traversal respectively. Public accessibility is therefore needed only for walking behavior, not to compile or operate box jumps and ladder actions.
No problem instantiates a leaf type called barrier. What barrier.lisp owns is vaultable-barrier, a composite spanning gate and screen (owned elsewhere) plus fence, the one genuinely new leaf type the file introduces. So this file is centered on a behavior (vault-over) applicable to a composite of objects, rather than on one owned object — distinct from box/jammer/ladder, which are each centered on one problem-facing leaf type. jammer.lisp's target (either gate) is a smaller-scale version of the same thing, but jammer still has its own real leaf type anchoring it, so it stays object-centered.
Case study. barrier.lisp needs the agent's standing elevation, but that elevation may come from either a box or the location floor. Keeping occupant-elevation in box.lisp made barrier vaulting depend on the public box technology and encouraged an unnecessary departure-box precondition. Moving the query to -support-elevation.lisp makes the role explicit: barrier can vault directly from a raised location 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 itself still doesn't nest -height — its own relaying logic is purely visibility/pairing-based and never checks a connector's vertical extent; only beam-direct's corridor-blocking check does.
Why barrier-height stayed separate. Fence/gate/screen vaulting height defaults per kind (fence 2, gate/screen 3), not to a single constant, so it isn't a candidate for the same shared query — 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. -reachability/reachability, -visibility/visibility, and -beam-substrate/its peer technologies use this pattern.
| File | Calls (soft) | Actually defined in | Requirement |
|---|---|---|---|
gate | depressed, jamming | plate, jammer | Conditional on nonempty plate and jammer, respectively |
beam-direct / beam-crossing | open | gate owns its updates; the relation declaration is shared | Conditional on nonempty gate |
-beam-coordinates | los-to-fixture, los-to-location | visibility | Conditional on the problem authoring BEAM-POSITION> facts and including visibility |
For a new object type, work through these in order.
barrier's vaultable-barrier or jammer's target). Don't create a substrate file for something with exactly one user.either form by hand (see the barrier 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 los-to-location are symmetric, while traversable> 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.