feat(dsl,compiler): timed-gate disarm — the hazard ladder's third rung (task #184) - #275
Merged
Merged
Conversation
…ung (task #184) `timed-gate` gains an optional `disarm{via, sets_flag}`, the exact shape a trap's disarm takes. Interacting with the affordance suppresses the gate's clock permanently with the gate RESTING OPEN — a jammed portcullis stays up — which is the souls dossier's §5.2 third rung: readable, avoidable, and finally disable-able (Smouldering Lake's ballista, the Fringefolk chariot). Emission: the existing schedule ping-pong gains one score guard. Every line of `tgate_close_<id>` — the crush judgement, the fill and the next hop — plus the open half's `schedule` is prefixed `execute unless score guarded: a jam landing while the gate is shut leaves one already-scheduled open in flight, and that open is what parks the portcullis in its resting position. A disarmed gate therefore can never crush — the judgement is inside the suppressed clock, not beside it. Zero per-tick cost beyond the affordance poll every interact affordance already carries. Permanence is structural, exactly as a shortcut's is: nothing un-latches the sentinel, and no `close-gate` may name a disarmable gate (`DW0389`, the mirror of `DW0372`). There is no `schedule clear` — a close already in flight fires into the guard and does nothing, including not scheduling the next open, so the ping-pong dies of its own accord within one hop. Proofs: `DW0377` covers the structural half (an unresolvable `disarm.via`, or one that IS the gate anchor — a lever inside the span that crushes); `DW0389` refuses any re-arm; `DW0393` proves the lever is walkable from the campaign entry while the gate is SHUT, the same pre-commitment clause `DW0373` puts on a shortcut's unlock; `DW0420` obliges the compiler-owned visible hardware. `DW0378`'s 20% duty cycle and `DW0388`'s observability proof are untouched and apply identically — observability is about the pre-disarm read. Fixture `souls-timed-gate-disarm` builds green and its generated PackTest proves post-jam permanence across three former cycle boundaries on a live server, asserted immediately after each close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnrGdWS7ADsjCoJfiJc6GC
stellarfeline
force-pushed
the
worker/t184-timed-gate-disarm
branch
from
August 5, 2026 10:53
77360af to
46d13ec
Compare
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 changed at the design level
timed-gategains an optionaldisarm{via, sets_flag}— the same shape a trap'sdisarmtakes, deliberately, so the delve has one affordance grammar for every mechanism the party can switch off. It is the souls dossier's §5.2 third rung: a hazard should be readable, avoidable, and finally disable-able, the way Smouldering Lake's ballista and the Fringefolk chariot can be removed for good. Before this the engine could express the first two rungs and not the third — a clock the party could only ever dance with.Semantics. Interacting with the affordance suppresses the clock permanently, with the gate RESTING OPEN — a jammed portcullis stays up. Permanence is structural exactly as a shortcut's is: nothing re-arms it, and there is no way to spell a re-arm.
Consumer: the bell remake's boulder-jam set piece (content
campaigns/the-drowned-bell/REMAKE.md§4 entry D — the boulder release jammed from the stair head).Emission evidence
The existing schedule ping-pong gains one score guard and nothing else:
disarmtgate_open_<id>fill … air replace <block>(unguarded), thenexecute unless score #tgdis_<id> dw.sys matches 1 run schedule … tgate_close_<id> <open>ttgate_close_<id>execute unless score #tgdis_<id> dw.sys matches 1— the crush judgement, thefill, and the next hoptgate_disarm_<id>(new)#tgdis_<id>→ raisesets_flagon#party→fill … air replace <block>once →killthe affordance hardwaresetup_finishdw_hw_…item_display (DW0420)tickTwo design points worth the review:
fillis deliberately NOT guarded. A jam landing while the gate is shut leaves one already-scheduled open in flight, and that open is what parks the portcullis in its resting position. Guarding it would freeze the gate CLOSED — the opposite of a disarm.damage @sin the shipped datapack must begin with the guard prefix), so it cannot regress by someone adding a fourth line later.schedule clear. A close already in flight fires into the guard and does nothing — including not scheduling the next open — so the ping-pong dies of its own accord within one hop. Clearing a schedule that may not exist would be a command that fails at runtime for no gain.Zero per-tick cost beyond the affordance poll every interact affordance already carries; the clock itself still costs nothing per tick.
Proof matrix
DW0377(extended)disarm.viaresolves to a real prefab anchor, and is not the gate anchor — a lever inside the span the portcullis closes on (and, withcrush, kills in)DW0389(new)close-gateanywhere may name a disarmable gate. The mirror of a shortcut'sDW0372; the scan descends nested effect lists, so a re-seal buried in asequenceis caught. Aclose-gateon a timed gate with no disarm is untouchedDW0393(new)DW0373puts on a shortcut's unlock. A jam you can only reach by first surviving the crossing disables nothing; it is a trophy dressed as counterplayDW0420/DW0421tgate_disarm_<id>DW0388DW0378DW0359(eclipse) /flowambient flags / scoreboard initCatalog rows for
DW0389/DW0393are indocs/reference/compiler.md(surface row, emission row and feature ledger updated in the same PR);tools/check-dw-codes.pyis green in both directions with code-asserting tests for each.Fixture pair, and a machine red→green
New fixture
souls-timed-gate-disarm(thesouls-timed-gateportcullis,crush: true, given a jam lever). It builds green and emitssouls_timed_gate_disarm, a generated PackTest that drives the real jam and then the realtgate_close_<id>— the exact function the schedule would have re-entered — across three former cycle boundaries.Measured on the pinned toolserver:
tgate_close_<id>→souls-timed-gate-disarm:souls_timed_gate_disarm failed … Expected #tgd_c3 dw.sys to match 1, but got 0, suite 1 required test failedA note on how that proof was earned, because it is the debugging lesson of this PR: the first version of the template asserted after the following open, and it passed against the deliberately unguarded build — the open clears the span either way, so the assertion proved nothing. The template now asserts immediately after each close, with a compiler test that reads the emitted template and fails if any
#tgd_c<n>assertion has an open between it and its close. The pitfall is automated out of existence rather than left in a comment.The negative half of the fixture pair is at the DSL tier (
close_gate_on_a_disarmable_gate_is_dw0389, plus the nested-in-a-sequenceform), since a re-arm never reaches emission.CI gains one PackTest pass (
dw-ci-tgdisarm), matching the existing per-fixture passes for the bonfire and TD-lane templates — a generated template only exists for a campaign that emits it, so without a pass it would be shape-verified in Rust and never actually RUN.Version fence — a decision for the planner
No
dsl_versionbump.disarmis an additive optional field ontimed_gates[], and that whole section is already fenced at 0.6 (DW0141); a campaign that omits the field compiles byte-identically (asserted). This follows thecrushprecedent — the immediately preceding additive field on the same struct, also unfenced for the same reason.So this PR does not touch
envelope.rs's version ordinal and does not participate in the pending three-way 0.9.0 fence collision between #272, #260 and any other in-flight version work. If the planner would rather every new field carry its own fence, say so and I will add a 0.9.0 one in whichever PR lands the ordinal first — but doing it here would have created a fourth colliding edit to the same lines for no behavioural gain.What CI now proves
cargo fmt/clippy -D warningsclean; full workspace suite green (125 test binaries). New coverage: 6 DSL tests (DW0377disarm shapes,DW0389direct + nested + the untouched plain-clock case, the flag-producer registration), 4 nav unit tests (DW0393pass/fail/long-way-round/vacuous), 7 compiler fixture tests (guard placement, the crush-inside-the-guard invariant, the jam function's exact body, affordance visibility,DW0388parity, the PackTest's own shape, and byte-level non-interference with a gate that declares no disarm), plus the live PackTest pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01RnrGdWS7ADsjCoJfiJc6GC