Skip to content

feat(dsl,compiler): timed-gate disarm — the hazard ladder's third rung (task #184) - #275

Merged
stellarfeline merged 1 commit into
mainfrom
worker/t184-timed-gate-disarm
Aug 5, 2026
Merged

feat(dsl,compiler): timed-gate disarm — the hazard ladder's third rung (task #184)#275
stellarfeline merged 1 commit into
mainfrom
worker/t184-timed-gate-disarm

Conversation

@stellarfeline

Copy link
Copy Markdown
Owner

What changed at the design level

timed-gate gains an optional disarm{via, sets_flag} — the same shape a trap's disarm takes, 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:

function with a disarm
tgate_open_<id> fill … air replace <block> (unguarded), then execute unless score #tgdis_<id> dw.sys matches 1 run schedule … tgate_close_<id> <open>t
tgate_close_<id> every line prefixed execute unless score #tgdis_<id> dw.sys matches 1 — the crush judgement, the fill, and the next hop
tgate_disarm_<id> (new) latch #tgdis_<id> → raise sets_flag on #partyfill … air replace <block> once → kill the affordance hardware
setup_finish summon the interaction hitbox + its dw_hw_… item_display (DW0420)
tick the same one-shot poll a shortcut unlock uses

Two design points worth the review:

  • The open's own fill is 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.
  • A disarmed gate can never crush, and that is not a second rule: the judgement rides the closing tick, and the closing tick is inside the suppressed clock. Asserted structurally (every damage @s in the shipped datapack must begin with the guard prefix), so it cannot regress by someone adding a fourth line later.
  • 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. 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

proof tier what it holds
DW0377 (extended) validation disarm.via resolves to a real prefab anchor, and is not the gate anchor — a lever inside the span the portcullis closes on (and, with crush, kills in)
DW0389 (new) validation no close-gate anywhere may name a disarmable gate. The mirror of a shortcut's DW0372; the scan descends nested effect lists, so a re-seal buried in a sequence is caught. A close-gate on a timed gate with no disarm is untouched
DW0393 (new) build (nav) the lever is walkable from the campaign entry with the gate span SEALED — the same pre-commitment clause DW0373 puts on a shortcut's unlock. A jam you can only reach by first surviving the crossing disables nothing; it is a trophy dressed as counterplay
DW0420 / DW0421 build the affordance carries compiler-owned visible hardware, retired only by its own tgate_disarm_<id>
DW0388 unchanged a disarm-capable gate is still a timed hazard needing a watch bay — observability is about the pre-disarm read, which is how the party decides the jam is worth the walk. Asserted explicitly
DW0378 unchanged the 20% duty-cycle proof is untouched
DW0359 (eclipse) / flow ambient flags / scoreboard init extended the new affordance and its flag join the existing machinery

Catalog rows for DW0389 / DW0393 are in docs/reference/compiler.md (surface row, emission row and feature ledger updated in the same PR); tools/check-dw-codes.py is green in both directions with code-asserting tests for each.

Fixture pair, and a machine red→green

New fixture souls-timed-gate-disarm (the souls-timed-gate portcullis, crush: true, given a jam lever). It builds green and emits souls_timed_gate_disarm, a generated PackTest that drives the real jam and then the real tgate_close_<id> — the exact function the schedule would have re-entered — across three former cycle boundaries.

Measured on the pinned toolserver:

  • guard stripped from 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 failed
  • guard in place → All 11 required tests passed

A 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-sequence form), 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_version bump. disarm is an additive optional field on timed_gates[], and that whole section is already fenced at 0.6 (DW0141); a campaign that omits the field compiles byte-identically (asserted). This follows the crush precedent — 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 warnings clean; full workspace suite green (125 test binaries). New coverage: 6 DSL tests (DW0377 disarm shapes, DW0389 direct + nested + the untouched plain-clock case, the flag-producer registration), 4 nav unit tests (DW0393 pass/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, DW0388 parity, 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

…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
stellarfeline force-pushed the worker/t184-timed-gate-disarm branch from 77360af to 46d13ec Compare August 5, 2026 10:53
@stellarfeline
stellarfeline merged commit 2183065 into main Aug 5, 2026
10 checks passed
@stellarfeline
stellarfeline deleted the worker/t184-timed-gate-disarm branch August 5, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant