diff --git a/crates/gc-extract/tests/disc.rs b/crates/gc-extract/tests/disc.rs index 4a69f0a..80a9f8b 100644 --- a/crates/gc-extract/tests/disc.rs +++ b/crates/gc-extract/tests/disc.rs @@ -3232,9 +3232,14 @@ fn chunk_records_start_16_byte_aligned() { /// boundaries measured against each record's successor, and ragged chunks /// skipped, only 2 types show a varying class-ID slot. /// -/// What survives is narrow and is asserted as such: 2 chunk types show a slot -/// that varies in the manner of a class ID, and exactly 1 of those still clears -/// 100x enrichment over chance. The +/// **Nothing survives, and the flaw was present from the start.** The +/// discriminator the original version lacked: a slot that really carries moby +/// class IDs must span the ID space, so some values land ABOVE a level's moby +/// count. The slot that scored 305x has 9 distinct values -- +/// 1, 3, 4, 5, 10, 11, 12, 13, 15 -- and **none** above the count, because all +/// are under ~215. Every slot that does span the ID space scores 10x to 64x. The +/// original "5 to 42 distinct values at 153x to 289x" had exactly the artifact's +/// shape, so the record-boundary fix was never what made it wrong. The /// `distinct >= 4` filter is still load-bearing -- without it the top slots are /// constants that read as 305x because any value below the moby count counts as /// a hit. See `docs/formats/core-data-regions.md`. @@ -3259,6 +3264,10 @@ fn moby_class_ids_sit_in_one_slot_per_chunk_type() { // (type, slot) -> (nonzero, hits, expected, distinct) let mut stat: BTreeMap<(u32, usize), (u64, u64, f64, usize)> = BTreeMap::new(); + // (type, slot) -> how many of that slot's values land above the moby count. + // Keyed per SLOT, not per type: a type with one artifact slot and several + // ordinary slots must not have the ordinary ones vouch for it. + let mut above: BTreeMap<(u32, usize), usize> = BTreeMap::new(); let mut seen: BTreeMap<(u32, usize), BTreeSet> = BTreeMap::new(); for n in 0..27 { @@ -3294,6 +3303,8 @@ fn moby_class_ids_sit_in_one_slot_per_chunk_type() { e.0 += 1; if (v as u64) < nm { e.1 += 1 + } else { + *above.entry((t, k)).or_default() += 1 } } } @@ -3328,16 +3339,37 @@ fn moby_class_ids_sit_in_one_slot_per_chunk_type() { // boundaries fixed, and this test skipping ragged chunks, only 2 types show // a varying class-ID slot -- so "one class-ID slot per chunk type" was a // measurement of the parser's over-read, not of the disc. + // The discriminator the original version lacked. A slot that really carries + // moby class IDs spans the ID space, so some of its values must land ABOVE a + // level's moby count. The artifact does the opposite: every one of its values + // sits below the count, so every value scores as a hit. + // + // type 0xd76cc453 slot 6 305x 9 distinct values 0 above the count + // type 0xf2ef4275 slot 34 64x 9 distinct values 341 above the count + // type 0xf2ef4275 slot 7 46x 16 distinct values 412 above the count + // + // The 305x slot's values are 1, 3, 4, 5, 10, 11, 12, 13, 15 -- all under a + // level's ~215 moby classes, giving 65536/215 = 305x. Nine small numbers is + // not a class-ID array, and every slot that does span the ID space scores in + // single digits to tens. Nothing reaches 100x while also exceeding the count. + let spanning: Vec<_> = best + .iter() + .filter(|(&t, &(k, r, _))| r >= 100.0 && above.get(&(t, k)).copied().unwrap_or(0) > 0) + .collect(); let strong: Vec<_> = best.values().filter(|(_, r, _)| *r >= 100.0).collect(); - assert!( - !strong.is_empty(), - "no type reaches 100x enrichment -- was 20 before the record-boundary fix" + eprintln!( + "chunk class-ID slots: {} types; {} at >=100x, of which {} also have \ + values above the moby count ({} is the small-value-set artifact)", + best.len(), + strong.len(), + spanning.len(), + strong.len() - spanning.len() ); - assert_eq!(strong.len(), 1, "strong-enrichment type count moved"); assert!( - best.len() >= 2, - "only {} chunk types carry a varying class-ID slot (was >= 20)", - best.len() + spanning.is_empty(), + "{} slots clear 100x while spanning the ID space -- there may be a real \ + class-ID array after all", + spanning.len() ); assert!( best.len() < 20, diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index 80aa61e..51f5656 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -986,13 +986,40 @@ With record boundaries measured against each record's successor, and ragged chunks skipped, what survives is: chunk types with a varying class-ID-like slot 2 (was >= 20) - of those, clearing 100x enrichment 1 (was >= 20) + of those, ALSO spanning the ID space 0 So the general claim -- one class-ID slot per chunk type, across the container -- -does not hold and is withdrawn. One chunk type still shows a slot that varies in -the manner of a class ID at 100x enrichment over chance. That is asserted, and -the test also asserts the count has *not* returned to 20, so the retraction -cannot quietly lapse back into the claim. +does not hold and is withdrawn. + +### And the flaw was there from the start + +The discriminator the original version lacked is simple: **a slot that really +carries moby class IDs must span the ID space**, so some of its values land above +a level's moby count. The artifact does the opposite. + + type slot enrichment distinct values ABOVE the moby count + 0xd76cc453 6 305x 9 0 + 0xf2ef4275 34 64x 9 341 + 0xf2ef4275 7 46x 16 412 + 0xf2ef4275 14 34x 16 432 + 0xd76cc453 9 32x 28 702 + +The 305x slot's nine values are `1, 3, 4, 5, 10, 11, 12, 13, 15` -- all under a +level's ~215 moby classes, so every one scores as a hit and 65536/215 = 305x. +Every slot that actually spans the ID space scores 10x to 64x, which is chance. + +So the original claim's own numbers gave it away and it was not read that way: +**"5 to 42 distinct values at 153x to 289x"** is exactly the artifact's shape -- +a handful of small values, none above the count. The record-boundary fix was +never what made this claim wrong. It was wrong when written, and the retraction +is complete rather than partial. + +The test now separates the two: it counts slots clearing 100x (currently 1) and +slots that clear 100x *while also* having values above the moby count (currently +0). The second must stay empty; the first is expected to be the artifact. The +count above the moby count is tracked per **slot**, not per type, because a type +with one artifact slot and several ordinary slots must not have the ordinary +ones vouch for it. The `distinct >= 4` filter this section introduced is still load-bearing and is unaffected: without it the top slots are constants that read as 305x, because any