namespace AcDream.App.Tests.Rendering.Walk;
///
/// S3 chunk 1 (§11.2 B4): the offline transcript signature diff. Given an
/// acdream transcript (printed by the production
/// ACDREAM_DUMP_WALK_TRANSCRIPT=1 emitter — WalkTranscriptDump)
/// and a retail capture (or any two -parsed
/// transcripts), reports the FIRST divergent event and its position, frame
/// by frame, at the DI/DC/BLD/LS/LC/SC/EC/OC level. A test-side comparison
/// utility only — no runner, no dashboard, no new tool under
/// tools/ ([[feedback-evidence-infrastructure-sink]]); the S3 review
/// and G3 consume its report by hand.
///
public static class WalkTranscriptSignatureDiff
{
/// The first point two transcripts disagree, or
/// when every frame both sides share matches
/// exactly. is the retail-side frame's
/// own number (matching the "F n" line);
/// is the zero-based index into that frame's DI/DC/BLD/LS/LC/SC/EC/OC
/// event signature. "<end of frame>"/"<missing
/// frame>" mark a length mismatch rather than a content
/// mismatch.
public readonly record struct Divergence(
int FrameNumber, int Position, string Expected, string Actual);
/// Diffs two raw transcripts (e.g. one file read as lines each)
/// — the file-to-file form B4 names directly.
public static Divergence? FirstDivergence(
IEnumerable expectedLines, IEnumerable actualLines)
=> FirstDivergence(
WalkOracleTrace.Parse(expectedLines), WalkOracleTrace.Parse(actualLines));
/// Diffs two already-parsed frame lists — the form the
/// synthetic self-test below uses directly, without a round trip
/// through a temp file.
public static Divergence? FirstDivergence(
IReadOnlyList expected, IReadOnlyList actual)
{
int frameCount = Math.Min(expected.Count, actual.Count);
for (int f = 0; f < frameCount; f++)
{
IReadOnlyList e = Signature(expected[f]);
IReadOnlyList a = Signature(actual[f]);
int shared = Math.Min(e.Count, a.Count);
for (int i = 0; i < shared; i++)
{
if (!string.Equals(e[i], a[i], StringComparison.Ordinal))
return new Divergence(expected[f].Number, i, e[i], a[i]);
}
if (e.Count != a.Count)
{
return new Divergence(
expected[f].Number,
shared,
shared < e.Count ? e[shared] : "",
shared < a.Count ? a[shared] : "");
}
}
if (expected.Count != actual.Count)
{
int frameNumber = frameCount < expected.Count
? expected[frameCount].Number
: actual[frameCount].Number;
return new Divergence(
frameNumber,
0,
frameCount < expected.Count ? "" : "",
frameCount < actual.Count ? "" : "");
}
return null;
}
/// One frame's DI/DC/BLD/LS/LC/SC/EC/OC turns, in transcript
/// order — the full eight-kind vocabulary B4 names (unlike
/// ,
/// which deliberately stays at the pre-S3-chunk-3 four-kind level for
/// the B3 still-fixture rows).
private static IReadOnlyList Signature(WalkOracleFrame frame)
=> frame.Events.Select(e => e.Kind switch
{
WalkOracleEventKind.Landscape => "LS",
WalkOracleEventKind.Building => $"BLD:{e.CellId!.Value:x8}",
WalkOracleEventKind.DrawInside => $"DI:{e.CellId!.Value:x8}",
WalkOracleEventKind.DrawCells =>
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
WalkOracleEventKind.LandCell => $"LC:{e.CellId!.Value:x8}",
WalkOracleEventKind.SortCell => $"SC:{e.CellId!.Value:x8}",
WalkOracleEventKind.EnvCellShell => $"EC:{e.CellId!.Value:x8}",
WalkOracleEventKind.ObjectCellTurn => $"OC:{e.CellId!.Value:x8}",
_ => "?",
}).ToList();
}