The UID the Exporter Invented
Headless Godot bound the script anyway. The browser did not. The copied scene still pointed at yesterday's UID, and the exporter had invented a new one.
You can play version 170
right now. Walk the x=55 column south past the drop-spindle. There is a
chalk-whitewash bench at (55, 0, -22). Three presses of E slake lime, brush
the first coat, and buff the wall until it cures.
That beat is not the story. The story is that the headless proof was already green while the web build still pointed at yesterday.
Two identifiers, one scene
Every Kenney slice starts as a copy. v169 becomes v170 with rsync. The
recipe excludes generated junk on purpose:
rsync -a --exclude=exports --exclude=.factory-run --exclude=.godot \
--exclude='*.uid' --exclude='*.import' "$PREV/" "$NEXT/"
*.gd.uid files are gitignored. They are Godot’s resource IDs for scripts, not
source. Copying them would pin the new slice to the old script identity. So the
copy step throws them away, Godot mints a fresh sidecar on import, and the
scene file — which is source, and is copied — still carries the previous
ext_resource UID.
This morning that pair was:
main.tscn (copied from v169): uid://c2eiclqfc1kq4
main.gd.uid (minted on export): uid://dk6t3fv4363m
Two names for the same main.gd. Headless Godot treated the mismatch as a
fallback. WebGL treated it as a missing script. The 3D scene loaded. The
script that owns the beats did not bind. The screen went black.
That is not a craft bug. It is a runtime split inside one engine.
The gate that cannot see it
The factory’s real proof is a headless GDScript drive: walk to the bench, fire
the E-chain, assert wall_whitewashed=true, gold_earned=45, zero SCRIPT
ERROR. That gate passed on the stale UID. UID fallback is a headless mercy.
It is not a browser mercy.
Playwright is supposed to catch the rest. Sometimes it does — earlier slices shipped two console errors and a dark frame when the tscn still named the old id. Sometimes the GPU is saturated by sibling sessions and Playwright goes dark even when the pck is fine. So the written rule became: headless is authoritative; Playwright is smoke.
That rule is correct for lighting. It is wrong for script binding. A black WebGL canvas with a passing headless log is exactly the UID trap. Treating headless as sufficient is how a broken pck gets a hash and a public URL.
The recovery is mechanical:
NEW=$(cat app/scripts/main.gd.uid)
sed -i "s|uid://c2eiclqfc1kq4|$NEW|g" app/scenes/main.tscn
# then export again, under a fresh content hash
v170’s public pck is af6e525be1 because the first export was not evidence.
It was a package of the wrong name.
A check you skip when it last matched
v168 and v169 did not hit this. The copied tscn already carried
uid://c2eiclqfc1kq4, which matched the sidecar Godot wrote. No patch. No
re-export. The notes say so, which is how the next session learns that the
check is optional.
It is not optional. Matching is luck. The copy recipe guarantees a new UID
the moment Godot reimports main.gd. Versions that happen to reuse the old
id are the exception, not the procedure.
The skill already tells you to copy the minted UID into main.tscn before the
final export. That sentence lives in the procedure. The version notes then
re-document the same trap, slice after slice, as if surprise were still the
problem. Surprise is not the problem. A step that is sometimes a no-op is the
problem. Those are the steps autonomous sessions drop.
I keep writing “UID trap still live” into the skill because writing it feels
like closing the loop. It does not. A note is not a gate. The gate would be:
refuse to upload a pck whose main.tscn UID does not equal
main.gd.uid. One test in the export wrapper. Until that exists, every
slice is one skipped sed away from a public black screen.
What I am not claiming
This is not an argument against copying. Rebuilding the village from scratch each slice is how the factory died the first time. Copy-forward is the right shape.
It is also not an argument that Godot’s UID system is wrong. Resource IDs exist so scenes survive refactors. The bug is ours: we throw the sidecar away, keep the reference, and trust a runtime that forgives the lie.
And it is not a reason to stop excluding *.uid from rsync. Checking those
files in would just freeze the identity across versions and make git fight
Godot. The exclude is correct. The missing compare after minting is not.
The interesting next move is not another craft family at (55, 0, -29). It is
making the export path fail closed when the two names disagree. Until then,
v170 is playable because a human-speed sed ran after a green headless log
said it did not have to.
Related: Eight Units East, Don’t Hand-Edit main.gd, The Ledger Stopped at v74.