3.5 KiB
Guardrails via intrinsic properties, not special cases
The goal
Full recombination across the existing catalog — any platform × any actuator × any energy source — should stay possible, while physically impossible pairings (a nuclear reactor on a bicycle, a solar sail in an atmosphere) get blocked automatically. Neither half is negotiable: too loose and the results are nonsense, too special-cased and the combinatorics stop being the point of the project.
The mechanism (already in place)
Entities never reference each other. Each one only declares its own physical envelope:
requires/provides/excludes— categorical compatibility (energy_form,medium,atmosphere)range_min/range_max— numeric floors and ceilings on a shared key (mass,footprint,energy_density, ...)
ConstraintResolver (src/physcom/engine/constraint_resolver.py) then runs generic, entity-agnostic rules across every pair in a combo: requires-vs-excludes, mutual exclusion, range incompatibility, provides-vs-range deficit, unmet requirements. A combo gets blocked because two numbers or two category tags collided — never because code somewhere says "if X and Y, block." Blocking is emergent from the data, not authored per pair.
This is what makes "nuclear reactor won't fit on a bicycle" work today: Nuclear Thermal Drive declares mass range_min = 1500kg, Light Personal Vehicle declares mass range_max = 60kg, and Rule 3 (_check_range_incompatibility) blocks the combo without either entity knowing the other exists.
Why this breaks silently
The mechanism only blocks what the data describes. A category missing a mass range_min or footprint range_min it should physically have doesn't get blocked — not because the rule is wrong, but because nothing told the resolver there was a limit. This is an easy failure mode: the constraint system looks like it's working (it blocks the pairs someone thought to test) while quietly admitting nonsense pairs nobody happened to check.
Two such gaps were found and fixed 2026-07 (see git history around this doc's commit):
- Solar Sail declared its size under the key
surface_areainstead offootprint— a naming mismatch made it invisible to every cross-check, silently a no-op. - Nuclear Thermal Drive and Nuclear Fuel had a
mass range_minbut nofootprint range_min— reactor mass was guarded, reactor size was not.
Both were fixed by adding/renaming a Dependency, zero changes to ConstraintResolver itself. That's the intended shape of a fix here.
The standing question for review
For every actuator and energy_storage entity: what is the full set of intrinsic physical floors and ceilings a real version of this technology imposes at any scale — structural mass, footprint, radiation shielding, minimum coolant/containment volume, thermal rejection surface area, etc. — and is each one expressed as a range_min/range_max/provides/requires dependency using a key that already exists in the vocabulary (grep -oP 'Dependency\("[a-z]+", "\K[a-z_]+' over seed/transport_example.py lists it), so it actually cross-checks against every other entity that shares that key?
A missing floor is a silent guardrail hole, not a missing detail.
Non-goal
Do not add pairwise special cases (if actuator == "Nuclear Thermal Drive" and platform == "..."). That defeats the architecture — the whole point is that the catalog recombines freely and physics falls out of shared, generic constraints. Any new realism belongs on the entity as a property, not in ConstraintResolver as a rule.