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

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

Overview

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

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

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

Vocabulary

KindOwnsExamplesRule of thumb
role One composite either-type + its one relation (occasionally one helper query) -location.lispmobile-object/has-location;
-support-occupancy.lispsupport + support-occupant/on/cleartop
Nest it, never hand-copy it. Exactly one owner per role, full stop.
service Capability-specific relations and derived queries built on top of roles, but no object-role composite of its own accessibilityaccessible; reachabilityreachable; visibilityvisible 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.

The tier picture

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

Object & behavior technologies
box, plate, gate, jammer, ladder, barrier, beam-direct/relay/crossing — own a leaf type (or composite), plus their own queries and actions
↑↓
Capability services
accessibility, reachability, visibility — derive facts from roles; read gate's open
Substrate (nested-only roles)
-location, -holding, -position, -height, -elevation, -support-occupancy, -support-elevation, -passability, -reachability, -visibility, -beam-substrate, -beam-coordinates

File inventory

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

FileOwnsNestsActionsKind
-location.lispmobile-object, has-locationrole
-holding.lispcargo, holdingrole
-position.lispfixed-position-object, has-positionrole
-height.lispheighted-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.lispsupport, support-occupant, on, query cleartoprole
-support-elevation.lispQueries support-top-elevation/occupant-elevation-support-occupancy, -location, -position, -height, -elevationrole
-passability.lispOptional gate/screen/ladder, open, queries obstacle-clear/all-clear-holdingrole
-reachability.lispIdentity-default reachable queryrole (hook/interface variant)
-visibility.lispfixture, optional fixture leaf types, null-default visible queryrole (hook/interface variant)
-beam-substrate.lisphue/transmitter/receiver (optional), active, has-chroma, null-object default hooks, update-receiver-status!role (hook/interface variant)
-beam-coordinates.lispOptional 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.lispelevated-object (location/gate/screen/fence/transmitter/receiver), has-elevation, queries object-elevation/location-elevation/fixture-elevationrole
elevation.lispPublic problem-facing wrapper for -elevation-elevationrole wrapper
accessibility.lispwalk-via, accessible, one-step-accessible-support-occupancy, -location, -passability, -elevationmoveservice (the one with an action)
reachability.lispgate (optional), reachable-via, extended reachable, reachable-clear-reachabilityservice
visibility.lisplos-to-fixture, los-to-location, extended visible, visible-clear-visibilityservice
gate.lispgate/plate/jammer/mode/receiver (optional), open, controls, query energized, update-gate-status!-beam-substratehub
plate.lispplate (optional), depressed, update-plate-status!-support-occupancyassembly (no actions)
box.lispplate/box (optional), jump-via-support-elevation, -reachability, -passabilitypickup-box, put-box, jump-toassembly (fullest example)
jammer.lisptarget composite, plate/jammer/box (optional), jamming, jam-disallowed>-holding, -support-elevation, -reachability, -visibilitypickup-jammer, jam-targetassembly
ladder.lispladder (optional), traversable>, query one-way-clear-support-occupancy, -location, -position, -passabilityuse-ladderassembly
barrier.lispvaultable-barrier composite, box/fence (optional), traversable/traversable>, queries barrier-height/vaultable-barrier-list/vault-clearance-height-support-elevationvault-overassembly (composite-behavior variant — see below)
beam-direct.lispbeam-blocker composite, gate/transmitter/receiver (optional), coupled, beam-via, elevation-aware query beam-clear + hook overrides-beam-substrate, -location, -support-occupancy, -height, -elevationpeer-plugin
beam-relay.lispterminus composite, several optional types, paired, color, update-connector-status! + hook overrides-beam-substrate, -holding, -location, -support-occupancy, -position, -visibility, -reachabilitypickup-connector, connect-connectorpeer-plugin (the one with actions)
beam-crossing.lispgate/transmitter (optional), crossing-active, beam-crossing>, update-crossing-status! + hook overrides-beam-substrate, -beam-coordinatespeer-plugin

Worked example: box

box.lisp doesn't own mobile-object, cargo, support/support-occupant, heighted-object, fixed floor elevation, or support elevation — it nests the role files that do, and adds only 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.

Special cases

gate — the hub

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.

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

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

beam-direct 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 — identity default with an optional extension

-reachability.lisp supplies the baseline reachable query used by manipulation actions: two locations are reachable when they are identical. box, jammer, and beam-relay nest this substrate, so they remain self-contained without authored reach edges. Including the public reachability technology overrides the same query with identity plus 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 — null default with an optional extension

-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 — shared traversal clearance

-passability.lisp owns the action-free rule for clearing a traversal edge's obstacle list: open gates pass, while screens and ladders require an empty-handed agent. 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.

barrier — composite-behavior assembly

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.

declared-height — a consolidated role query

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.

Two axes of dependency: hard vs. soft

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.

FileCalls (soft)Actually defined inRequirement
gatedepressed, jammingplate, jammerConditional on nonempty plate and jammer, respectively
beam-direct / beam-crossingopengate owns its updates; the relation declaration is sharedConditional on nonempty gate
-beam-coordinateslos-to-fixture, los-to-locationvisibilityConditional on the problem authoring BEAM-POSITION> facts and including visibility

Integration checklist

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

  1. Classify it first. Does it just need to participate in existing roles (have a location, be held, rest on/be rested on, have a fixed position, have a height), or does it need a genuinely new relation nobody has? Most new objects are almost entirely the former.
  2. New role, one consumer → declare its type + relation inline in the new object's own assembly file (like barrier's vaultable-barrier or jammer's target). Don't create a substrate file for something with exactly one user.
  3. New role, 2+ plausible consumers → give it its own dash-prefixed, nest-only file with only the shared types, relations, and queries needed for that role. No actions or driver logic.
  4. Existing role → nest the owning substrate file. Never retype the either form by hand (see the barrier case study above).
  5. Declare the leaf type optional (define-optional-types) in the new assembly and in every other file that references it as a bare pre-param type. This is idempotent and order-independent, so repeating it is always safe.
  6. Design each relation signature deliberately. A $-prefixed declared type marks a functional/fluent value position. When the same normalized type appears in multiple positions, Wouldwork treats those positions as symmetric and installs their permutations; append > to the relation name when those repeated positions instead have ordered roles. Thus walk-via and los-to-location are symmetric, while traversable> and jam-disallowed> are directional.
  7. Identify soft dependencies — which services (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.
  8. New derived boolean fact? Give it a define-update, and slot a call into propagate-consequences! in the right causal position.
  9. Write the actions. This is the one genuinely bespoke step.
  10. Test via REPL, unless the new tech isn't referenced by any problem's include-tech yet, in which case the testing waiver applies.