fix(#811,#812,#813,#814): detect the joint palette structurally, delimit its token, key downgrades by file, and floor the cap - #820
Conversation
…mit its token, key downgrades by file, and floor the cap #811: joint-palette detection was `src.contains("JointMatrices")` -- a struct NAME. A shader declaring a fixed-length mat4x4 uniform palette under any other name, or through a type alias, shipped unguarded and measured green. The IR check that resolves uniform-address-space struct members through naga's type arena now runs over the WHOLE discovered corpus instead of a name-derived subset, so the set under test is no longer chosen by the thing being tested. A new test substitutes a sentinel cap and asserts every palette length in the IR tracks it, which also catches an alias-declared or whitespace-spread palette that happens to be the correct value today. #812: the substitution was `src.replace("JOINT_CAP", "128")`, which is not word-boundary safe -- any identifier with `JOINT_CAP` as a prefix was corrupted into a number, and every comment mentioning the cap was rewritten into self-contradicting prose. The token is now the delimited `${JOINT_CAP}`; `$`, `{` and `}` cannot appear in a WGSL identifier, so the token cannot be a substring of one. Three tests pin this: the token cannot occur inside an identifier, substitution cannot alter an identifier containing the bare word, and substitution leaves every comment byte in every corpus shader identical. #813: the downgrade map re-keyed on (label, gender), which double-reports one file for archetypes that have no `_f` variant -- both genders load the same GLB. The key is now the file name actually loaded, and the path resolution that decides which file that is has been lifted out of `ensure_character_model` into `resolve_character_model_path` so the tests drive the real resolver rather than a transcription of it. A property test asserts entry count always equals distinct file count over the powerset of `_f` presence. #814: unifying the cap removed the only loud check on its VALUE, so a too-small cap silently reclassified real rigs as ExceedsCap and rendered them unskinned. The bound was measured from the shipped GLB corpus (widest rig 127 joints) and is now enforced twice: a `const _: () = assert!` that fails the build, and a non-ignored runtime test that names the measured distribution and its provenance.
CHANGES REQUESTEDIndependent adversarial review. I did not write this change. I worked in my own worktrees at the PR The code is right. The structural detection works, the delimited token works, the file key works, 1. BLOCKING — the reach control does not detect a truncated walk; three tracked sentences say it doesThree places make the claim:
its own docstring:
and the PR body:
Why it cannot holdThe control derives both of its sets from the same walk it is certifying: let corpus = shader_corpus();
assert!(corpus.len() >= EXPECTED_JOINT_SHADERS.len(), ...); // floor is 3
for name in EXPECTED_JOINT_SHADERS { assert!(corpus.contains_key(name), ...); }
let parsed = corpus_palette_lengths(); // == shader_corpus().iter().map(..)
assert_eq!(parsed.keys(), corpus.keys(), ...);
Measured (RV-M3)Planted, in my worktree at the PR head:
alias JMat = mat4x4<f32>;
struct DecoyPalette { mats: array<JMat, 99> };
@group(0) @binding(0) var<uniform> decoy: DecoyPalette;
// RV-M3: simulate the eqoxide#778 failure mode — a walk that silently stops short of its corpus.
let keep: Vec<String> = EXPECTED_JOINT_SHADERS.iter().map(|s| s.to_string()).collect();
out.retain(|k, _| keep.contains(k));Result — fully green, with the violation present on disk: That is 13/0 — indistinguishable from the clean control — with a 99-length uniform What the control does catch (RV-M9, so this is a scoped claim, not a dismissal)I mutated only the parse stage, leaving the walk intact So the control detects walk-vs-parse divergence. It does not detect walk truncation, which is the Suggested fix (small, and it closes the gap rather than only correcting the prose)There is already an independent enumeration of the corpus in the tree — (12 occurrences, 11 distinct files — 2. NON-BLOCKING — the walk is not recursive, but its docstring reads as if it is
It is a single non-recursive
alias JMat2 = mat4x4<f32>;
struct NestedPalette { mats: array < JMat2 , 77 > };
@group(0) @binding(0) var<uniform> nested_decoy: NestedPalette;In the same run that caught the top-level decoy (RV-M1, below), nothing named the nested file: No subdirectory exists today, so this is a wording fix ("every 3. NON-BLOCKING — two sentences describe
|
base 9e41178 |
head 6736287 |
|
|---|---|---|
Finished |
test profile in 3m 13s |
test profile in 33.36s (warm) |
running N tests? headers |
54 | 54 |
test result: lines |
54 | 54 |
| headers == results | yes | yes |
| header sum | 1825 | 1834 |
| passed | 1778 | 1787 |
| failed | 0 | 0 |
| ignored | 47 | 47 |
| filtered | 0 | 0 |
| passed+failed+ignored+filtered | 1825 | 1834 |
| == header sum | yes | yes |
| all-zero targets (field-anchored) | 15 | 15 |
non-canonical test result: lines |
0 | 0 |
^error|panicked|Killed in stderr |
0 | 0 |
Delta: +9 passed / +0 targets / +0 ignored / +0 failed — matches the PR body exactly. Accounted
for: joint_cap_single_source 9 → 13 (+4, names read off both runs), skin_cap_selection 9 → 14
(+5), one ignored test in that file on both trees. Green control on the unmutated head for the two
files under change: 13 passed / 0 failed and 14 passed / 0 failed / 1 ignored.
Live end-to-end run
Release build of this branch, built on the remote builder and fetched back; I confirmed the running
process was that binary before believing anything it printed (/proc/<pid>/exe resolved to this
worktree's target/release/eqoxide, whose mtime was ~2 minutes before launch). Logged into a live
world as a level-4 wood elf ranger, zoned into steamfont.
Skinned models actually loaded and rendered:
renderer: loaded skinned model 'race_elf' (109 joints, 131 clips)
renderer: loaded skinned model 'race_hum' (104 joints, 141 clips)
renderer: loaded skinned model 'race_gnm' (104 joints, 138 clips)
Filtering the whole session log:
$ grep -aiE "wgsl|shader|validation error|panicked|adapter error|naga" live.err
(no output)
$ grep -aiE "unskinned|ExceedsCap|exceeds|downgrad|static fallback|StaticReason|no skin" live.err
(no output)
$ grep -cE ' ERROR ' live.clean
0
27 WARN lines, all accounted for and all unrelated to this change: 26 doors: missing model "…" — using fallback box and one egui_wgpu framebuffer-format note. Zone assets reported
state: ready, terrain_meshes: 24, collision_loaded: true. Clean shutdown via the lifecycle exit
endpoint; process confirmed gone and the port released.
Honest limitation on the live evidence. The widest rig I got on screen was 109 joints
(race_elf) — narrower than the author's reported 110, and well short of the 127-joint
race_pcfroglok that sets the floor. I could not widen it: the character was frozen at the zone-in
point by an unrelated pre-existing movement condition (controller HOLD [embedded_no_recovery] —
loudly and correctly reported, not a silent failure, and in a file this PR does not touch), so I
could not travel to find a wider rig, and no froglok PC was available. So the live run confirms the
premise that a 128-slot palette renders real rigs with zero shader or validation noise, and confirms
nothing about the 127 boundary. Per the standard I was given, it discharges no claim containing
"never", "always", or "cannot" — and I have not used it to.
NOT verified
- The 127 boundary at runtime. No 127-joint rig was rendered live. The floor is verified by the
const assert, by the runtime test, and by my own GLB re-derivation — all static. - Block comments in shaders.
substitution_leaves_every_shader_comment_byte_identicaldoes not
model them (its docstring says so). None exist in the corpus today; I did not add a guard. - GPU-side behaviour of the uniform palette. I checked declared array lengths and buffer sizes
structurally; I did not read back rendered joint matrices or verify the shadow pass's palette on
the GPU. - The
#[ignore]dno_shipped_local_model_exceeds_the_cap_today. I did not run it. I wrote my
own independent GLB scanner instead and scanned the same cache, which is why the numbers above are
mine rather than that test's. - Non-x86_64 / non-Vulkan backends. One machine, one adapter.
- Whether
${JOINT_CAP}collides with any other preprocessing stage (none exists in-tree
today; I grepped for a secondreplaceover shader source and found none, but I did not audit
build scripts). - Anything outside
crates/eqoxide-renderer/. The diff is confined there and I confirmed that,
but I did not review the rest of the workspace.
…ll a hardcoded length from a tracked one
Measured, not reasoned: a planted decoy declaring `array < JMat2 , 77 >` slipped
past `every_palette_length_tracks_the_substituted_token` while the sentinel was
77. A length derived from ${JOINT_CAP} equals *both* sentinels under two
substitutions; a length written into the shader can equal at most one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQVEpaaKeXsZcW9VT2roeV
Round 2 — all five fixed, with the measurement behind each replacement sentenceThanks for the reproduction. B1 was the right call: I had rebuilt #778 inside the control written to prevent #778, and I would not have found it by reading the test — I wrote both sides of that Head is now B1 —
|
| base | branch | |
|---|---|---|
running N tests? headers / test result: lines |
55 / 55 | 55 / 55 |
| non-canonical result lines | 0 | 0 |
| all-zero targets | 14 | 14 |
| passed / failed / ignored / filtered | 1798 / 0 / 47 / 0 | 1809 / 0 / 47 / 0 |
| sum == header sum | 1845 == 1845 | 1856 == 1856 |
^error|panicked|Killed in stderr |
0 | 0 |
+11 passed, 0 failed, ignored unchanged. Accounted for by running the two affected binaries at the base commit: joint_cap_single_source 9 → 14, skin_cap_selection 9 → 15 (1 ignored on both sides). The diff touches crates/eqoxide-renderer/ only, so no other target's count can have moved.
NOT verified this round
- No new live run. Round 2 changed test files and three doc paragraphs;
pipeline::wgsl's body is byte-identical. That is a reasoned scope argument about which code moved, not a second measurement, and I am marking it rather than implying the earlier live run covers this tree. - The oracle narrows the fix(#760): pin the health clock in test fixtures so a handler's HTTP status cannot depend on machine load #778 hole, it does not close it — a
.wgslnoinclude_str!names stays invisible to it. - The comment extractor is checked against the forms I planted (line, block, nested block) plus the no-string-literal premise assertion. It is not fuzzed.
- The two-root collision is a unit test, not an observation.
assets_pathis set once per renderer in the shipped flow, so I have no evidence it has ever happened; the test proves it can, which is what the old doc denied. MAX_MEASURED_CHARACTER_RIG_JOINTS = 127is still a human-maintained number; nothing in CI notices if the asset set gains a wider rig.
…S, and a phantom could pad its own floor
The reviewer reasoned that the `.wgsl` extension filter does not stop a comment
spelling `include_str!("shaders/ghost.wgsl")`, and asked for a measurement. Both
consequences measured:
- False RED: the planted comment made discovered_corpus_is_not_silently_truncated
fail with `the shader walk never returned ["ghost.wgsl"]`.
- False GREEN (worse, and not predicted): a phantom inflates included.len(), which
is what MIN_PIPELINE_INCLUDED_SHADERS counts. Orphan .wgsl on disk + a comment
naming it + the scan silently dropping one genuinely-included shader measured
14 passed / 0 failed. The oracle's own reach control was defeated by prose.
The scan now skips hits inside comments, routed through comment_spans — the same
scanner the comment-identity test uses, refactored to byte spans so there is one
comment scanner in the file rather than two. Adds
the_include_scan_ignores_comments_but_not_code, which pins both directions against
a synthetic source so it keeps testing the rule after pipeline.rs changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HQVEpaaKeXsZcW9VT2roeV
Round 3 — your hypothesis confirmed, and the second path is worse than either of us thoughtHead is now 1. The probe you asked for — false RED, confirmedPlanted Your reasoning is now a measurement. Note the failure message itself asserts the false thing — "those files provably exist (the crate would not build otherwise)" — about a file that never existed. 2. The composite — reachable, and it measures GREENYou asked whether the padding path is real or whether the superset check always fires first. It is real. The superset check fires first only when the phantom names a file that is not on disk. A phantom that names a file which is on disk passes the superset check and still inflates To reach it: an orphan Fourteen passed, zero failed, with the oracle silently missing a real shader. The floor was padded by the phantom back to 11, and the superset check never looked at the dropped name — a name the scan loses is a name the scan cannot check. That is a silent wrong answer in the control written to prevent silent wrong answers, so I fixed it rather than narrowing the sentence. 3. The fix
The scan is also split into
4. Reach controls for the fix, both directions
RC-M10 and RC-M11 are the two ways the comment scanner can be wrong, and both are loud: swallowing real code drops names below the floor, exposing extra text yields a name that is not on disk. That matters because Final state
Per your instruction I did not re-run the full workspace. That means the published merged-tree figures (1809 passed) describe NOT verified this round
|
…ypes Closes #803. `/v1/observe/zone_exits` answered `[]` with a 200 when the zone's `.wtr` failed to load. An empty list and a failed read are different facts about the world, and the agent driving this client has no second channel to tell them apart — so "this zone has no exits" was published for a zone whose exits were simply unread. The fix makes them different types rather than different values. The honest refusal now has a shape: `usable_collision()` returns `Result<&Arc<Collision>, NotUsable>`, so `get_zone_exits` takes the verdict and the grid from a single call and cannot answer from a grid it did not just validate. The previous shape — an `if let Some(col)` with no `else` — is gone, which is what makes this a type-level fix rather than another guard. The unreachable arm is spelled as a refusal rather than an `unwrap`, on the grounds that the honest answer to "we cannot find the grid" is never an empty world. **The doc claim that the fallback is unreachable was attacked in both directions, and needed both.** Forcing the fallback reachable (P2) turns the run RED at ten tests, including `usable_collision_agrees_with_usability_for_every_state` firing its own "came from the unreachable fallback" assertion. Changing the fallback's error *value* (P1) **survives** — which is the correct signature of genuine unreachability, and is only evidence when read together with P2. Either probe alone is ambiguous: P2 alone cannot distinguish an unreachable arm from an untested one, and P1 alone cannot distinguish unreachable from unpinned. An invented third mutation (P8) — the plausible "check the grid first" simplification, which would bless the *previous* zone's grid — is RED at two tests. The pin is an enumeration over the five state variants that exist today, not a property of the type, and one seam keeps it that way: `ZoneAssetState::collision()` matches with a `_ => None` wildcard, so a future variant carrying a grid would silently return `None` and make the documented-unreachable arm reachable, where `usability`'s wildcard-free match would refuse to compile. Left as a follow-up rather than fixed here, and filed rather than left in a review comment. Both mutations that survived round 1 are now RED, reproduced by the reviewer rather than accepted from the author: severing the `Unmeasured` arm's `set_region_data` gives `237 passed; 1 failed` on "the Unmeasured arm must RECORD why"; deleting the production `set_region_data` gives `211 passed; 2 failed` at exactly the two named wiring tests. The second fix is **not** a source-text pin — it extracts the wiring into a named function driven from real `.wtr` bytes on disk, so it covers the path convention and the loader too. That distinction matters here: this repo has now measured six separate ways a source-text pin can prove a call is *written* rather than *reached* (#799). **The fixture edits were measured, not reasoned about.** `ready_state()` has eight call sites in `observe.rs`, so installing `RegionMap::flat_below(-10.0)` there is not a local change — a corpus edit on this project has already silently made a mutant unkillable while every named test stayed green. Reverting the fixture (P4) turns exactly one test red, the disclosed one; no pre-existing test relied on the absence of region data. Flipping both fixtures to all-water (P6) **survives**, so the water axis is inert and the edit moves only the axis it was made for. And the plausible wrong fix at the new call site (M2″) is now caught by **three** tests, up from one in round 1 — the opposite of the usual failure mode where a repair re-instantiates the finding it replaced. The case-sensitivity window between `usability`'s `eq_ignore_ascii_case` and `zone_needs_reload`'s exact compare is closed rather than relocated: `zone_needs_reload` is strictly the more eager of the two, so a case difference can only cause a spurious reload into `Pending` and an honest 503. It cannot produce a stale-but-blessed grid. Round 1's three other blockers: the v1-map parenthetical was deleted rather than softened (v1 zone lines read as index 0, index 0 is included, so a v1 map yields `[0]` and not `[]`); both usability inventories corrected three consumers to four, re-derived by grep rather than by reread; and the unservable `NotAttached` state was documented honestly instead of being made reachable, with the doc labelling its own limit as an enumeration of construction sites rather than a type guarantee. Figures measured by the reviewer on the PR head, which **is** the merged tree — `origin/main` `41cca4e` is already an ancestor, so there is no gap between what was measured and what lands. Streams captured separately: `Finished` in 10m 42s, exit 0 · **55 `running N tests?` headers = 55 `test result:` lines** (equality is the lost-binary check) · **0** non-canonical result lines · **14** targets with nothing to run, anchored on all three fields · **1823 passed + 0 failed + 47 ignored + 0 filtered = 1870 = the header sum** · zero `FAILED`. The +13 against the 1857 baseline is reconciled two ways, by name and by a per-commit series, with all four round-2 tests named and green. The loose `0 passed` predicate reads 18 rather than 14 on this tree, as expected — it also counts targets whose tests are entirely `#[ignore]`d. NOT verified, and not claimed: - No live run this round. Round 1's live evidence does not transfer, because the B4 rewiring changed where the grid comes from. That is a scope statement, not a measurement. - The 1857 baseline is not the reviewer's own measurement; it was measured during the #820 merge verification on the tree that became `41cca4e`. - The `cfg`-marker scan supporting the `NotAttached` enumeration is a file-level heuristic, hand- checked only in the one file where the ambiguous shape occurs. - The 47 `#[ignore]`d tests were not run (#777). - "Unreachable" is pinned for the five state variants that exist today, by enumeration. Nothing in the type system enforces it, and the `_ => None` wildcard above is the seam that would break it.
Four follow-ups left behind by #809. They are in one PR because they touch the same four files (
pipeline.rs, the three palette shaders,renderer.rs,joint_cap_single_source.rs).Closes #811.
Closes #812.
Closes #813.
Closes #814.
#811 — the palette guard was keyed on a struct NAME
joint_palette_shaders()selected its corpus withsrc.contains("JointMatrices"). The IR check that actually resolves palette lengths — walking naga's type arena for uniform-address-space struct members whose type is a fixed-lengtharray<mat4x4<f32>, N>— was already alias-immune by construction, but it only ever ran over the name-derived subset. The set under test was chosen by the property being tested.Fix: the IR check now runs over every shader the corpus walk discovers. Name matching is gone from the detection path entirely;
palette_bearing_shaders_are_exactly_the_expected_threenow derives that set structurally and asserts it, so a fourth palette-bearing file fails rather than being ignored.[corrected] The corpus walk recurses.
shader_corpus()was a single non-recursiveread_dirwhile its docstring said "every.wgslundersrc/shaders/". The reviewer planted a palette-bearing decoy one directory deep and nothing named it, in the same run that named a top-level one. It now walks recursively and keys by the/-separated path relative tosrc/shaders/. Measured after the fix: the nested decoy is reported asnested/decoy_c.wgsl(RC-M4 below).[corrected]
every_palette_length_tracks_the_substituted_tokennow really is whole-corpus, and uses two sentinels. Two sentences gave this test whole-corpus scope while it iterated the three-name const list. It now iterates the discovered corpus; the const list survives only as the end-of-test positive control (a corpus that lost all three palette shaders would otherwise satisfy the loop vacuously).The second sentinel is not belt-and-braces, and I did not reason my way to it — I measured it. With a single sentinel of 77, a planted decoy declaring
array < JMat2 , 77 >slipped past this test, because a shader that hardcodes exactly the sentinel is indistinguishable from one that tracks the token.SENTINELS: [u32; 2] = [77, 91]closes that: a length derived from${JOINT_CAP}equals both under two substitutions, a length written into the shader can equal at most one.[corrected]
discovered_corpus_is_not_silently_truncatedderived both sides of its equality from the walk it was certifying. The previous version of this section said it "asserts every walked file was actually parsed, so a scanner that stops early fails instead of measuring green over a shrinking corpus." That was false, and the reviewer measured it false: with a 99-length uniformmat4x4decoy on disk and the walk truncated to three files, this test file stayed 13 passed / 0 failed.corpus_palette_lengths()is a total map overshader_corpus(), so a short walk shrank both sides of theassert_eq!together, and the only other defence was a count floor ofEXPECTED_JOINT_SHADERS.len()= 3 against a real corpus of 11. That is the #778 shape reproduced inside the control written to prevent #778.The replacement compares the walk against an enumeration this test file does not produce: the shader file names
pipeline.rscompiles in viainclude_str!, scanned out of its source text byshaders_pipeline_includes(). Two structural properties make that a sound oracle rather than the same circularity under a new name:include_str!takes a literal path. It cannot be fed a runtime value, so the list provably does not route throughshader_corpus().The oracle gets its own reach control (
MIN_PIPELINE_INCLUDED_SHADERS = 11, so a scan bug cannot shrink it into a vacuous superset check), and that floor's number was measured with a different tool:grep -o 'include_str!("shaders/[^"]*"' crates/eqoxide-renderer/src/pipeline.rs | sort -u | wc -l→ 11 distinct names (12 occurrences;character.wgsltwice).[corrected, round 3] The oracle had the same disease one level down. Its scan took
include_str!hits frompipeline.rsincluding hits inside comments, while its docstring claimed the.wgslextension filter prevented phantoms. The reviewer reasoned that out; I measured both consequences, and the second was not predicted by either of us:// include_str!("shaders/ghost.wgsl")planted as a comment made the test fail withthe shader walk never returned ["ghost.wgsl"], an alarm about a file that never existed.included.len(), which is exactly what the floor counts. An orphan.wgslon disk, a comment naming it, and the scan silently dropping one genuinely-included shader measured 14 passed / 0 failed. The floor was padded by prose, and the superset check never looked at the dropped name — a name the scan loses is a name the scan cannot check.The scan now skips hits inside comments via
comment_spans—comments()refactored to byte spans so the file still has exactly one comment scanner, not two.the_include_scan_ignores_comments_but_not_codepins both directions against a synthetic source, so it keeps testing the rule afterpipeline.rschanges and fails without any plant.Both directions of the new assertion were planted and run at the call site, per the #818 lesson that a mutation inside the thing both sides read is not a test of the guard: truncating
shader_corpus()itself (RC-M2), and the reviewer's exact truncate-plus-decoy combination that used to measure green (RC-M1). Both now go RED.Honest limitations, stated in the code's own docstrings:
include_str!names — a planted decoy, or a.wgslused by a dev bin alone — is still invisible to this control. Nothing outside the disk knows such a file exists, so no oracle in this file can miss it loudly. This is a real remaining hole, narrowed rather than closed.no_shader_writes_a_numeric_palette_lengthis defeated by a type alias and by the whitespace formarray < mat4x4<f32>, 99 >. It is retained only because it covers address spaces the IR palette check deliberately ignores (non-uniform). Mutations 811-M4 and RC-M5 both show it staying green while the IR checks go red.#812 — the substitution was not word-boundary safe
src.replace("JOINT_CAP", "128")rewrote any identifier withJOINT_CAPas a prefix into a number, and rewrote every comment mentioning the cap into self-contradicting prose. The token is now${JOINT_CAP}.$,{and}cannot appear in a WGSL identifier, so the token cannot be a substring of one — that is a property of the delimiters, not of the current corpus, and the test asserts it against the WGSL identifier grammar rather than against today's shaders.[corrected] The delimiters do not make prose safe, and
pipeline.rssaid they did. Its doc claimed the token "touches no identifier and no prose". The identifier half is structural and holds; the prose half is simply false — the reviewer measured"// the ${JOINT_CAP} placeholder"→"// the 128 placeholder". Delimiting did not fix that and was never going to. The doc now says so explicitly, and separates the two claims by how each is true: identifiers by construction, prose by a guard.That guard also had a hole.
substitution_leaves_every_shader_comment_byte_identicalcovered line comments; a block comment spelling the token was rewritten silently. Rather than file that as a separate issue and widen the gap's lifetime, I closed it: acomments()extractor now handles//and nesting/* … */, the test compares every comment byte of both forms across the substitution, and the extractor itself carries a self-check (comment_extractor_finds_both_forms_including_nesting) so a broken extractor fails rather than passing over an empty set. The test also asserts its own premise — that WGSL has no string literals, so quote characters outside comments cannot smuggle token text past it.Measured after the fix (RC-M6): a block comment spelling the token in
character.wgslnow fails loudly withleft: "/* … the ${JOINT_CAP} placeholder. */"/right: "/* … the 128 placeholder. */".#813 —
(label, gender)double-reported one fileThe downgrade map keyed on
(label, gender), so an archetype with no_fvariant produced two entries for the one GLB both genders load. The key is now the file name actually loaded.The path resolution that decides which file that is has been lifted out of
ensure_character_modelinto a publicresolve_character_model_path, because #813 is about the (label, gender) → file relationship: a test carrying its own transcription of that rule would grade its own restatement. The tests drive the real resolver against a scratch asset directory.entry_count_always_equals_distinct_file_countis a property test over the powerset of_f-variant presence, not an example. [corrected] its docstring now states its quantifier honestly: one asset root, where distinct path and distinct base name coincide. They do not coincide in general.[corrected] "two loads of two files cannot produce one, because the key IS the file" was wrong. The key is
file_name(), andassets_pathispub, so two files sharing a base name under two roots collapse into one entry — the later load silently overwrites the earlier. I did not delete the sentence and move on; I split the paragraph by direction and pinned the false half with a test. One direction stays structural (two loads of one file cannot produce two entries, becausedowngrade_keyis a pure function of the path). The other is nowtwo_roots_with_the_same_basename_collide_into_one_entry, which asserts the collision happens and that the 140-joint rig under the other root is not reported at all. The doc also says why the base name is still the right key: this map is agent-facing over HTTP, and an absolute path is unstable across machines and is local detail this project does not publish.#814 — nothing was checking the cap's VALUE
Unifying the cap removed the only loud check on it. A too-small cap does not error — it reclassifies real rigs as
SkinFit::ExceedsCapand renders them unskinned. Silent downgrade, confident green.The bound was measured, not copied. I read
skins[].joints.len()out of the JSON chunk of every character GLB in the shipped asset set. Distribution: 127 (widest), 110, 109, 108, 106, 105, 104, 8.MAX_MEASURED_CHARACTER_RIG_JOINTS = 127; the docstring names the widest rig's file, the full distribution, and the date the corpus was measured, so a future reader can tell whether the number is stale. The reviewer independently re-derived it (136.glb, 49 skinned, max 127 onrace_pcfroglok.glb) and got the same figure.Enforced twice, deliberately:
const _: () = assert!(JOINT_CAP >= MAX_MEASURED_CHARACTER_RIG_JOINTS, ...)that fails the build, andMutation table — round 1
Every row was planted and run. Green control on the round-1 tree:
joint_cap_single_source13 passed / 0 failed,skin_cap_selection14 passed / 0 failed / 1 ignored. The pass counts in this table are round-1 counts; round 2 adds five and six tests respectively, so the same mutations now report higher totals.decoy_b.wgsl(alias JMat = mat4x4<f32>;+array<JMat, 99>in a uniform struct) dropped into the corpusevery_uniform_mat4_palette_in_the_corpus_is_exactly_joint_cap(Offenders: ["decoy_b.wgsl: palette length 99"]),palette_bearing_shaders_are_exactly_the_expected_three,raw_palette_shader_sources_do_not_parse_without_substitutionJointMatrices→BonePaletteRenamedin one shadervar<uniform>tovar<storage, read>palette_bearing_shaders_are_exactly_the_expected_three,every_palette_length_tracks_the_substituted_tokenalias JMat = mat4x4<f32>; struct JointMatrices { mats: array < JMat , 128 > };every_palette_length_tracks_the_substituted_token;no_shader_writes_a_numeric_palette_lengthstayed green — the documented gap, demonstrated.pipeline.rs+ all three shaders, plusconst JOINT_CAP_SCALE: f32 = 1.0;character_skinned.wgsl: WGSL failed to parse: expected identifier, found '128'the_joint_cap_token_cannot_occur_inside_an_identifier,substitution_cannot_alter_an_identifier_that_contains_the_word_joint_cap(left: "const 128: u32 = 1u;"),substitution_leaves_every_shader_comment_byte_identicalJOINT_CAP_SCALEidentifier added to the fixed tree alonedowngrade_keymutated back to the pre-#798 label key (strip_f)entry_count_always_equals_distinct_file_count,the_downgrade_key_is_the_file_name,two_genders_that_load_two_different_files_are_reported_separatelyresolve_character_model_pathmutated to drop the.exists()fallbackgender_1_falls_back_to_the_male_glb_when_no_f_variant_exists,two_genders_that_load_the_same_file_are_reported_onceJOINT_CAP = 126error[E0080]: evaluation panicked: JOINT_CAP is below the widest character rig that ships …const _: () = assert!JOINT_CAP = 126and the const assert short-circuited with `truePre-fix controls, run on a throwaway worktree at the base commit so the "measured green" claim in #811 is my own measurement and not a restatement of the issue:
decoy_b.wgslon the base tree → 9 passed / 0 failed. The reproduction ships unguarded, exactly as reported.JointMatricesrename on the base tree → RED 8/1 onjoint_palette_shaders_are_exactly_the_expected_three, with the palette still present and still correct. The old guard alarmed on the name, not the palette.Mutation table — round 2
Green control on the round-2 tree, unmutated:
joint_cap_single_source14 passed / 0 failed,skin_cap_selection15 passed / 0 failed / 1 ignored.zz_palette_data.wgsldecoy (alias form, length 99) andshader_corpus()truncated to the three expected filesthe shader walk never returned ["billboard.wgsl", "character.wgsl", "nav_debug.wgsl", "shadow_masked_instanced.wgsl", "sky.wgsl", "weather.wgsl", "zone.wgsl", "zone_instanced.wgsl"] … Walked: ["character_skinned.wgsl", "shadow.wgsl", "skin_probe.wgsl"]shader_corpus()truncated to three files, nothing plantedif out.len() >= 3 { break; }insideshaders_pipeline_includesthe pipeline.rs include_str! scan found 3 shader file name(s), fewer than the 11 measured to be there — the corpus ORACLE is truncated, so the superset check below would pass vacuously. Found: {"character.wgsl", "skin_probe.wgsl", "zone.wgsl"}Offenders: ["zz_palette_data.wgsl: palette length 99"]src/shaders/nested/decoy_c.wgslnested/decoy_c.wgsl: a uniform mat4 palette is 77 when the token is substituted with 91zz_fourth_palette.wgsl: a uniform mat4 palette is 128 when the token is substituted with 77every_uniform_mat4_palette_…_is_exactly_joint_capcorrectly stayed green (128 is right) andno_shader_writes_a_numeric_palette_lengthstayed green — the documented text-sweep gap, demonstrated again.${JOINT_CAP}appended tocharacter.wgslcharacter.wgsl:62: pipeline::wgsl rewrote a comment (eqoxide#812),left: "/* … ${JOINT_CAP} placeholder. */"/right: "/* … 128 placeholder. */"downgrade_keymutated to return the whole pathtwo_roots_with_the_same_basename_collide_into_one_entryfails withleft: 2 / right: 1pipeline.rsspellinginclude_str!("shaders/ghost.wgsl").wgslon disk + a comment naming it + the scan droppingzone.wgslfound 10 … fewer than the 11comment_spansmade to swallow the whole file as one commentfound 0 shader file name(s)Workspace test figures
Full workspace,
--locked --no-fail-fast, stdout and stderr captured separately. The branch column is the tree aftergit merge origin/main, so both columns describe commits that exist.7428809Finishedtestprofile in 9m 54stestprofile in 6m 07srunning N tests?headerstest result:lines^error|panicked|Killedin stderrDelta: +11 passed / +0 targets / +0 ignored / 0 failed. Accounted for exactly, by running the two affected binaries at the base commit:
joint_cap_single_source9 → 14 (+5),skin_cap_selection9 → 15 (+6), ignored 1 → 1. No test was deleted or newly ignored. The diff touchescrates/eqoxide-renderer/only, so no other target's count can have moved.Live run
#812 changes the text wgpu actually receives, and naga validating a string is not proof a driver accepts it, so this part was run live on a release build (verified the running process was the new binary via its
exelink and mtime before believing anything).Zoned in and observed 6 distinct skinned models load and render through the shared palette path — 109, 104, 104, 38, 29 and 15 joints. 0 lines matching
wgsl|shader|validation error|panicked|adapter error. 0 cap-downgrade / static-fallback lines. The only warnings present are pre-existing graphics-backend and UI ones unrelated to this change. Clean shutdown afterwards.NOT verified
pipeline::wgsl, whose body is byte-identical. That is a reasoned scope argument about which code moved, not a second live measurement, and it is marked as such.MAX_MEASURED_CHARACTER_RIG_JOINTSis stale and nothing in CI will notice — the const assert compares the cap against a number a human maintains.pipeline.rsis byte-identical tof1cd140, verified bymd5sumagainst a copy taken before planting.comment_spanswas written for WGSL, which has no string literals. I measured that a maximally-wrong comment scanner is loud (RC-M11); I did not write apipeline.rsstring containing/*and watch that specific case.include_str!names stays invisible. Every shader that ships today is named, so today the two sets coincide; that is a fact about the current tree, not a property.no_shader_writes_a_numeric_palette_lengthremains defeatable by a type alias and by the whitespace form. I did not fix it; I documented it and demonstrated the gap in 811-M4 and RC-M5.${JOINT_CAP}identifier claim is grammar-level, not corpus-level. I assert$/{/}are not WGSL identifier characters. I did not exhaustively fuzz the substituter against arbitrary shader text.resolve_character_model_pathis exercised against a scratch directory, not against the real asset tree.assets_pathis set once per renderer in the shipped flow, so I have no evidence the collision has ever occurred; the test proves it can, which is precisely the claim the old doc denied.