Skip to content

feat: schem→nbt converter (delve-schem) — Sponge v2/v3, safety strip, oversize split - #32

Merged
stellarfeline merged 1 commit into
mainfrom
m3-schem-converter
Jul 31, 2026
Merged

feat: schem→nbt converter (delve-schem) — Sponge v2/v3, safety strip, oversize split#32
stellarfeline merged 1 commit into
mainfrom
m3-schem-converter

Conversation

@stellarfeline

Copy link
Copy Markdown
Owner

What changed (design level)

New workspace crate crates/schem (spec-0007 shared machinery item 1): delve-schem converts Sponge schematics (v2 and v3, auto-detected) into vanilla structure .nbt pinned to MC 1.21.11 (DataVersion 4671).

  • Safety strip (community-contract audit hook): command/structure/jigsaw blocks and all spawner-class block entities are unconditionally replaced with air (DW0700 warning each, with position); block entities whose NBT embeds Command/spawn data are stripped conservatively. --palette-report feeds the future admission allowlist audit.
  • Oversize splitting: --split tiles >48³ inputs into deterministic grid parts + a reassembly manifest.
  • Determinism: BTreeMap-backed NBT carrier (fastnbt's Compound is hash-ordered) → double-convert byte-identity holds for single outputs, all split parts, and manifests.
  • In-code fixtures only (no network): v2/v3 rooms, block-entity cases (chest survives with items, command block stripped), 60×10×60 split case.
  • Out of scope v1 (documented): biomes, entities, block-state migration across DataVersions (DW0702 warning on mismatch).

What CI now proves

11 new tests: round-trips, v2/v3 convergence, byte-identity, strip policy, lossless split reassembly, CLI exit codes. Workspace fmt/clippy/test green. No existing crate touched.

Mechanical-class PR → auto-merge on green.

🤖 Generated with Claude Code

New `delvewright-schem` crate + `delve-schem` binary: converts Sponge
schematics (v2 and v3) into deterministic 1.21.11 structure templates
(DataVersion 4671) for the prefab admission pipeline.

- Parses both Sponge layouts (flat v2 root; v3 `Schematic`/`Blocks` wrapper),
  palette block-states with properties, LEB128 varint block data, and
  position-rebased block entities.
- Safety strip (community-contract audit hook): unconditionally removes command,
  structure, and jigsaw blocks plus the spawner family (mob_spawner /
  trial_spawner / vault) and any block entity carrying an embedded command or
  spawner definition, emitting a per-strip DW0700 warning. `--palette-report`
  prints the admission allowlist feed.
- Oversize splitting past the 48-cube cap into `<base>.xIyJzK.nbt` parts plus a
  `<base>.split.json` reassembly manifest.
- Determinism (ADR-0006): BTreeMap-backed NBT value type (fastnbt's Value is a
  HashMap), sorted palette, fixed block iteration, gzip mtime 0 — proven by a
  double-convert byte-identity test.
- In-code v2/v3 reference fixtures (no network); tests cover block-for-block
  round-trips, v2/v3 convergence, strip + chest survival, byte-identity, and
  lossless split reassembly, plus end-to-end CLI tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@stellarfeline
stellarfeline merged commit 017a137 into main Jul 31, 2026
6 checks passed
@stellarfeline
stellarfeline deleted the m3-schem-converter branch July 31, 2026 15:57
stellarfeline added a commit that referenced this pull request Jul 31, 2026
Harden the gap-8 transport wait so the critical-path bot reliably survives a
compiler-emitted cross-area `teleport @s` (areas sit ~256 blocks apart across
void). The teleport itself always registers via mineflayer's `forcedMove`; the
race was the harness racing ahead of it into the next step's pathfinding.

- Reset the pathfinder on a large server-forced position jump (`forcedMove`
  handler), so a path computed in the OLD area cannot survive the teleport and
  strand the next step with a spurious "No path to the goal!".
- After the jump lands, wait for the destination chunk to load and the bot to
  rest on solid footing (`onGround` + `blockAt != null`) before the next step
  pathfinds — closing the race where A* starts against an unloaded chunk.
- Factor the transport/footing waits through one bounded, death-aware `waitFor`
  helper, so nothing can hang the run and a mid-transport death still fails fast
  (PR #50 death fast-fail exit 3 preserved; no death/select-class changes).

Navigation/assertions only. New unit tests cover jump-wait, footing-hold,
pathfinder reset on the jump, death-abort, and the large-jump threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stellarfeline added a commit that referenced this pull request Jul 31, 2026
…59)

Harden the gap-8 transport wait so the critical-path bot reliably survives a
compiler-emitted cross-area `teleport @s` (areas sit ~256 blocks apart across
void). The teleport itself always registers via mineflayer's `forcedMove`; the
race was the harness racing ahead of it into the next step's pathfinding.

- Reset the pathfinder on a large server-forced position jump (`forcedMove`
  handler), so a path computed in the OLD area cannot survive the teleport and
  strand the next step with a spurious "No path to the goal!".
- After the jump lands, wait for the destination chunk to load and the bot to
  rest on solid footing (`onGround` + `blockAt != null`) before the next step
  pathfinds — closing the race where A* starts against an unloaded chunk.
- Factor the transport/footing waits through one bounded, death-aware `waitFor`
  helper, so nothing can hang the run and a mid-transport death still fails fast
  (PR #50 death fast-fail exit 3 preserved; no death/select-class changes).

Navigation/assertions only. New unit tests cover jump-wait, footing-hold,
pathfinder reset on the jump, death-abort, and the large-jump threshold.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
stellarfeline added a commit that referenced this pull request Aug 4, 2026
…e-leg is success (#134) (#235)

A step can complete MID-WALK: the tide-mill `obj/wheelpit` reach fires its
distance check as the bot lands a timed-gate crossing, and the objective's
emission teleports it to the next area over a physically one-way transport.
The leg's remaining hops then fail on the position discontinuity, and the
harness read that as the gate blocking a leg the bot had already walked:
three gate 'attempts', then a re-center toward a cell the one-way transport
makes unreachable — six red runs of a campaign whose server state said
dw.o_wheelpit = 1 the whole time.

The walk's failure paths now consult a completion oracle (LegSettled) built
from the two authoritative signals the harness already consumes: the step
objective's anchored [dw:complete] marker, and the bot standing at the
step's compiler-exported transport destination (the same arrival predicate
awaitTransport uses). Either one ends the leg as SUCCEEDED — no window
wait, standoff, or recovery may path the bot back across the transport —
and the leg cursor already sits at the next area's leg, so navigation
resumes there. Failure paths only: a healthy hop is never shortcut, and a
genuinely blocked leg with an unsettled step fails exactly as before.

No compiler change needed: critical-path.json already exports per-step
transport destinations (gap 8, task #32); the harness had only consulted
them AFTER a step succeeded, never while its walk was still being judged.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
stellarfeline added a commit that referenced this pull request Aug 8, 2026
87a8ade -> e4c58c0f78f2e0e949c625697c3f5b04df125aff

Carries content #29 (the flock walks out of the pen), #32 (three actor
nameplates the widened l10n inventory now requires) and #33 (untrack the
absolute-path symlink).

Ordering, established by measurement rather than assumed: this must land
BEFORE #317. A sidecar row the engine does not inventory is tolerated (built
green against today's main), but an inventory key with no sidecar row is
DW0180 and hard — so new-engine-with-old-content reds, and old-engine-with-
new-content does not. The pin therefore leads the engine here. Note this is
the OPPOSITE of the #260/content#21 ordering, where PrefabMeta's
deny_unknown_fields made engine-first the only safe order: the direction is a
property of the change, never a rule of the repo.
stellarfeline added a commit that referenced this pull request Aug 8, 2026
…on (spec-0029) (#317)

* feat(compiler): two holes where player-visible text escapes translation (spec-0029)

The owner played the island in Chinese and read English. Two independent leaks,
both closed here, plus the general form of each so neither class can recur.

**Hole 1 — a body's name was not in the inventory.** `dsl::l10n::each_string`
inventoried an actor's DROPS but never the actor's own `name`, so every
`actors[].name` shipped as a literal. That is one hole with two signatures on the
island: the giant is `npc/polyphemus` (translated, correct when he stands still)
AND four actor puppets all named `Polyphemus` (untranslated, English the moment a
cutscene stages one) — while the eight sheep have no NPC identity at all and were
English in every frame. 16 actors, 16 carrying a name, 0 inventoried.

Inventorying the field alone would have made it worse: a translator asked for
`Polyphemus` five times can answer five ways, and the giant's name would change as
he walked into a cutscene — an authored defect rather than an accidental one. So
**an entity display name is keyed by its canonical English text, not by its
declaration site**: the first site in the traversal (NPCs before actors) owns the
key and every later site carrying the identical name emits that same key. The
translator is asked once; two bodies a player reads as one character cannot render
as two. Scoped to `npc.*.name` + `actor.*.name` — prose stays per-site, and
merging `wave.*.mob.*.name` would retire keys live campaigns already translate,
which is an owner call, not this PR's.

**The general form.** `DW0185` proves a string the inventory KNOWS reaches a
component; it is blind to one the inventory never met, which ships English in
silence — how this survived twenty playtest rounds. `crates/dsl/tests/
l10n_surface.rs` closes that half: it enumerates every string-valued property of
the seven stage schemas (derived from the Rust types, so complete by construction
— 78 today) and requires each to be classified Inventoried / Reference / Machine /
NotPlayerVisible(<why>), in both directions. A new `String` anywhere in the DSL is
red until somebody records whether a player reads it. A test rather than a DW
code because the defect is in the compiler: no campaign input can produce it.

**Hole 2 — the compiler's own chrome was untranslatable.** Thirteen strings the
compiler writes itself had no key and no override. They are now compiler-owned end
to end (`dsl::chrome`): reserved `delvewright.ui.…` keys, English canonical,
translations shipped with the engine, zero campaign authoring. Eight are product
chrome (`New objective: `, `Delve Complete`, `Choose your class`, …) — no author
wants to write those, which is why the answer is not an override; five are the
diegetic defaults whose authored overrides already exist, are unchanged, and still
win.

Chrome rides the existing road rather than a parallel one: it enters emission as a
translation tag, lowers through `emit::tr`/`snbt_component`, and a site that fails
to is `DW0185` like any authored string.

Three of the eight were CONCATENATIONS, which freeze English word order into every
language. They are now one key with `%s` carried by the component's `with` —
`"%s — complete."`, `"New objective: %s"`, `"Waiting for the party — %s / %s"` —
vanilla's own primitive for the job. A unit test requires every language's
placeholder count to equal the English's.

`DW0186` reserves the namespace: a sidecar may not define a chrome key, which
would otherwise be written into the language file and replace product chrome.

**Language coverage.** `dsl::mclang::CLIENT_LANGS` is the pinned client's own
language set — 143 stems DERIVED from Mojang's 1.21.11 asset index, with the
digests recorded and `tools/derive-client-langs.py` to re-derive; it replaces the
hand-maintained 25-entry table and is a strict superset, so nothing that compiled
stops compiling. Chrome tables: 30, covering 47 locales; every other language
reads English — absent from its lang file, never faked. Chrome is emitted only
into the files a delve already ships, so a French client never gets French chrome
around English story.

The translations are machine-produced and unreviewed; that is stated in the
module header, not glossed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL

* feat(compiler,skill): translation provenance, and the naming rule that prevents divergence

Three follow-ups on the review of #317.

**1. A sidecar row can go stale silently — now it cannot (`DW0187`).** Coverage
validation compares key SETS: rewrite an authored line and its translation is
present, applied and WRONG, with no key moved and every check green. The text-owned
key scheme this PR introduced adds a second, nastier way in — rename ONE body and
the key migrates to ANOTHER, so the row that goes stale is not the row the author
edited, and `DW0180` points somewhere else entirely.

The sidecar now records the English each row was translated FROM (`source`), so the
compiler compares instead of a human auditing. It is deliberately the GENERAL fix,
not a migration-only guard: any edited line is caught, whatever kind of string it
is.

`source` is additive — an older sidecar parses unchanged — so `DW0188` counts its
unguarded rows on every run and names the deprecation window. An unadopted sidecar
is a reported number, never a silence that reads like a pass. `i18n-translate.py`
writes `source`, so adoption is a re-run that retranslates nothing.

**2. Wave-mob names stay per-site keyed** (owner ruling, 2026-08-06). Live
campaigns keep the keys they already translate.

**3. The prevention moves to authoring time, where she put it.** Declining the
merge leaves the divergence real, so the rule goes where an author can act on it:
`SKILL.md` *Writing craft* §D — byte-identical entity names collapse to one name
and one translation, wave-mob names do not, and what to do in each direction.
Written as a rule applied while naming, for an authoring agent; the key-scheme
mechanics stay in `compiler.md`, for a compiler maintainer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL

* chore(audit): two baked-string findings are closed by this PR — drop their ledger entries

The capability-ownership checker refuses a stale exemption by name: an entry in
BAKED_STRINGS whose constant no longer exists is a FAIL, because "a stale
exemption hides the next one". This PR moves SEAL_HINT_DEFAULT and
BOUNDARY_DEFAULT_MESSAGE behind `dsl::chrome`, where they are inventoried and
translatable — so both stop being baked literals, and the guard fired on its
first real occasion.

Ledger entries dropped; the audit doc's row 23 and its check-B binding count
(5 -> 3) follow, and the demonstrated-firing transcript is marked historical for
B with a surviving entry named for reproducing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjQ5p1Kv5MrkGPumi7yXWL

* chore: re-pin content onto the island v1.1 batch

87a8ade -> e4c58c0f78f2e0e949c625697c3f5b04df125aff

Carries content #29 (the flock walks out of the pen), #32 (three actor
nameplates the widened l10n inventory now requires) and #33 (untrack the
absolute-path symlink).

Ordering, established by measurement rather than assumed: this must land
BEFORE #317. A sidecar row the engine does not inventory is tolerated (built
green against today's main), but an inventory key with no sidecar row is
DW0180 and hard — so new-engine-with-old-content reds, and old-engine-with-
new-content does not. The pin therefore leads the engine here. Note this is
the OPPOSITE of the #260/content#21 ordering, where PrefabMeta's
deny_unknown_fields made engine-first the only safe order: the direction is a
property of the change, never a rule of the repo.

---------

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