Switchyard's grammar was chosen by generating four independent ontologies — each from a different design philosophy (minimal-orthogonal, canon-first empiricist, type-theoretic, scene-graph) — and stress-testing each adversarially against the full canon. This document records what survived and why, so future changes don't re-introduce a solved problem.
All four drafts stored the means-vs-side-effect label and trusted the author to set it honestly. That's fatal: the entire philosophical content of the trolley problem is the difference between the switch (side effect) and the footbridge (means), and a grammar that lets you assert "this is a means" has encoded nothing. Every reviewer independently arrived at the same fix:
Derive causation by counterfactual — would the rescued still be rescued if the victim were absent? If removing the victim un-saves the others, their death is a means.
Switchyard implements a structural proxy for exactly that (deriveCausation in
src/model.ts):
interpose/consume→ the body/organs are the stopper → means.rerouteonto a segment whoseloopsBackTorejoins a rescued party's terminus → the loop closes the same counterfactual → means.- any other
reroute→ the victim merely happened to be on the diverted path → side effect.
And validate() enforces MEANS_BACKING: a means-death that rescues no one is rejected as
incoherent. You cannot stamp "means" without a rescue that structurally depends on it.
- Cohort / Channel split (minimal-orthogonal): identity dimensions on the noun, geometry dimensions on the edge. This is the orthogonality that lets the generator dial who-dies independently of how-they-die.
- causation ⟂ force (canon-empiricist, type-theoretic): the doctrine-of-double-effect axis and the personal-force axis are independent, so the loop (means + impersonal) and the footbridge (means + personal) and the switch (side effect + impersonal) are three distinct points, not one flat label.
loopsBackTotopology (scene-graph): encoding the loop as a graph fact, and letting that fact licensemeansunder a reroute, is what makes the switch/loop distinction structural.- Total
fatesmap (canon-empiricist): every choice reassigns every party's fate. This is what makes triage — a partition over N parties, not a single victim — expressible as one choice, and it gives every scorer a complete accounting. baselineVictimIdsanchor (type-theoretic): doing-vs-allowing is computed by diffing a choice against a fixed omission baseline, not tagged ad hoc.- Branded
ValidDilemma(type-theoretic): "validity is a constructor, not a checker." domainas a free skin (canon-empiricist): footbridge and transplant share a logical shape; the domain string is decoration, so non-rail dilemmas are first-class, not hacks.
- The counterfactual is a proxy, not a simulation.
deriveCausationreads mechanism +loopsBackTorather than running a full causal model. It is correct on the canon and blocks the obvious fakes, but a determined author with exotic topology could still mislabel. v0.2 should carry a minimalsavedDependsOncausal edge and derive purely from it. - The renderer is tuned for rail-shaped dilemmas.
triageandalignmentrender as a best-effort schematic; they want their own renderers (a lifeboat grid, a dependency graph). - Probabilistic fates are per-party independent. The
tunnelmodels death probabilities per party, which doesn't capture correlated outcomes (the swerve either works or doesn't). Fine for expected-value scoring; revisit if a scorer needs joint distributions. - Scorer weights are illustrative. The numeric penalties (means = −100, etc.) are tuned to produce defensible recommendations on the canon, not claimed as a calibrated moral theory.