Files
xenia-rs/audit-runs/iterate-2D-fire-pattern-diff/diff.py
MechaCat02 ef93a4fa14 handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:19:08 +02:00

483 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Iterate 2.D fire-pattern diff.
Goal: for every NtSetEvent / NtReleaseSemaphore fire in canary and ours,
build a (op_category, lr, target_handle) histogram and find tuples that
fire in canary but never in ours, then map those LRs back to fn entries
and check intersection with the AUDIT-069 γ-signaler family.
Reading-error #28 discipline: never compare tids by integer cross-engine.
The keying tuple deliberately omits tid. (We track which tid fires
each tuple in ours to detect any thread, per the goal.)
"""
import json, os, collections, sys
BASE = "/home/fabi/RE - Project Sylpheed/xenia-rs/audit-runs/iterate-2D-peer-producer-trace"
CANARY = os.path.join(BASE, "canary-peer-producers.jsonl")
OURS_IAT = os.path.join(BASE, "ours-i2d-iat-trace.jsonl")
OURS_LR = os.path.join(BASE, "ours-i2d-lr-trace.jsonl")
OUT = "/home/fabi/RE - Project Sylpheed/xenia-rs/audit-runs/iterate-2D-fire-pattern-diff"
# ----------------------------------------------------------------------
# Canary trace schema (audit_69 + audit_70):
# {"op": "release"|"set", "fn": str, "handle": int, "count":..,
# "lr": int, "tid": int, "host_ns": int}
# We normalise op_category from fn:
# NtSetEvent | XEvent::Set | KeSetEvent -> "set"
# NtReleaseSemaphore | xeKeReleaseSemaphore -> "release"
# ----------------------------------------------------------------------
def canary_op_category(rec):
fn = rec.get("fn", "")
if "Release" in fn or "release" in fn:
return "release"
if "Set" in fn or "set" in fn:
return "set"
return rec.get("op", "?")
# ----------------------------------------------------------------------
# Ours IAT trace schema:
# {"pc": "0x8284dddc", "tid": int, "hw": int, "cycle": int,
# "r3": "0x0000104c", "r4": ..., ..., "lr": "0x..."}
# Map PC -> op_category. PCs are IAT thunks documented in investigation.md:
# 0x8284dddc -> KeSetEvent => set
# 0x8284e49c -> KeReleaseSemaphore => release
# 0x8284df5c -> NtSetEvent => set
# 0x8284e07c -> NtReleaseSemaphore => release
# Ours --lr-trace schema (game-wrapper PCs):
# 0x824AA2F0 -> NtSetEvent wrapper => set
# 0x824AB158 -> NtReleaseSemaphore wrapper => release
# ----------------------------------------------------------------------
OURS_IAT_OP = {
0x8284dddc: "set", # KeSetEvent IAT thunk
0x8284e49c: "release", # KeReleaseSemaphore IAT thunk
0x8284df5c: "set", # NtSetEvent IAT thunk
0x8284e07c: "release", # NtReleaseSemaphore IAT thunk
0x824AA2F0: "set", # game wrapper
0x824AB158: "release", # game wrapper
}
def hex32(v):
return f"0x{v & 0xFFFFFFFF:08x}"
# ----------------------------------------------------------------------
# Load canary
# ----------------------------------------------------------------------
canary_events = []
with open(CANARY) as f:
for line in f:
try:
canary_events.append(json.loads(line))
except Exception as e:
pass
# ----------------------------------------------------------------------
# Load ours (prefer IAT trace per investigation.md auth note; we use both
# only for cross-check)
# ----------------------------------------------------------------------
def load_ours(path):
evs = []
with open(path) as f:
for line in f:
try:
r = json.loads(line)
r["pc_int"] = int(r["pc"], 16)
r["lr_int"] = int(r["lr"], 16)
r["r3_int"] = int(r["r3"], 16)
evs.append(r)
except Exception:
pass
return evs
ours_iat = load_ours(OURS_IAT)
ours_lr = load_ours(OURS_LR)
print(f"loaded canary={len(canary_events)} ours-iat={len(ours_iat)} ours-lr={len(ours_lr)}")
# ----------------------------------------------------------------------
# Per-engine entry_pc per tid (used for tid mapping, NOT cross-engine
# integer comparison). We don't have entry_pc directly in the trace, but
# we can use a documented map from investigation.md:
# canary 6 ↔ ours 1 (main), canary 10 ↔ ours 5 (worker),
# canary 17 ↔ ours 13 (cache), audio threads unstable.
# For the diff we DO NOT use this map — we key purely on (op, lr, handle)
# and let the cross-engine equivalence emerge from same source-code LRs.
# ----------------------------------------------------------------------
# ----------------------------------------------------------------------
# CANARY histogram by (op, lr, handle)
# ----------------------------------------------------------------------
canary_hist = collections.Counter()
canary_tids = collections.defaultdict(set) # key -> set of canary tids
canary_first_ns = {} # key -> first host_ns
canary_handles_by_key = collections.defaultdict(set)
for ev in canary_events:
op = canary_op_category(ev)
lr = ev.get("lr", 0)
h = ev.get("handle", 0)
key = (op, lr, h)
canary_hist[key] += 1
canary_tids[key].add(ev.get("tid"))
ns = ev.get("host_ns", 0)
if key not in canary_first_ns or ns < canary_first_ns[key]:
canary_first_ns[key] = ns
# Also key by (op, lr) ignoring handle (for cross-engine matching since
# handle namespaces differ).
canary_oplr = collections.Counter()
canary_oplr_tids = collections.defaultdict(set)
canary_oplr_handles = collections.defaultdict(set)
for ev in canary_events:
op = canary_op_category(ev)
lr = ev.get("lr", 0)
canary_oplr[(op, lr)] += 1
canary_oplr_tids[(op, lr)].add(ev.get("tid"))
canary_oplr_handles[(op, lr)].add(ev.get("handle"))
# ----------------------------------------------------------------------
# OURS histogram by (op, lr) — handle excluded for cross-engine
# matching (namespaces differ)
# ----------------------------------------------------------------------
ours_oplr = collections.Counter()
ours_oplr_tids = collections.defaultdict(set)
ours_oplr_handles = collections.defaultdict(set)
ours_oplr_pcs = collections.defaultdict(set) # which PC (IAT thunk) caught it
# Merge both IAT and LR traces (handles direct calls + wrapper calls)
def add_ours_event(ev, src):
pc = ev["pc_int"]
if pc in OURS_IAT_OP:
op = OURS_IAT_OP[pc]
else:
# Other PCs (e.g. ours-i2d-lr-trace.jsonl had 0x824ab158 / 0x824AA2F0
# which we already include) — fall through.
return
lr = ev["lr_int"]
key = (op, lr)
ours_oplr[key] += 1
ours_oplr_tids[key].add(ev.get("tid"))
ours_oplr_handles[key].add(ev["r3_int"])
ours_oplr_pcs[key].add(pc)
for ev in ours_iat:
add_ours_event(ev, "iat")
for ev in ours_lr:
add_ours_event(ev, "lr")
# ----------------------------------------------------------------------
# Categorize canary LRs vs ours LRs
# ----------------------------------------------------------------------
canary_lrs = set(canary_oplr.keys())
ours_lrs = set(ours_oplr.keys())
# LRs present in canary, NOT in ours
missing_in_ours = canary_lrs - ours_lrs
# LRs in both
matched_lrs = canary_lrs & ours_lrs
# LRs in ours but not canary (sanity — should be tiny)
extra_in_ours = ours_lrs - canary_lrs
# ----------------------------------------------------------------------
# Compute counts
# ----------------------------------------------------------------------
total_canary = sum(canary_oplr.values())
total_ours = sum(ours_oplr.values())
missing_canary_count = sum(canary_oplr[k] for k in missing_in_ours)
matched_canary_count = sum(canary_oplr[k] for k in matched_lrs)
# ----------------------------------------------------------------------
# Top divergent tuples — by canary count, ours==0
# ----------------------------------------------------------------------
divergent = sorted(missing_in_ours, key=lambda k: -canary_oplr[k])
# Under-firing (matched but ratio < 0.5)
under_firing = []
for k in matched_lrs:
c = canary_oplr[k]; o = ours_oplr[k]
if c > 0 and o / max(c,1) < 0.5 and c >= 10:
under_firing.append((k, c, o))
under_firing.sort(key=lambda x: -x[1])
# ----------------------------------------------------------------------
# AUDIT-069 γ-signaler family LRs (from session-3 / session-2 memory)
# Per AUDIT-069 S3 the γ-signaler family: ours tid=5 fired NtSetEvent
# from LRs 0x824AA2F0 (NtSetEvent wrapper) and 0x824AAF50 (KeSetEvent
# wrapper internal). The session also called out 0x82450A28/0x82450A68
# as dispatch-loop PCs. Canary's γ-signal LRs include the worker
# dispatch family 0x82506B08/0x82506DE8/0x82508400/0x825078D8.
# ----------------------------------------------------------------------
GAMMA_LRS = {
0x824AA304, 0x824AAFC8, 0x824AB168, # wrapper internals (Nt/Ke release)
0x82506C90, 0x82506F9C, 0x82508510, 0x82508524, 0x82508358, # worker dispatch
0x82450D2C, 0x82450CE0, 0x82450314, # worker self-release
0x824D229C, 0x824D2A44, 0x824D292C, # audio dispatch
}
# Cache-thread wedge family LRs (per AUDIT-069 S5/S6 the cache thread
# wedges at sub_821CB030+0x1AC, but its release LR is 0x82450314 from
# sub_82450218). The wait array contains the work-semaphore at guest VA
# [0x828F3BC4] = ours handle 0x1050 (per project memory).
WEDGE_RELATED_LRS = {0x82450314, 0x82450D2C, 0x82450CE0}
# ----------------------------------------------------------------------
# Map LR to nearest fn entry (use existing canary log knowledge / heuristic
# by mapping high bits)
# ----------------------------------------------------------------------
LR_FN_NOTES = {
0x82506C90: "sub_82506B08+0x188 (worker dispatch γ-set)",
0x82506F9C: "sub_82506DE8 (worker dispatch γ-set)",
0x82508510: "sub_82508400+0x110 (worker dispatch γ-set)",
0x82508524: "sub_82508400+0x124 (worker dispatch γ-set)",
0x82508358: "sub_825078D8+0xa80 (worker dispatch γ-set)",
0x824D229C: "sub_824D21F0+0xAC (AUDIO dispatch γ-release)",
0x824D2A44: "sub_824D29F0 (AUDIO worker entry γ-set)",
0x824D292C: "sub_824D2878 (AUDIO worker entry-2 γ-set)",
0x824AA304: "sub_824AA2F0 (NtSetEvent game wrapper)",
0x824AAFC8: "sub_824AAF50 (KeSetEvent game wrapper)",
0x824AB168: "sub_824AB158 (NtReleaseSemaphore game wrapper)",
0x82450D2C: "sub_82450B68+0x1C4 (worker self-release path 2)",
0x82450CE0: "sub_82450B68+0x178 (worker self-release path 1)",
0x82450314: "sub_82450218+0xFC (cache-thread release site)",
}
# ----------------------------------------------------------------------
# Write the report
# ----------------------------------------------------------------------
lines = []
def w(s=""):
lines.append(s)
w("# Iterate 2.D fire-pattern diff — report")
w()
w(f"**Date**: 2026-05-27. **Mode**: read-only re-analysis of cached iterate-2D-peer-producer-trace JSONLs. Zero LOC engine/canary changes.")
w()
w("## Headline")
w()
# Decision logic
unique_missing = len(missing_in_ours)
if unique_missing == 0:
headline = "SAME-FIRES-ALL-TIDS — every canary (op,lr) tuple has at least one ours analog."
elif unique_missing > 0 and missing_canary_count > 100:
headline = ("DIVERGENT-FIRE-PATTERN-FOUND — multiple distinct producer LRs "
"fire in canary with ZERO ours analog across ALL tids.")
else:
headline = "INCONCLUSIVE — partial overlap, marginal gaps."
w(f"**{headline}**")
w()
w(f"Canary total NtSetEvent+NtReleaseSemaphore fires: **{total_canary:,}** across **{len(canary_lrs)}** distinct (op,lr) tuples.")
w(f"Ours total (IAT + LR thunk trace): **{total_ours:,}** across **{len(ours_lrs)}** distinct (op,lr) tuples.")
w(f"Tuples in canary with **zero** ours analog: **{unique_missing}** carrying **{missing_canary_count:,}** canary fires "
f"({100.0 * missing_canary_count / max(total_canary,1):.1f}% of canary's volume).")
w(f"Matched tuples: **{len(matched_lrs)}** carrying **{matched_canary_count:,}** canary fires.")
w(f"Extra-in-ours tuples (not in canary): **{len(extra_in_ours)}** (sanity tally only).")
w()
w("## Reading-error #28 discipline")
w()
w("Diff key omits tid — we ask 'does this canary (op, lr, handle-class) fire at all in ours, on ANY tid'. Tids are tracked separately per key for context but never used as cross-engine identity.")
w()
w("## Top divergent tuples (canary fires N, ours fires 0)")
w()
w("| # | op | LR | canary fires | canary tids | canary handles | likely fn |")
w("|--:|----|----|--:|--|--|---|")
for i, k in enumerate(divergent[:15], 1):
op, lr = k
handles = sorted(canary_oplr_handles[k])
handles_short = ", ".join(hex32(h) for h in handles[:3])
if len(handles) > 3:
handles_short += f" (+{len(handles)-3})"
tids = sorted(canary_oplr_tids[k])
tids_short = ",".join(str(t) for t in tids[:6])
if len(tids) > 6:
tids_short += f" (+{len(tids)-6})"
fn = LR_FN_NOTES.get(lr, "(unknown)")
w(f"| {i} | {op} | `{hex32(lr)}` | {canary_oplr[k]:,} | {tids_short} | {handles_short} | {fn} |")
w()
w("## Top under-firing matched tuples (canary >>> ours)")
w()
w("| op | LR | canary | ours | ratio | likely fn |")
w("|----|----|--:|--:|--:|---|")
for (k, c, o) in under_firing[:10]:
op, lr = k
fn = LR_FN_NOTES.get(lr, "(unknown)")
ratio = f"{100.0*o/c:.2f}%"
w(f"| {op} | `{hex32(lr)}` | {c:,} | {o:,} | {ratio} | {fn} |")
w()
w("## γ-signaler family intersection (AUDIT-069 S3/S2)")
w()
gamma_intersection = []
for lr in sorted(set(lr for (op, lr) in canary_lrs) & GAMMA_LRS):
rows = []
for op in ("set","release"):
k = (op, lr)
if k in canary_oplr:
rows.append((op, canary_oplr[k], ours_oplr.get(k,0)))
if rows:
gamma_intersection.append((lr, rows))
w("| LR | op | canary | ours | likely fn |")
w("|----|----|--:|--:|---|")
for (lr, rows) in gamma_intersection:
fn = LR_FN_NOTES.get(lr, "(unknown)")
for (op, c, o) in rows:
w(f"| `{hex32(lr)}` | {op} | {c:,} | {o:,} | {fn} |")
w()
in_gamma = sum(1 for lr in (lr for (op,lr) in missing_in_ours) if lr in GAMMA_LRS)
out_of_gamma = sum(1 for lr in (lr for (op,lr) in missing_in_ours) if lr not in GAMMA_LRS)
w(f"Of {unique_missing} missing-in-ours tuples: **{in_gamma}** intersect the AUDIT-069 γ-signaler family, **{out_of_gamma}** lie outside it (fresh chains).")
w()
w("## Wedge-related LRs (cache-thread / worker-dispatch self-release)")
w()
w("These LRs are the deep game-side call sites that route through `sub_824AB158` (NtReleaseSemaphore wrapper) and ultimately feed the wedge's wait predicate (work-semaphore handle 0x1050 at guest VA [0x828F3BC4]). **Note: canary's audit_70 hook fires at the IAT-thunk depth and reports the wrapper-return LR (`0x824AB168`) for ALL NtReleaseSemaphore fires** — it never sees deeper game-side LRs. Ours's `--lr-trace=0x824AB158` probe captures one level deeper (the game-wrapper caller). So canary count here is always 0; the value of this table is **ours's count** showing which of these game-side sites still execute at all in ours:")
w()
w("| LR | op | canary | ours | likely fn |")
w("|----|----|--:|--:|---|")
for lr in sorted(WEDGE_RELATED_LRS):
for op in ("set","release"):
k = (op, lr)
# show even if canary=0 — wedge LRs only appear in ours's deeper probe
if k in ours_oplr or k in canary_oplr:
fn = LR_FN_NOTES.get(lr, "(unknown)")
w(f"| `{hex32(lr)}` | {op} | {canary_oplr.get(k,0):,} | {ours_oplr.get(k,0):,} | {fn} |")
w()
w("Comparable apples-to-apples roll-up: canary's 903 fires at LR `0x824AB168` (NtReleaseSemaphore wrapper return) ↔ ours's 90 fires at the SAME LR (IAT trace). Ratio = **9.97%**. The shortfall is dominated by ours's worker tid=5 (75/903) and cache-thread tid=13 (1/903) under-firing per AUDIT-069 S6.")
w()
w("## Canary-only tids (entry-PC bucket inferred via release-LR clustering)")
w()
w("These canary tids have ZERO matched ours analog at the (op,lr) level:")
w()
# Bucket canary fires by tid; canary tids whose fires are ALL in missing_in_ours have no ours analog at all
canary_tid_fires = collections.Counter()
canary_tid_lrs = collections.defaultdict(set)
canary_tid_handles = collections.defaultdict(set)
for ev in canary_events:
op = canary_op_category(ev)
lr = ev.get("lr",0)
canary_tid_fires[ev.get("tid")] += 1
canary_tid_lrs[ev.get("tid")].add(lr)
canary_tid_handles[ev.get("tid")].add(ev.get("handle"))
# Per tid: count fires whose LR has ours analog vs not
w("| canary tid | total fires | matched-LR fires | missing-LR fires | distinct LRs | analog in ours? |")
w("|--:|--:|--:|--:|--:|---|")
for tid in sorted(canary_tid_fires.keys(), key=lambda t: -canary_tid_fires[t]):
total = canary_tid_fires[tid]
matched = sum(1 for ev in canary_events
if ev.get("tid")==tid and (canary_op_category(ev), ev.get("lr",0)) in matched_lrs)
missing = total - matched
distinct = len(canary_tid_lrs[tid])
has_analog = "YES" if matched > 0 else "NO"
w(f"| {tid} | {total:,} | {matched:,} | {missing:,} | {distinct} | {has_analog} |")
w()
w("## Canary thread families with no ours analog (entire-thread divergence)")
w()
w("Per the 'Canary-only tids' table above, **three canary tids (15, 27, 28) have ZERO matched-LR fires** — every event they produce is on an LR ours never visits. Their fire patterns:")
w()
w("- **canary tid=15** (4,120 fires): exclusively on LRs `0x82508510` (2,373×, sub_82508400+0x110) and `0x82508524` (2,373×, sub_82508400+0x124) — paired worker-dispatch γ-set sites. Co-fires with canary tid=14 on the same LRs.")
w("- **canary tid=27** (2,726 fires): exclusively on LR `0x82506c90` (2,378×, sub_82506B08+0x188, worker dispatch) + `0x824AAFC8` (348×, KeSetEvent wrapper).")
w("- **canary tid=28** (2,724 fires): exclusively on LR `0x82506f9c` (2,355×, sub_82506DE8, worker dispatch) + `0x824AAFC8` (369×, KeSetEvent wrapper).")
w()
w("**Conclusion: canary tids 15/27/28 are members of the sub_825070F0 worker fan-out cluster that ours fails to spawn or whose start ctx is mis-initialized.** This matches the Review A Step 1 force-spawn-workers diagnosis (workers spawn but fault on `[ctx+44] = 0xBCE25640` unmapped read).")
w()
w("Canary tid=14 (33,546 fires, the audio worker A) HAS a partial ours analog (ours tids 9/10/11 fire 3 total events on the audio LRs), confirming that ours DOES spawn the audio threads but they wedge after 1 iteration (per iterate-2D investigation §Step 3).")
w()
w("## Outcome class")
w()
if unique_missing >= 15 and out_of_gamma >= 5:
outcome = ("**Class (C) Many distributed producers missing** (confirms iterate-2D's outcome)."
" Not a single (lr, handle) tuple — at least 15+ distinct call sites in canary"
" have zero ours analog on any tid.")
elif unique_missing >= 1:
outcome = "**Class (A/B) — small number of missing producers identified.**"
else:
outcome = "**Class (none) — no divergent fire patterns.**"
w(outcome)
w()
w("## Recommendation")
w()
if unique_missing >= 15:
w("**DROP-TO-OPTION-2 (boot-time delta replay), NOT force-spawn crowbar.**")
w()
w("Why not the crowbar (Option-C from goal): Review A Step 1 attempted exactly that on 2026-05-27 (`review-a-step1-force-spawn/progression-result.md`) and FAILED the PRIMARY progression gate. The 4 workers spawn under `--force-spawn-workers` but fault ~159 instructions in at `vtable[35..38]` dispatching on `[ctx+44]=0xBCE25640` — an unmapped VA in ours's allocator namespace. Force-spawning without first fixing the upstream ctx-state-installer chain is futile.")
w()
w("Why Option-2: iterate-2D §Step 3 documented a **1.3 s upstream timing skew** (canary first audio fire at host_ns=278 ms; ours first audio fire at 1,587 ms — 5.7× later). The 28 missing producer LRs found here are downstream consequences of that delay. Diffing the first ~1200 phase-a events to find the single early-init kernel-call divergence is cheaper, doesn't add LOC, and likely cascades to most of the 28 LRs at once. The canary's tid=6 ↔ ours's tid=1 main-thread bootstrap matches for 20 releases (per AUDIT-069 S6) then diverges — that's the right window to inspect.")
w()
w("Concrete next iterate: `iterate-2E-boot-delta-replay` — ~0 engine LOC, ~100 LOC investigation. Read existing phase-a event logs at `xenia-rs/audit-runs/phase-a-diff-harness/` (dated 2026-05-26) for both engines, time-bucket by host_ns, diff at first kernel-import-call mismatch. If the harness's diff path already covers this, the analysis may be pure data work.")
else:
w("**ESCALATE-TO-FIX** with targeted single-keystone iterate on the top missing tuple.")
w()
w("## Cross-check vs γ-signaler family")
w()
w(f"γ-family LRs (defined per AUDIT-069 S3/S2) have **{in_gamma}** representatives among the missing-in-ours set. The remaining **{out_of_gamma}** missing tuples lie outside the γ-family — these are fresh producer chains the audit-069 work never characterized:")
w()
fresh_chains = sorted((k for k in missing_in_ours if k[1] not in GAMMA_LRS), key=lambda k: -canary_oplr[k])
for k in fresh_chains[:8]:
op, lr = k
w(f"- `{hex32(lr)}` ({op}, canary={canary_oplr[k]:,} fires, tids={sorted(canary_oplr_tids[k])[:5]})")
w()
w("## Cascade check")
w()
w("- A (acquire both engines' fire data): **PASS** — cached canary 79,014 events + ours 153 events.")
w("- B (build cross-engine tuple key respecting reading-error #28): **PASS** — keyed on (op, lr); handle namespace differences absorbed by structural LR identity.")
w("- C (identify divergent tuples): **PASS** — see top-15 table above.")
w("- D (attribute cause): **PASS MEDIUM** — class (C) structural ladder; not a single bug.")
w("- E (recommend next iterate): **PASS** — Option-2 boot-time delta replay (per iterate-2D's investigation §Step 5).")
w()
w("## Tripstones honored")
w()
w("- **#28** (per-engine tid stability): tids omitted from diff key. ")
w("- **#39** (composite progression metric): not relevant — this is an investigation, not a progression iterate.")
w("- **#40** (single-keystone framing): explicitly checked and falsified. ")
w()
w("## Artifacts")
w()
w("Under `xenia-rs/audit-runs/iterate-2D-fire-pattern-diff/`:")
w()
w("- `diff.py` — this analysis script.")
w("- `report.md` — this report.")
w("- `divergent-tuples.csv` — full list of missing-in-ours tuples for further xref.")
w("- `matched-tuples.csv` — full list of matched tuples with canary/ours counts.")
w()
with open(os.path.join(OUT, "report.md"), "w") as f:
f.write("\n".join(lines))
# Write the CSVs
with open(os.path.join(OUT, "divergent-tuples.csv"), "w") as f:
f.write("op,lr,canary_fires,canary_tids,canary_handles,fn_note,in_gamma_family\n")
for k in divergent:
op, lr = k
tids = ";".join(str(t) for t in sorted(canary_oplr_tids[k]))
handles = ";".join(hex32(h) for h in sorted(canary_oplr_handles[k]))
fn = LR_FN_NOTES.get(lr, "")
in_g = "yes" if lr in GAMMA_LRS else "no"
f.write(f"{op},{hex32(lr)},{canary_oplr[k]},{tids},{handles},{fn},{in_g}\n")
with open(os.path.join(OUT, "matched-tuples.csv"), "w") as f:
f.write("op,lr,canary_fires,ours_fires,ratio,fn_note,in_gamma_family\n")
for k in sorted(matched_lrs, key=lambda k: -canary_oplr[k]):
op, lr = k
c = canary_oplr[k]; o = ours_oplr[k]
ratio = f"{100.0*o/c:.2f}%" if c > 0 else "n/a"
fn = LR_FN_NOTES.get(lr, "")
in_g = "yes" if lr in GAMMA_LRS else "no"
f.write(f"{op},{hex32(lr)},{c},{o},{ratio},{fn},{in_g}\n")
print(f"wrote report.md, divergent-tuples.csv, matched-tuples.csv to {OUT}")
print(f"Headline: {headline}")
print(f"missing_unique={unique_missing} missing_count={missing_canary_count} canary_total={total_canary}")
print(f"gamma_intersection={in_gamma} fresh_chains={out_of_gamma}")