Merge pull request #10320 from TheMrZZ/fix/squid-damage-result

Preserve inherited squid damage acceptance result
This commit is contained in:
Kacper authored and GitHub committed 2026-11-06 09:18:42 +01:00
commit e5b2d2ea71
4 files changed
+370 -3

No files matched your search

+71 -2
View File
@@ -3613,7 +3613,10 @@ impl Mob {
if self.species() == Species::GlowSquid && self.ai.last_hurt_by.is_some() {
aquatic::hurt_server(self);
}
true
// Squid.hurtServer (also GlowSquid's result): parent health damage has
// happened, but without a last living attacker the subclass returns false.
!matches!(self.species(), Species::Squid | Species::GlowSquid)
|| self.ai.last_hurt_by.is_some()
}
/// `damage` by a source whose direct entity is `direct` (an entity type id), which `source`
/// cannot carry: `DamageSources.indirectMagic(potion or cloud, owner)`.
@@ -5586,6 +5589,70 @@ mod effect_refusal_tests {
);
}
fn check_squid_damage_result(id: &str) {
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
let types = MobTypes::load_for_server(&root).unwrap();
let mut attacker = mob(&types, "minecraft:cow");
attacker.id = 2;
attacker.position = Vec3::new(4.0, 64.0, 4.0);
assert!(attacker.alive());
let by_cow = Damage {
kind: DamageKind::Generic,
..Damage::mob(attacker.id, [4.0, 64.0, 4.0]).with_species(Species::Cow)
};
// An unrelated parent's generic acceptance remains true.
let mut cow = mob(&types, "minecraft:cow");
assert!(cow.damage(1.0, Damage::of(DamageKind::Generic)));
assert_eq!(cow.health, 9.0);
// A parent's existing rejection never reaches the subclass aftermath.
let mut protected = mob(&types, id);
protected.invulnerable_flag = true;
assert!(!protected.damage(1.0, by_cow));
assert_eq!(protected.health, 10.0);
if protected.species() == Species::GlowSquid {
assert_eq!(aquatic::dark_ticks(&protected), 0);
}
for no_ai in [false, true] {
for attributed in [true, false] {
let mut victim = mob(&types, id);
victim.no_ai = no_ai;
assert_eq!(victim.health, 10.0);
assert!(victim.ai.last_hurt_by.is_none());
let source = if attributed {
by_cow
} else {
Damage::of(DamageKind::Generic)
};
let accepted = victim.damage(1.0, source);
// Check the parent's state first: false must not roll back damage.
assert_eq!(victim.health, 9.0);
assert_eq!(
victim.ai.last_hurt_by,
attributed.then_some(Who::Mob(attacker.id))
);
if victim.species() == Species::GlowSquid {
assert_eq!(
aquatic::dark_ticks(&victim),
if attributed { 100 } else { 0 }
);
}
assert_eq!(accepted, attributed, "{id} NoAI={no_ai}");
}
}
}
#[test]
#[ignore = "needs prepared entity types in HORIZON_TEST_RESOURCES"]
fn glow_squid_damage_result_keeps_parent_health_side_effect() {
check_squid_damage_result("minecraft:glow_squid");
}
#[test]
#[ignore = "needs prepared entity types in HORIZON_TEST_RESOURCES"]
fn squid_damage_result_keeps_parent_health_side_effect() {
check_squid_damage_result("minecraft:squid");
}
/// Squid.hurtServer reports a hit only when a living entity dealt it, so
/// environmental damage leaves a glow squid glowing; a mob's blow dims it.
#[test]
@@ -5599,11 +5666,13 @@ mod effect_refusal_tests {
let types = MobTypes::load_for_server(&root).unwrap();
let mut burned = mob(&types, "minecraft:glow_squid");
burned.health = 10.0;
assert!(burned.damage(1.0, Damage::of(DamageKind::Campfire)));
assert!(!burned.damage(1.0, Damage::of(DamageKind::Campfire)));
assert_eq!(burned.health, 9.0);
assert_eq!(aquatic::dark_ticks(&burned), 0);
let mut struck = mob(&types, "minecraft:glow_squid");
struck.health = 10.0;
assert!(struck.damage(1.0, Damage::mob(2, [0.0, 64.0, 0.0])));
assert_eq!(struck.health, 9.0);
assert_eq!(aquatic::dark_ticks(&struck), 100);
}
}
+145
View File
@@ -0,0 +1,145 @@
# Squid's inherited damage acceptance result
Pinned Java26.3 `Squid.hurtServer` first calls its parent, then reports true only
with a non-null last living attacker. `GlowSquid.hurtServer` returns that same
result and sets dark ticks100 only when true. A false subclass result does not
undo the parent's health loss.
Current main already has the correct ink/dark guards, from #8864 (closed #8724).
They remain untouched. The sole gameplay change replaces the shared adapter's
final unconditional true with the inherited squid/glow acceptance condition.
Other species retain true after their accepted shared parent path. The old guard
test's erroneous environment-hit true assertion is corrected, adding health-loss
checks without changing that guard's implementation.
No parent-health/identity refactor, H176 timer, stale-attacker lifecycle, ink RNG,
particles, sound, packet, rendering or other-species behavior repair/claim.
## Declared actual Game/Java program
```
python3 tools/test_squid_damage_result_parity.py --binary /path/to/game --work-dir /scratch/run
python3 tools/test_squid_damage_result_parity.py --full-diagnostic --binary /path/to/game --work-dir /scratch/full
```
`--reference /scratch/old/vanilla.jsonl` requires byte-identical fresh Java bytes.
The existing command harness runs actual authoritative Game with its connected
owner and independently runs pinned Java in isolated fresh data. No new observer.
Six fresh, unique-UUID victims: GlowSquid NoAI0/1 crossed with generic/cow-attributed
hits; ordinary Squid NoAI1 is the independently Java-measured inherited control.
Each starts Health10 and receives one generic1 hit. A real healthy NoAI cow causes
the attributed hits. Actor UUID/type/Health/Pos/NoAI/NoGravity/Silent/Persistence,
loaded water/floor/glass geometry, owner identity/position and actual relative
clock0 guard admission. No world steps, reused victim, stale attacker or cooldown
history; the cow remains independently alive/unchanged after all actions.
Each hit has fresh result/success sentinels set to-99. Fresh Java FIRST measured
failure feedback/callback/stores: this program invokes false/0 and stores0 for
unattributed hits, not an assumed missing callback. Health becomes9 and glow dark
remains0. Cow-attributed hits give callback true/1 and both stores1, Health9 and
glow dark100. Every damage message/style/callback/store remains strictly compared.
Predeclared before native captures: **193** complete raw records. Only full bodies,
Motion vectors and absolute clock-start/end are diagnostic; all commands remain
traced. `--full-diagnostic` strictly compares all **221** records. No masks,
tolerances, feedback normalization or selection changes after capture. Complete
Motion records are retained without an RNG/physics parity claim.
Frozen source: `2266c4de14561146044c422e6c525b3ce52b0f79`.
| Program | Exact script SHA256 |
|---|---|
| Scoped193 | `25eec1729a070935d3b4bf075978bd1a44399df11a86e770a6387a8d862e4a7b` |
| Full221 | `548fe826c429f551e55012a16bdff078ffca9e5147e4c7e58b009674028c3dc1` |
Old current Game: **184/193**. Health9 and dark0 already match; historical dark100
failures are not recycled as this baseline. Exactly these three suffixes differ
for EACH of glow_ai0_generic, glow_ai1_generic and squid_generic (nine records):
| Suffix | Java expected | Old native actual |
|---|---|---|
| -hit | Invulnerable failure, callback false/0 | Applied1 damage success, true/1 |
| -result | 0 | 1 |
| -success | 0 | 1 |
Every attributed hit, actual health/dark/type/identity/geometry/clock control
passes old. Full old diagnostic: **196/221**, including full bodies and Motion.
## Reachable regressions and retained evidence
`glow_squid_damage_result_keeps_parent_health_side_effect` and its Squid twin
call actual `Mob.damage` on real prepared entity definitions. Both fail old at
returned true versus false AFTER asserting Health9, last attacker and dark0.
Both pass fixed. Fresh victims cover both NoAI modes, with prior positive cow-hit,
other-species Cow generic acceptance and parent-invulnerability controls. No
copied health algorithm, synthetic return value or clock setter. The existing
resource-backed glow guard test also passes with its corrected return expectation.
Programs, byte-identical scoped references, raw styled feedback/callbacks, full
bodies/Motion, transcripts, source/checkpoint hashes, pinned bytecode and immutable
old/fixed executables are retained under the owner's `.local/h177`. No game files
are published. Available file timestamps and logged stage durations are retained;
unobserved event stage times remain unknown.
## Fixed result and complete diagnostic residuals
Final actual Game: **193/193**, versus **184/193** old, with byte-identical scoped
script and fresh Java reference bytes. Both resource-backed Mob.damage tests and
the corrected existing dark-guard test pass. Full221: **196/221 → 205/221**. No
full221, whole-body, RNG/ink or raw-clock parity claim.
The full program is byte-identical across builds. Its fresh Java references differ
only at absolute clocks; non-clock records are identical. All Motion records in
this fixture match, but do not establish a general movement/RNG contract. All16
remaining full-record differences are listed below. In addition to extra default
attributes, the three attributed after-bodies omit Java's saved attacker fields.
This saved-attacker projection remains an unrelated diagnostic; runtime attributed
acceptance/dark100 are strictly covered. Complete raw bodies/styles/callbacks
remain mismatches in the full diagnostic. No normalization changes the counts.
| Exact label | Java expected | Fixed native actual |
|---|---|---|
| `clock-start` | `305 (old fresh reference: 265)` | `0` |
| `before-cow-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.20000000298023224d}]` |
| `glow_ai0_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `glow_ai0_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` |
| `glow_ai0_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `glow_ai0_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `glow_ai1_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `glow_ai1_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` |
| `glow_ai1_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `glow_ai1_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `squid_cow-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `squid_cow-after-body` | `attributes: []; last_hurt_by_mob: [I; 177, 0, 0, 100]; ticks_since_last_hurt_by_mob: 0` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]; both saved attacker fields absent` |
| `squid_generic-before-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `squid_generic-after-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.7d}]` |
| `after-cow-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.20000000298023224d}]` |
| `clock-end` | `305 (old fresh reference: 265)` | `0` |
Scoped reference SHA256: `6b3a5398408e2bcdac6d6d949fb5bed88e8cb6d25d96fc97b913321bfcb492ef`.
Full fixed fresh reference SHA256: `2856764df3a97dbf707dc6ad5417a205f34b53c3a473ce05062f0e42b9a04f5e`.
## Local readiness and source checkpoint
Final production/test patch: rustfmt, clippy with warnings denied, doc tests and
public-tree checks pass. `pr_ready.py --quick --base 2266c4de14561146044c422e6c525b3ce52b0f79`
runs 1887 passing Rust tests, one failure, 64 ignored. The failure is the existing
`drops_match_vanillas_motion_bit_for_bit` Z last-bit difference at yaw37.5,
pitch-21.25: actual4597448471913462162 versus expected4597448471913462163.
The own archived OLD executable reproduces exactly the same failure. No drop
physics edit. Both new ignored resource-backed tests were explicitly run and pass,
as does the corrected existing guard test.
Python: 696 tests, four failures and one skip. One is the installer signature
fixture offering x86_64 assets on an ARM64 host; three are optional-release
archive combinations hitting macOS Bash3.2 unset arrays. Narrow installer rerun
confirms its prerequisite. No installer/release repair. Broad parity is skipped
by `--quick`; the explicit actual Game/Java pairs above were run independently.
The readiness command exits nonzero for these unrelated failures; it is not green.
Latest actual main pull/checkpoint: `42e6d9cc498998b7bd9d5648360f72531ae0fd8e`.
The relevant Mob/species/damage-command and command-harness/Java-oracle files have
no delta from the frozen proof base. No rebase, main merge or combined-latest
runtime claim. Evidence-only documentation was completed after the required run;
whitespace/public-tree checks are repeated on the final staged files.
+2 -1
View File
@@ -1,6 +1,7 @@
Squid.hurtServer inline crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context after shared parent damage, require last living attacker for true; retain health loss and existing ink guard
GlowSquid.aiStep port crates/horizon-client/src/mobs/aquatic.rs::glow_squid_ai_step
GlowSquid.checkGlowSquidSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules S::GlowSquid arm: 33 blocks below sea level, dark, in water
GlowSquid.hurtServer port crates/horizon-client/src/mobs/aquatic.rs::hurt_server
GlowSquid.hurtServer inline crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context inherited Squid acceptance is returned after parent health effects; existing dim guard calls aquatic::hurt_server
WolfSoundVariants skip:data registry bootstrap of the wolf sound sets; sounds are in species.rs and finalize.rs
AbstractDragonPhaseInstance.getTurnSpeed inline crates/horizon-client/src/game/dragon.rs::Game::tick_dragon 0.7 / dist / rot turn for every phase but Landing; only applied while a fly target is set
DragonChargePlayerPhase.doServerTick inline crates/horizon-client/src/game/dragon.rs::Game::tick_phase Charging arm: 10-tick countdown once close; gap: minor: no collision re-target
1 GlowSquid.aiStep Squid.hurtServer port inline crates/horizon-client/src/mobs/aquatic.rs::glow_squid_ai_step crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context after shared parent damage, require last living attacker for true; retain health loss and existing ink guard
1 Squid.hurtServer inline crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context after shared parent damage, require last living attacker for true; retain health loss and existing ink guard
2 GlowSquid.aiStep GlowSquid.aiStep port port crates/horizon-client/src/mobs/aquatic.rs::glow_squid_ai_step crates/horizon-client/src/mobs/aquatic.rs::glow_squid_ai_step
3 GlowSquid.checkGlowSquidSpawnRules GlowSquid.checkGlowSquidSpawnRules port port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules S::GlowSquid arm: 33 blocks below sea level, dark, in water S::GlowSquid arm: 33 blocks below sea level, dark, in water
4 GlowSquid.hurtServer GlowSquid.hurtServer port inline crates/horizon-client/src/mobs/aquatic.rs::hurt_server crates/horizon-client/src/mobs.rs::Mob::damage_with_player_context inherited Squid acceptance is returned after parent health effects; existing dim guard calls aquatic::hurt_server
5 WolfSoundVariants WolfSoundVariants skip:data skip:data registry bootstrap of the wolf sound sets; sounds are in species.rs and finalize.rs registry bootstrap of the wolf sound sets; sounds are in species.rs and finalize.rs
6 AbstractDragonPhaseInstance.getTurnSpeed AbstractDragonPhaseInstance.getTurnSpeed inline inline crates/horizon-client/src/game/dragon.rs::Game::tick_dragon crates/horizon-client/src/game/dragon.rs::Game::tick_dragon 0.7 / dist / rot turn for every phase but Landing; only applied while a fly target is set 0.7 / dist / rot turn for every phase but Landing; only applied while a fly target is set
7 DragonChargePlayerPhase.doServerTick DragonChargePlayerPhase.doServerTick inline inline crates/horizon-client/src/game/dragon.rs::Game::tick_phase crates/horizon-client/src/game/dragon.rs::Game::tick_phase Charging arm: 10-tick countdown once close; gap: minor: no collision re-target Charging arm: 10-tick countdown once close; gap: minor: no collision re-target
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Fresh Squid/GlowSquid damage results through actual Game and pinned Java.
No world steps. Full bodies, Motion and absolute clocks remain executable raw
traces; --full-diagnostic strictly compares those too. No RNG/ink/timer claim.
"""
import hashlib
import json
from pathlib import Path
import sys
import test_command_exec_parity as driver
COW = '000000b1-0000-0000-0000-000000000064'
CASES = [('glow_ai0_cow', 'glow_squid', 0, True),
('glow_ai0_generic', 'glow_squid', 0, False),
('glow_ai1_cow', 'glow_squid', 1, True),
('glow_ai1_generic', 'glow_squid', 1, False),
('squid_cow', 'squid', 1, True), ('squid_generic', 'squid', 1, False)]
def program(full=False):
ops = []
def cmd(text, label, result=None, callback=None, admission=True, diagnostic=False):
op = {'op': 'cmd', 'cmd': text, 'label': label, 'admission': admission}
if result is not None:
op['expect_result'] = result
if callback is not None:
op['expect_callback'] = callback
if diagnostic and not full:
op.update(record=False, trace=True)
ops.append(op)
cmd('/tick freeze', 'freeze')
cmd('/gamerule spawn_mobs false', 'no-spawn')
cmd('/gamerule advance_time false', 'no-daylight')
cmd('/forceload add 0 0', 'force', 1)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256)
cmd('/fill 1 200 1 14 204 14 glass', 'walls-roof', 980)
cmd('/fill 2 200 2 13 203 13 water[level=0]', 'water', 576)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
cmd('/tp @s 8.5 205 14.5 0 0', 'owner-position', 1)
cmd('/gamemode creative @s', 'owner-mode')
cmd('/execute if block 8 204 14 glass', 'owner-support', 1)
cmd('/scoreboard objectives add h177 dummy', 'objective')
cmd('/execute store result score start h177 run time query gametime', 'clock-start', diagnostic=True)
cmd('/execute if entity @s[type=player,name=Player]', 'owner-type', 1)
for field, result in [('UUID', 4), ('Health', 20), ('playerGameType', 1), ('Pos', 3)]:
cmd(f'/data get entity @s {field}', 'owner-' + field, result)
cmd('/summon cow 12.5 201 10.5 {UUID:[I;177,0,0,100],Health:10f,NoAI:1b,NoGravity:1b,Silent:1b,PersistenceRequired:1b,CustomName:"H177_cow"}', 'cow-summon', 1)
def cow(label):
cmd(f'/execute as {COW} if entity @s[type=minecraft:cow]', label + '-cow-type', 1)
cmd('/execute if loaded 12 201 10', label + '-cow-loaded', 1)
cmd('/execute if block 12 201 10 water[level=0]', label + '-cow-water', 1)
for field, result in [('UUID', 4), ('Health', 10), ('Pos', 3), ('NoAI', 1), ('NoGravity', 1)]:
cmd(f'/data get entity {COW} {field}', label + '-cow-' + field, result)
cmd(f'/data get entity {COW}', label + '-cow-body', diagnostic=True)
cow('before')
for n, (case, kind, no_ai, attributed) in enumerate(CASES, 1):
x = n * 2
uuid = f'000000b1-0000-0000-0000-{n:012x}'
dark = ',DarkTicksRemaining:0' if kind == 'glow_squid' else ''
cmd(f'/execute if loaded {x} 201 4', case + '-loaded', 1)
cmd(f'/execute if block {x} 201 4 water[level=0]', case + '-water', 1)
cmd(f'/execute if block {x} 199 4 stone', case + '-floor', 1)
cmd(f'/summon {kind} {x + .5} 201 4.5 {{UUID:[I;177,0,0,{n}],Health:10f,NoAI:{no_ai}b,'
f'NoGravity:1b,Silent:1b,PersistenceRequired:1b,CustomName:"H177_{case}"{dark}}}', case + '-summon', 1)
for phase in ['before', 'after']:
if phase == 'after':
for holder in ['result', 'success']:
cmd(f'/scoreboard players set {holder} h177 -99', case + '-reset-' + holder, -99)
source = f' by {COW}' if attributed else ''
cmd(f'/execute store result score result h177 store success score success h177 '
f'run damage {uuid} 1 minecraft:generic{source}', case + '-hit', admission=attributed)
for holder in ['result', 'success']:
cmd(f'/scoreboard players get {holder} h177', case + '-' + holder, admission=attributed)
cmd(f'/execute as {uuid} if entity @s[type=minecraft:{kind}]', case + '-' + phase + '-type', 1)
for field, result in [('UUID', 4), ('Health', 10 if phase == 'before' else 9), ('Pos', 3),
('NoGravity', 1), ('Silent', 1), ('PersistenceRequired', 1)]:
cmd(f'/data get entity {uuid} {field}', case + '-' + phase + '-' + field, result)
cmd(f'/data get entity {uuid} NoAI', case + '-' + phase + '-NoAI',
callback=[True, 1] if no_ai else [False, 0])
if kind == 'glow_squid':
expected = 100 if phase == 'after' and attributed else 0
cmd(f'/data get entity {uuid} DarkTicksRemaining', case + '-' + phase + '-dark', expected)
cmd(f'/data get entity {uuid} Motion', case + '-' + phase + '-Motion', diagnostic=True)
cmd(f'/data get entity {uuid}', case + '-' + phase + '-body', diagnostic=True)
cow('after')
cmd('/execute store result score now h177 run time query gametime', 'clock-end', diagnostic=True)
cmd('/scoreboard players operation now h177 -= start h177', 'clock-delta-operation', 0)
cmd('/scoreboard players get now h177', 'clock-delta', 0)
return ops
def main():
full = '--full-diagnostic' in sys.argv
if full:
sys.argv.remove('--full-diagnostic')
reference = None
if '--reference' in sys.argv:
i = sys.argv.index('--reference')
reference = Path(sys.argv[i + 1]).resolve()
del sys.argv[i:i + 2]
if '--work-dir' not in sys.argv:
sys.argv += ['--work-dir', str(driver.lab.LOCAL / 'squid-damage-result-parity')]
build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve()
ops = program(full)
driver.corpus_module.groups = lambda: [('squid inherited damage result', ops)]
sys.argv.append('--allow-gaps')
driver.main()
recorded = [op for op in ops if op['op'] == 'cmd' and op.get('record', True)]
java = [json.loads(s) for s in (build / 'vanilla.jsonl').read_text().splitlines()]
for op, row in zip(recorded, java):
if 'expect_results' in op:
assert row['results'] == op['expect_results'], ('Java action admission', op, row)
rows_by_label = {op['label']: row for op, row in zip(recorded, java)}
# Measured on the fresh reference before either native capture: an error
# here invokes false/0, not an assumed missing callback or stale sentinel.
for case, _, _, attributed in CASES:
expected = [[True, 1]] if attributed else [[False, 0]]
assert rows_by_label[case + '-hit']['results'] == expected
for holder in ['result', 'success']:
assert rows_by_label[case + '-' + holder]['results'] == [[True, int(attributed)]]
if '--reference-only' in sys.argv:
return
source = reference or build / 'vanilla.jsonl'
if reference:
assert source.read_bytes() == (build / 'vanilla.jsonl').read_bytes(), 'fresh Java bytes changed'
expected = [json.loads(s) for s in source.read_text().splitlines()]
native = [json.loads(s) for s in (build / 'rust.jsonl').read_text().splitlines()]
assert len(expected) == len(native) == len(recorded)
differences = [{'label': op['label'], 'expected': a, 'actual': b}
for op, a, b in zip(recorded, expected, native) if a != b]
report = {'records': len(native), 'passed': len(native) - len(differences),
'reference_sha256': hashlib.sha256(source.read_bytes()).hexdigest(),
'comparison': 'complete raw records, no masks/tolerances', 'differences': differences}
(build / 'strict-report.json').write_text(json.dumps(report, indent=2) + '\n')
if not full:
for op, a, b in zip(recorded, expected, native):
if op['admission']:
assert a == b, ('actual fixture admission', op, a, b)
print(f'Strict raw records: {report["passed"]}/{report["records"]}')
if differences:
raise SystemExit(1)
if __name__ == '__main__':
main()