Skip to content

D and T: the dataset and threat-picture cycles (#45) - #68

Merged
bgent19 merged 2 commits into
mainfrom
feat/45-dataset-threat-cycles
Aug 17, 2026
Merged

D and T: the dataset and threat-picture cycles (#45)#68
bgent19 merged 2 commits into
mainfrom
feat/45-dataset-threat-cycles

Conversation

@bgent19

@bgent19 bgent19 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Closes #45.

Two keys that move an axis without leaving the beat the instructor is on. Park on a Pareto detent, press T, and watch the same plan buckle under three different disruptions; or hold the same step and press D to change network. Day 2's textbook-then-theater move becomes a keypress rather than a server restart.

The beat index carries, and neither cycle assigns it

Both ladders are data-derived and their lengths genuinely move — the theater's interdiction ladder is three rungs pristine and two under strait_mined. So the index carries across and is clamped by the clamp setSpine already had. Nothing new was added to hold it in range, and neither cycleDataset nor applyThreat writes it.

The threat picture is per-dataset state

It is stashed on the way out of a dataset and restored on the way back in, checked against the target's own declarations — so the excursion to the textbook set comes home to the picture it left, rather than carrying a name the new dataset never heard of.

The dataset cycle opens on the default and moves away from it, so pressing D twice at two datasets brings you home.

T with nothing to cycle is labelled

Two of the three shipped datasets declare no threat pictures, so a silent decline is the common case — and a key that does nothing in silence is indistinguishable from one that is broken. The refusal says so on screen. The label is transient state, cleared by resolveBeat along with everything else derived.

Only D awaits, only D rebuilds

Every picture's payloads are prefetched with the dataset, because T is a keypress and anything a keypress can reach is fetched before the first keypress. T is therefore synchronous and installs spines from payloads already in hand; D is the one key that awaits, and the one structural rebuild the architecture allows.

One accepted deviation, recorded rather than overlooked: T installs new spine arrays, so repaint rebuilds the beat timeline's rows. A picture genuinely changes how long the story is, so rows have to appear or go — it is the same fact that makes setSpine's clamp necessary. What the criterion protects is the topology pass, and the test pins that directly: buildGraph has exactly one caller in the page. Grepping applyThreat's body alone could never have seen a rebuild reached some other way.

Supply figures, resolved in the panel

Removing a node zeroes its lanes and leaves its stock alone, so bravo_struck severs HUB-BRAVO and the theater still reports its full 180 of supply. A panel reading an unchanged total beside a visibly severed hub invites exactly the wrong question at the moment the instructor is making the opposite point.

The total stays the server's. A second line under it says how many hubs are severed, and the hub itself is greyed with its quantity struck through — the same absent-not-alarming vocabulary a removed lane already wears. Severance is read out of the changes block through isRemoved, so it catches a hub isolated one lane at a time as well as a removed node.

It is deliberately not a fourth NODE_STATE: those are Day 1's per-beat plan failures, and filed with them a severed hub would flicker back to healthy on the next keypress. A severed transit node is greyed and stays uncounted — nothing can move through it either, but it has no stock to strand, so counting it would inflate a figure about supply.

The note gets drawn

The instructor-written note on each disruption is displayed for the first time — it has been authored into every scenario file and drawn by nothing. It gets a slot of its own beside the headline's sentence rather than sharing it, because both are true at once on the textbook set in Cost & Risk: a flat frontier to declare and no picture to cycle.

No disruption arithmetic, and the tests enforce it

Every disrupted number is read off the server's changes block. The tests now fail if the frontend so much as names a disruption kind or reads a factor.

Review pass (second commit)

  • severedNodes was building a nodeId -> [edgeId] adjacency map inside every render pass. Which lanes touch a node is topology, so it joins the crossings, the anchors and the directed lookup in frozen — solved once per dataset, handed in rather than imported.
  • cycleDataset and the menu's change handler each spelled out stash-then-load. They are one switchDataset now, guarded against re-entry — which matters for exactly this key: D is designed to be pressed twice in quick succession, and two interleaved loads would build one dataset's DOM against another's payloads.
  • resetView cannot finish R by itself (clearing the picture cannot re-solve the theater), so the driver reaches it through one named resetToBaseline.
  • The severed class is a named export beside NODE_STATES, so a fourth state stays one edit plus a stylesheet rule.

Verification

  • 390 tests pass; 12 net-new source-inspection tests in tests/test_web_assets.py (the repo verifies the frontend from Python or not at all).
  • Exercised against real payloads on all three datasets: the theater's ladder shortening from three rungs to two under strait_mined, bravo_struck severing HUB-BRAVO with the 180 total intact, the labelled T refusal on textbook and greedy_trap, and a D round trip restoring the picture it left.
  • Both /code-review axes run and addressed in 003f400.

🤖 Generated with Claude Code

Two keys that move an axis WITHOUT leaving the beat the instructor is on.
Park on a Pareto detent, press `T`, and watch the same plan buckle under
three different disruptions; or hold the same step and press `D` to change
network. Day 2's textbook-then-theater move becomes a keypress rather than
a server restart.

Both ladders are data-derived and their lengths genuinely move -- the
theater's interdiction ladder is three rungs pristine and two under
`strait_mined` -- so the beat index carries across and is clamped by the
clamp `setSpine` already had. Neither cycle assigns it.

The threat picture is per-dataset state. It is stashed on the way out of a
dataset and restored on the way back in, checked against the target's own
declarations, so the excursion to the textbook set comes home to the
picture it left rather than carrying a name the new dataset never heard of.

`T` on a dataset with no threat pictures is a LABELLED no-op -- two of the
three shipped datasets declare none, so a silent decline is the common
case, and a key that does nothing in silence is indistinguishable from one
that is broken. The label is transient state, cleared by `resolveBeat`
with everything else derived.

Every picture's payloads are prefetched with the dataset, because `T` is a
keypress and anything a keypress can reach is fetched before the first
keypress. `T` is therefore synchronous and installs spines from payloads
already in hand; `D` is the one key that awaits, and the one structural
rebuild the architecture allows. `applyThreat` touches no DOM structure.

The instructor-written `note` on each disruption is displayed for the
first time -- it has been authored into every scenario file and drawn by
nothing. It gets a slot of its own beside the headline's sentence rather
than sharing it, because both are true at once on the textbook set in
Cost & Risk: a flat frontier to declare AND no picture to cycle.

Supply figures are resolved in the panel rather than by mutating the
numbers. Removing a node zeroes its lanes and leaves its stock alone, so
`bravo_struck` severs HUB-BRAVO and the theater still reports its full 180
of supply. The total stays the server's; a second line under it says how
many hubs are severed, and the hub itself is greyed with its quantity
struck through -- the same absent-not-alarming vocabulary a removed lane
already wears. Severance is read out of the `changes` block through
`isRemoved`, so it catches a hub isolated one lane at a time as well as a
removed node, and it is deliberately NOT a fourth NODE_STATE: those are
Day 1's per-beat plan failures, and filed with them a severed hub would
flicker back to healthy on the next keypress.

No disruption arithmetic anywhere, as ever: every disrupted number is read
off the server's `changes` block, and the tests now fail if the frontend
so much as names a disruption kind or reads a `factor`.
#45)

Four findings from the standards pass and two from the spec pass.

`severedNodes` was building a `nodeId -> [edgeId]` adjacency map inside
every render pass. Which lanes touch a node is TOPOLOGY -- no picture,
beat or view moves it -- so it joins the crossings, the anchors and the
directed lookup in `frozen`, solved once per dataset by the module that
owns structure. It is handed in rather than imported, the same import
direction that keeps `laneOf` a parameter of every spine builder.

`cycleDataset` and the menu's change handler each spelled out
stash-then-load. They are the same act, so they are now one
`switchDataset` -- and it is guarded against re-entry, which matters for
exactly this key: `D` is designed to be pressed twice in quick
succession, and two interleaved loads would build one dataset's DOM
against another's payloads.

`resetView` cannot finish `R` by itself -- clearing the picture cannot
re-solve the theater -- so the driver reaches it through one named
`resetToBaseline` rather than two calls inline, and the comment says
plainly that `resetView` alone is never the whole act.

The severed class is a named export beside `NODE_STATES` rather than a
literal in the render pass, so a fourth state stays one edit plus a
stylesheet rule.

On the spec side, the one criterion the review called partial: `T`
installs new spine arrays, so `repaint` rebuilds the beat timeline's
rows. That is accepted rather than overlooked and is now recorded as
such -- a picture genuinely changes how long the story is, so rows have
to appear or go, and it is the same fact that makes `setSpine`'s clamp
necessary. What the criterion protects is the topology pass, and the
test now pins that directly: `buildGraph` has exactly one caller in the
page. Grepping `applyThreat`'s body alone could never have seen a
rebuild reached some other way.

A severed transit node stays greyed and stays uncounted. Nothing can
move through it either, so saying so is true; it has no stock to strand,
so counting it would inflate a figure about supply.
@bgent19
bgent19 merged commit 78f2e1a into main Aug 17, 2026
1 check passed
@bgent19
bgent19 deleted the feat/45-dataset-threat-cycles branch August 17, 2026 12:11
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.

D and T: dataset and threat-picture cycling

1 participant