docs(audit): a capability belongs to the object class it acts on, not the verb that first needed it - #325
Open
stellarfeline wants to merge 1 commit into
Open
docs(audit): a capability belongs to the object class it acts on, not the verb that first needed it#325stellarfeline wants to merge 1 commit into
stellarfeline wants to merge 1 commit into
Conversation
… the verb that first needed it
Audit only — no behaviour changes, no player-facing byte moves.
`close-gate.sealed_hint` turned out not to be a capability keyed to the
wrong parent. It is a private re-implementation, inside one verb, of
`EnvTrigger{on:use} + effects` — which the DSL already exposes in full.
The root cause is one rung lower: `EnvTrigger` binds an interaction body
to a POINT at a cell, and a seal, a door and a boulder are VOLUMES, so
every volume object grew its own fleet.
Ledger of 24 rows in docs/notes/capability-ownership-audit.md, each
answering the owner's test — does this primitive encode a MECHANISM, or
a DESIGN DECISION about what the mechanism is for — and each carrying
its severity and version-adoption cost.
Adds tools/check-capability-ownership.py as a step of the existing
`docs (local link check)` job (a step, not a job: every job name is a
required status context, and a new one would deadlock branch
protection). Five ledgers, each stating a binding count and failing on
a zero binding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
An audit, not a fix. No behaviour changes; no player-facing byte moves. The
sealed_hint/shortcut-door lift belongs to another worker and this PR does nottouch that surface.
What the audit found
The owner's instance was
close-gate.sealed_hint. Checking it against the codeinverted the diagnosis twice:
re-implementation, inside one verb, of
EnvTrigger{on:use} + effects—which the DSL already exposes in full, with the whole effect vocabulary, flag
gating and
once.EnvTriggerbinds an interactionbody to a POINT at a cell, and a seal, a door and a boulder are VOLUMES.
close-gateis the only site in the compiler that binds to an object's shape(one body per shell cell). Rather than widening the general mechanism, the
general mechanism was taught to ride the private one
(
trigger_rides_seal,npc_hitbox_trigger_tags).So the fix is neither a second bespoke field nor a new stage-5 section — the
latter would be a third mechanism, strictly weaker than
EnvTrigger.Four row shapes, 24 rows, each with file:line evidence, severity, and the owner's
test answered explicitly (does this primitive encode a MECHANISM, or a DESIGN
DECISION about what the mechanism is for?). Full ledger:
docs/notes/capability-ownership-audit.md.The sharpest finding, and it is a live latent bug
shortcuts[].on_unlockis a sixth effect root that no enumeration knowsabout. It is a
Vec<QuestEffect>hanging off a stage-5 struct — structurallyidentical in kind to
traps[].payload, which is root R4 — and emission reallylowers it (
emit.rs:5043). It is not anEffectRootKindvariant and not innested_effect_lists. Every walk that inherits the five roots skips it: anarrateinside it is never l10n-inventoried; aset-flaginside it is invisibleto the flag model and to
emit::declared_flags.This is exactly the defect PRs #301/#302/#321 closed thirteen times, still live in
the one root the enumeration does not contain.
check-effect-roots.pycannot seeit: that gate greps for the five roots it knows, so a sixth is invisible to it
by construction. Zero live campaign usage is the only reason it has not shipped.
Two more of the same family, both verified directly:
DW0473(unavoidable lethal damage) walks 2 of 5 roots, so adamage-playersinside atraps[].payloadis invisible to it — and spec-0022made trap payloads the intended home for exactly that. Twelve further
hand-rolled walks still miss roots.
emit::affordancesclaims totality in its own comment ("the list is thedefinition of the class … which is what makes the proof total rather than a spot
check") and is a hand-enumerated list of four kinds, missing five.
What CI now proves
tools/check-capability-ownership.py, added as a step of the existingdocs (local link check)job — a step, not a job, because every job name is arequired status context and a new one would deadlock branch protection until it
was added there first.
Five ledgers, each an allowlist carrying a reason per entry. Each prints a binding
count and fails on a zero binding, because a gate that matched nothing is
vacuous, not a pass.
Green here is not a clean bill: most entries are OPEN FINDINGS with a named
lift. The gate's job is that none can be added or removed in silence.
Demonstrated firing on the live instances
Removing
seal_fnsandSEAL_HINT_DEFAULTfrom the ledger — i.e. simulatingsealed_hintbeing introduced today:Removing
on_unlockfrom check E's ledger:Check E is the answer to "why did the existing gate not catch this": it asks the
inverse question — every bundle in the DSL must be claimed by some enumeration —
which does not require knowing the roots in advance.
Recommended sequence (honest scope)
This is materially more than one round. The proposed cut is steps 0–3 as one
milestone — they block content today and none forces campaign adoption —
holding the rest, because step 5's shape depends entirely on step 1's outcome.
on_unlockinside an enumeration (theAmbush::to_triggerdesugarpattern, not a sixth root). First and alone.
EnvTrigger.atfrom a point to an anchor's shape, plus anarrateactionbarstyle — one lift; the prerequisite for thesealed_hintwork.requires_flags/forbids_flagsto everyQuestEffect(16 of 26 today;no staging or souls verb can be branch-gated).
DW0473first and alone.Adoption cost is far lower than expected:
on_unlock,disarm,via,sets_flag,payload,sealed_hint,crush,bonfire,shortcuts,timed_gates,trapsand eight more have zero live usages across alltracked campaign content.
Two things recorded but not judged
crates/grammar/src/library/bell/(1131 lines) holds one campaign's zoneprograms inside the engine crate — by the owner's principle the largest open
instance. Plausibly deliberate as the grammar back end's first production
workload. Owner's call; no move proposed.
nobodys-caveandthe-drowned-bellexist only as untracked build outputswith no source in git, so any DSL change orphans them permanently.
the-drowned-bellis the campaign whose playtest producedDW0420/DW0421.Process root cause
ADR-0015 already sets promotion gates (second-campaign, machine-proof). Four of
the fields audited were introduced by a task, not a spec, and recorded no gate:
sealed_hint(#142),missing_item_hint(v0.7, no spec, and absent from theDW0141list that claims to be exhaustive), timed-gatedisarm(#184), andon_unlock(one parenthetical in spec-0016 §2). The gate exists; the task routebypasses it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL