Skip to content

test(cliffs): the chunk-border gate cannot be scored from any fixture (#84) - #127

Merged
wormeyman merged 1 commit into
mainfrom
test/chunk-border-gate-unscoreable
Aug 4, 2026
Merged

test(cliffs): the chunk-border gate cannot be scored from any fixture (#84)#127
wormeyman merged 1 commit into
mainfrom
test/chunk-border-gate-unscoreable

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

#122 turned applyCliffs' fifth-argument test - updateConnections runs on the chunk's outer ring and nowhere else - from an inert reading into the thing its destroyed-versus-never-queued verdict rests on, and said scoring it was "now worth doing on its own account".

This is that attempt. It comes back negative, and the reason is worth more than the attempt.

The test that ought to work

A cliff end pointing at a cell that is not there is a dangling end. If updateConnections ran on every cell there could be none anywhere - trimming them is what the pass is for. If it runs only on the outer ring, one could survive on a non-border cell. So the game's own output should separate the two readings, with no new capture at all.

It does not, because there are none at all

Over thirteen arms from all three Vulcanus cliff fixtures - real settings and the collapsed rule, resources on and off, all four regions - across 2785 border and 3750 interior cells judged, zero cells have a dangling end. Neither population, not one arm.

The detector is not asleep: deleting one cell from each arm's set makes the same code report a dangling end immediately, in all thirteen.

Why - every removal mechanism preserves connection consistency

  • a destruction runs Cliff::onDestroy, which trims the facing end of every connected neighbour, so it cannot leave one dangling;
  • updateConnections trims dangling ends by definition;
  • and the crossing field never emits one - cliffConnections.spec.ts already measured the port's queue as connection-consistent, and this says the same of the game's output.

So both readings of the gate predict exactly what the game shows. No capture of map-generation output can separate them, however many regions it covers.

What this means for #122

Its conditional stands and cannot be discharged with what is on disk - and not for want of data, which is the useful part. The gate's only observable consequence is in a counterfactual: remove a cell the game has, and ask what its neighbour keeps. Map generation never produces the world that would settle it, because it never truncates a run without the cascade running.

Anyone revisiting this needs a different kind of evidence - the disassembly itself, or a runtime probe that destroys a cliff outside map generation - not another region. Do not capture more of the same hoping the gate falls out.

A property worth having anyway

Connection consistency of the game's cliff output is now pinned across every capture, which nothing asserted before. Any future model that emits a dangling end is wrong on the game's own terms, wherever it emits one.

Verification

pnpm run verify green - 1472 app (up 3) + 13 worker + 3 container tests, 0 warnings, exit 0. Measurement only.

🤖 Generated with Claude Code

https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU

…#84)

#122 turned `applyCliffs`' fifth-argument test - `updateConnections` runs on the
chunk's outer ring and nowhere else - from an inert reading into the thing its
destroyed-versus-never-queued verdict rests on, and said scoring it was now
worth doing on its own account. This is that attempt. It is negative, and the
reason is worth more than the attempt.

The test that ought to work: a cliff end pointing at an absent cell is a
DANGLING END. If `updateConnections` ran everywhere there could be none; if it
runs only on the outer ring, one could survive on a non-border cell. So the
game's own output should separate the readings with no new capture.

It does not, because there are none at all. Over THIRTEEN arms from all three
Vulcanus cliff fixtures - real settings and the collapsed rule, resources on and
off, all four regions, 2785 border and 3750 interior cells judged - zero cells
have a dangling end, in either population, in any arm. The detector is not
asleep: deleting one cell from each arm makes the same code report one
immediately, in all thirteen.

Why: every removal mechanism preserves connection consistency. A destruction
runs `Cliff::onDestroy`, which trims the neighbour's facing end;
`updateConnections` trims dangling ends by definition; and the crossing field
never emits one. So BOTH readings of the gate predict exactly what the game
shows, and no capture of map-generation output can separate them however many
regions it covers.

Consequence for #122: its conditional stands and cannot be discharged with what
is on disk - and not for want of data. Map generation never produces the world
that would settle it, because it never truncates a run without the cascade
running. Anyone revisiting needs a different KIND of evidence - the disassembly,
or a runtime probe that destroys a cliff outside map generation - not another
region.

Also pins connection consistency of the game's cliff output across every
capture, which nothing asserted before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU
@wormeyman
wormeyman merged commit 1109bc9 into main Aug 4, 2026
6 checks passed
@wormeyman
wormeyman deleted the test/chunk-border-gate-unscoreable branch August 4, 2026 02:09
wormeyman added a commit that referenced this pull request Aug 4, 2026
… is on chunk borders (#84) (#131)

Two things were stuck at a sample size rather than at an idea. The unexplained
population was 14 cells, and every structural test on it landed at 1.4-1.9 sigma
against its base rate - what a partition looks like at n=14 whether or not a
cause exists. And the shipped accuracy figure was measured on THREE regions,
chosen years into this investigation for reasons unrelated to sampling.

Four more regions with the ore lever, ~2.5s a capture.

THE PORT GENERALISES, measured where it was never fitted:

|                     | original 3 | the 4 new |
| ---                 | ---        | ---       |
| raw cells           | 1756       | 2789      |
| game cliffs         | 1531       | 2590      |
| raw a strict SUPERSET | yes      | yes, all four |
| predicate precision | 0.971      | 0.9877    |
| predicate recall    | 0.889      | 0.8090    |
| false rejections    | 6          | 2         |

[3000,3000] is exact - 362 raw, 362 game, nothing to explain - and is also the
one region with no resource entity at all, which is what every result from #123
onward predicts.

THE CHUNK-BORDER ENRICHMENT REPLICATES OUT OF SAMPLE:

|                | unexplained | on border  | base rate |
| ---            | ---         | ---        | ---       |
| original 3     | 14          | 9 (64.3%)  | 45.0%     |
| the 4 new      | 13          | 9 (69.2%)  | 47.2%     |
| combined       | 27          | 18 (66.7%) | ~46.3%    |

Read the status before the number. The replication is 1.59 sigma alone and the
combined figure ~2.1 - short of decisive, and this is a LEAD not a result. What
changed is the KIND of evidence: the hypothesis was formed on the original 14,
dismissed there as noise at 1.45 sigma, and is tested here on 13 cells captured
afterwards in regions chosen before any of them was known.

Why it matters: chunk borders are `updateConnections`' entire domain - the one
rule that treats border cells differently, which our port measures firing ZERO
times and models as an UPPER bound. #122 promoted its gate from inert to
load-bearing; #127 showed it cannot be scored from map-gen output. This is the
first positive evidence that it does anything.

And n is 27 now, which is what unblocks the next attempt: at 14 nothing could be
settled; at 27 the border question is answerable with one more capture round.

New fixture + PROVENANCE entry.


Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 4, 2026
…, 2.91 sigma (#84) (#132)

#131 reported a chunk-border enrichment as a lead and said one more capture
round would settle it. It did.

| batch                 | unexplained | on border  | base  | z    |
| ---                   | ---         | ---        | ---   | ---  |
| original 3 regions    | 14          | 9 (64.3%)  | 45.0% | 1.45 |
| +4 regions (#131)     | 13          | 9 (69.2%)  | 47.2% | 1.59 |
| +8 regions (here)     | 17          | 12 (70.6%) | 46.0% | 2.03 |
| combined, 15 regions  | 44          | 30 (68.2%) | ~46.3%| 2.91 |

p is about 0.002. This is a RESULT, not a lead.

THE PREDICTION WAS REGISTERED BEFORE THE CAPTURE, in
`captureVulcanusCliffEntitiesBorderBatch`'s own doc comment: ~26 more
unexplained cells with ~17 on a border if the enrichment is real at ~66%,
combined ~2.9 sigma - and a FALL back toward the base rate if it is noise. It
came back 12 of 17, at a higher rate on fewer cells, combined landing on 2.91.
Effect size stable across all three batches (64-71%) against a stable base rate
(45-47%).

The first row was correctly dismissed as noise when it was all there was. Its
value came from being written down so it could be re-tested once n grew.

WHY IT POINTS AT `updateConnections`: chunk borders are that rule's entire
domain - `applyCliffs` gates it on `tryToAddCliff`'s fifth argument, which is
`!onChunkBorder`. Nothing else in the pipeline treats border cells differently.
And it is the rule the port has the weakest grip on: measured firing ZERO times
on our own set, modelled as an UPPER bound (the one place the port is knowingly
not a transcription), promoted from inert to load-bearing by #122, and shown by
#127 to be unscoreable from map-gen output. This is the first positive evidence
it does anything.

WHAT IT DOES NOT SAY: not that those 30 are destroyed BY updateConnections. An
enrichment on a rule's domain is not the rule firing, and 14 of the 44 are not
on a border at all - if this is one cause it is not the only one.

The port also generalises: 5581 raw / 5134 game cliffs, strict superset in all
eight regions, precision 0.9818, recall 0.8434. Two regions have no unexplained
cells and [-1600,3200] reproduces the game exactly (686 = 686), so the residual
is concentrated rather than a background rate - itself a constraint on any
mechanism proposed for it.

New fixture + PROVENANCE entry; capture ~31s.


Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 4, 2026
…ism REFUTED (#84) (#134)

#132 gave `updateConnections` the lead on the clause "nothing else in the
pipeline treats border cells differently at all". That clause was false, and
the counter-example makes the lead stronger rather than weaker.

A cliff's collision box reaches 3.371 tiles; a border cell's centre is 1.5-2.5
tiles from the chunk edge and every interior cell's is 5.5-6.5. So 16 of the 20
orientations cross a chunk edge from the outer ring and none can from anywhere
else - a second border-only channel. And there is a rule on the far side of it:
`Surface::checkTileCollisions` skips a tile whose id is 0, and
`getEffectiveTileID` returns 0 for an absent chunk, so a box reaching into a
not-yet-generated chunk reads no tile and does not collide.

Scored on twelve regions already on disk, with the losing condition registered
before the run - "a non-crossing border rate that is not lower than the
crossing one refutes it":

| population | unexplained | rate |
| --- | --- | --- |
| interior | 9 / 4484 | 0.20% |
| border, box crosses an edge | 7 / 1407 | 0.50% |
| border, box does not cross | 14 / 2479 | 0.56% |

No-information predicts 7.6 crossing cells; observed 7. Refuted. Its own
direction fails too: an absent chunk makes the game KEEP a cliff, so its
signature is a false rejection with a blocking tile across an edge - 0 of 9,
and the predicate is non-vacuous.

The gain is a property the 2.91-sigma result did not have: the enrichment is
orientation-BLIND, which is what a cell-index gate looks like. Both halves keep
the 2.7x enrichment over the interior.

`Cliff`'s connection system is also now decompiled whole. Three facts change
what to try next:

- `onDestroy`'s cascade has FOUR gates, including `map->[0x240] == 0` and an
  entity flag set from the creation parameters. Map generation satisfies all
  four; a Lua or editor probe need not - a hazard for exactly the runtime probe
  #127 asked for.
- `crater-cliff` has no connections at all: the whole system is gated on
  `proto->place_as_crater == nullptr`.
- There is no second connection pass during map generation.
  `updateAndFixConnections` is called from one site in the binary,
  `CliffEditor::buildCliffs`.

No new fixtures, no shipping behaviour change.


Claude-Session: https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 4, 2026
…#84) (#135)

#134 recorded `Cliff::onDestroy`'s four cascade gates and warned that a Lua or
editor destroy "need not" satisfy them, which would make #127's runtime probe
vacuous. **That warning is withdrawn.** It was stated from inference - "map
generation evidently satisfies all four, because #113 measured the cascade
running" - and inference is what keeps going wrong on this issue.

| gate | cascade needs | `applyCliffs` | `luaCreateEntity` |
| --- | --- | --- | --- |
| entity flag bit 5 of `+0x6e` | clear | `params[0x80] = 0` | `params[0x80] = 0` |
| `this+0x82` | set | ctor writes 1 unconditionally | same ctor |
| `this+0x83` | set | `place_as_crater == nullptr` | same |
| `map->[0x240]` | zero | only `Map::~Map` writes 1 | same |

- Bit 5 is exactly `params[0x80] != 0` at construction. `applyCliffs` builds its
  params at `sp+0x20` and zeroes that byte with `str wzr, [x26]`, `x26 = params
  + 0x80` (`0x101623d9c`). `luaCreateEntity` builds its own at `sp+0x228` and
  zeroes the same byte on both paths (`0x1019e5ba8`, `0x1019e5c18`) - and that
  arm is identifiably the cliff one, storing the `0x14` sentinel into
  `params+0x87` before `LuaTable::getDefault<CliffOrientation>`.
- `map->[0x240]` is "the map is being torn down": `Map::~Map` sets it as its
  first act, before `ToDeleteList::clear` (`0x10163461c`), and `Map::resume`
  bails on it. Dozens of unrelated `onDestroy` handlers read the same byte.
  `Cliff+0x78` is the `Map` - `Entity::getGame` is `[[this+0x78]+0x490]`.
- The one path that WOULD differ, `Cliff::destroyWithoutCorrection`, is
  unreachable: zero direct callers under a scan of every `BL` AND `B` in
  `__text`, and no pointer in any vtable - only four in the STAB tables.

The same widened scan re-confirms #134's other caller claim, which had been made
on `BL` alone and could have missed a tail call: `updateAndFixConnections` still
has exactly one caller, `CliffEditor::buildCliffs`.

Documentation only - no code, no fixtures, no behaviour change.


Claude-Session: https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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