Skip to content

Level: one declared option contract instead of a copied list (#1649) - #1682

Merged
obiot merged 1 commit into
masterfrom
fix/1649-level-load-option-contract
Sep 18, 2026
Merged

obiot merged 1 commit into
masterfrom
fix/1649-level-load-option-contract

Conversation

@obiot

@obiot obiot commented Sep 18, 2026

Copy link
Copy Markdown
Member

Follow-up to #1654. Purely internal — no public API change, no behaviour change.

Why

#1654 fixed the six glTF options a Trigger was silently dropping by adding them to its hand-written allowlist. That fixes the instance, not the class: every option a level format gains has to be remembered in a second place or it vanishes without a warning, which is exactly how those six went missing for three releases.

#1649 said so itself — "The second is preferable on maintenance grounds" — and the PR took the first option with a one-line note and no argument for it. Nobody asked why at review time. This is that follow-up, and it is the same move #1648 made for the loader with normalizeSrc / needsBaseURL.

What

Each level format declares the options it reads, next to the code that reads them:

  • GLTFScene.loadOptions — beside addTo, which consumes them
  • TMXTileMap.loadOptions

level publishes the union internally as LEVEL_LOAD_OPTIONS, and Trigger forwards by that minus async. Naming an option once, beside its consumer, is now enough for it to travel everywhere.

Derived on first use, not at module scope: level reaches the formats, which reach the renderables, which reach trigger, so reading the export while that cycle unwinds throws on the temporal dead zone. Two specs caught it.

Backward compatibility

  • The forwarded set is byte-identical to before — asserted against the pre-change 12-name list; nothing added, nothing dropped.
  • level.load accepts exactly the same options, with the same typedoc.
  • index.ts untouched; all three declarations are @ignore @internal, so strip-internal keeps them out of the published .d.ts (verified on the built output).

Tests

The "every option survives" assertion is derived from the contract, so it fails the day a declared option stops travelling rather than only catching the six it was written for. Verified by declaring a new option on GLTFScene while restoring the old hand-written forwarder — it fails and names the option; under the contract the same option travels with no change to Trigger.

It also replaces a test from #1654 that asserted the six keys were absent and therefore passed identically with the fix reverted. Split into the two halves that do pin something: the explicit-undefined guard, and async exclusion on both paths.

Docs

Two statements I added alongside #1654 were wrong and are corrected here: the tilemaps skill said a Trigger "passes its own settings through" — the exact belief that produced #1649 — and the async rationale held only on the transition path, since a trigger with no fade never sets it.

7007 tests, lint clean. No changelog entry: the user-facing fix is already documented under #1649, and this is an internal refactor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t

Purely internal; no public API or behaviour change. The set a `Trigger`
forwards is byte-identical to before, `level.load` takes the same options
it always did, `index.ts` is untouched, and the new declarations are
`@internal` so they are stripped from the published types.

#1654 fixed the six glTF options a Trigger was dropping by adding them to
its hand-written allowlist. That fixes the instance, not the class: every
option a level format gains has to be remembered in a second place or it
silently vanishes, which is exactly how those six went missing for three
releases. #1649 said as much — "the second is preferable on maintenance
grounds" — and the PR took the first without arguing for it.

So each format now declares the options it reads next to the code that
reads them (`GLTFScene.loadOptions`, `TMXTileMap.loadOptions`), `level`
publishes the union as `LEVEL_LOAD_OPTIONS`, and `Trigger` forwards by
that minus `async`. Naming an option once, beside its consumer, is now
enough. Same shape as the loader's `normalizeSrc`/`needsBaseURL` contract
in #1648.

Derived on FIRST USE rather than at module scope: `level` reaches the
formats, which reach the renderables, which reach `trigger`, so reading
the export while that cycle unwinds throws on the temporal dead zone —
two specs caught it.

Tests: the "every option survives" assertion is derived from the contract,
so it fails the day a declared option stops travelling rather than only
catching the six it was written for. Verified by declaring a new option on
`GLTFScene` and restoring the old hand-written forwarder: it fails, naming
the option. Replaces a test that asserted the six keys were ABSENT and so
passed identically with the fix reverted — split into the two halves that
do pin something (the explicit-undefined guard, and `async` on both paths).

Docs: two statements I wrote alongside #1654 were wrong. The tilemaps
skill said a Trigger "passes its own settings through", which is the exact
belief that produced #1649; and the `async` rationale held only on the
transition path, since a trigger with no fade never sets it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
Copilot AI lite review requested due to automatic review settings September 18, 2026 04:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit b40fd3f into master Sep 18, 2026
6 checks passed
@obiot
obiot deleted the fix/1649-level-load-option-contract branch September 18, 2026 07:06
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.

2 participants