EVIDENCE / ORACLE / RUNTIME / CERTIFICATION

Reconstruction Engineering

Reconstructing abandoned software means turning fragmented evidence into a measurable system, then proving where the new runtime matches the original, where it diverges, and why.

Method
Evidence-led
Applies to
Abomination · Rebel Moon Rising
Proof
Exact-hash gates
Signed by
Machines, then people

Two projects in this portfolio reconstruct software that nobody maintains: a 1999 tactical game and a 1997 first-person shooter. They target different engines, decode different formats and answer to different constraints. They share a method, and this page is that method written down, because a method that only exists as habit cannot be criticized, transferred or improved.

The pipeline

Read it as a set of one-way boundaries rather than as a schedule. Evidence never becomes editable. Extraction never invents. Classification never upgrades itself. The simulation never renders. Certification never signs the human gate. Each arrow is a place where something is allowed to lose information and forbidden to gain it.

1 — Immutable evidence and inventory

The first artefact is not a decoded file. It is a read-only copy of the original media plus a hash inventory of every byte in it. Everything downstream cites that inventory, which is what allows a claim made six months later to be checked against the same bytes rather than against a directory somebody has been editing. Where the original media is proprietary, it stays on the machine that holds it: the reconstruction ships tools and a runtime, never somebody else's assets.

2 — Static extraction at a checked boundary

A parser for a format nobody documented is an argument about what the bytes mean, and an argument should fail loudly when it is wrong. Every read is bounds-checked, every refusal names the offset it refused at, and no accessor exists that skips the check. A parser that silently pads or guesses through corruption does not produce partial data; it produces confident fiction.

PUBLIC ARCHITECTURE CONTRACTarchitecture/reader.pyPython
from dataclasses import dataclass

class FormatError(ValueError):
    pass

@dataclass
class Reader:
    data: bytes
    offset: int = 0

    def take(self, size: int) -> bytes:
        end = self.offset + size
        if size < 0 or end > len(self.data):
            raise FormatError(
                f"read {size} bytes at {self.offset}, file={len(self.data)}"
            )
        chunk = self.data[self.offset:end]
        self.offset = end
        return chunk

    def u16le(self) -> int:
        return int.from_bytes(self.take(2), "little", signed=False)

The error message is the whole point: it carries the size requested, the offset it was requested at and the size of the file. A format failure becomes a coordinate somebody can go and look at.

3 — A normalized representation that keeps its unknowns

Extraction produces an intermediate representation, not a target-engine asset. Converting straight from an original format into an engine's native format collapses two different decisions — what the data means, and how this engine expresses it — into one irreversible step. Keeping them apart is what allows a later engine decision, and what allows a parser fix to be re-run without redoing the conversion by hand.

PUBLIC ARCHITECTURE CONTRACTarchitecture/import_result.pyPython
from dataclasses import dataclass, field
from typing import Generic, TypeVar

T = TypeVar("T")

@dataclass(frozen=True)
class ImportProvenance:
    source_sha256: str
    parser_version: str
    schema_version: str
    evidence_ids: tuple[str, ...]

@dataclass(frozen=True)
class ImportResult(Generic[T]):
    value: T
    provenance: ImportProvenance
    warnings: tuple[str, ...] = ()
    unknown_fields: dict[str, bytes] = field(default_factory=dict)

Unknown material is retained rather than discarded, and the parser version travels with the value. A later parser can revisit what an earlier one could not explain, without reconstructing the original file from memory.

4 — The evidence taxonomy

Most reconstruction arguments are really arguments about grade. A number read out of a data file, a number measured from a running original, a number computed from the first two, a number that best explains the evidence, a number nobody has tested yet, a number chosen deliberately, a number forced by the target engine, and a judgement nobody can automate are eight different things. Collapsing them is how a reconstruction becomes a tribute act without anybody deciding to make one.

  • Source evidence — the original bytes, hashed and read-only.
  • Observation — what the original runtime visibly did, under a recorded scenario.
  • Measurement — a repeatable procedure with units, inputs and outputs recorded.
  • Derivation — deterministically computed from observed or measured values.
  • Inference — the best current explanation of the evidence, not itself observed.
  • Hypothesis — a testable proposition with no evidence yet.
  • Design — a deliberate choice in the reconstruction, making no claim about the original.
  • Compatibility concession — a known departure forced by the target engine or platform.
  • Human verdict — an aesthetic, usability or acceptance decision no automation may sign.
PUBLIC ARCHITECTURE CONTRACTarchitecture/evidence.pyPython
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path

class EvidenceClass(StrEnum):
    OBSERVED = "observed"
    MEASURED = "measured"
    DERIVED = "derived"
    INFERRED = "inferred"
    HYPOTHESIS = "hypothesis"
    DESIGN = "design"
    COMPATIBILITY = "compatibility"
    HUMAN_VERDICT = "human_verdict"

@dataclass(frozen=True)
class EvidenceClaim:
    claim_id: str
    classification: EvidenceClass
    statement: str
    source_path: Path | None
    source_sha256: str | None
    scenario_id: str | None
    confidence: float | None

The classification is data, not prose buried in a report. A build can then refuse to promote a design value into a fidelity claim, and a reader can tell “measured at 36 Hz” from “designed to feel close”.

5 — An oracle is a measurement system

“We compared it to the original” is not evidence. An oracle is a controlled instrument: a named scenario, a seed, an enumerated set of legal actions, a command log with sequence numbers and ticks, a snapshot with a state hash after each command, a defined failure state, and repeatability from a clean profile. Without those, two runs of the original are two anecdotes.

PUBLIC ARCHITECTURE CONTRACTarchitecture/oracle.pyPython
from dataclasses import dataclass
from typing import Any, Protocol

@dataclass(frozen=True)
class OracleCommand:
    sequence: int
    tick: int
    name: str
    arguments: dict[str, Any]

@dataclass(frozen=True)
class OracleSnapshot:
    tick: int
    scene: str
    state_hash: str
    legal_commands: tuple[str, ...]
    values: dict[str, Any]

class OracleAdapter(Protocol):
    def handshake(self) -> dict[str, str]: ...
    def reset(self, scenario_id: str, seed: int) -> OracleSnapshot: ...
    def execute(self, command: OracleCommand) -> OracleSnapshot: ...
    def close(self) -> None: ...

The adapter exposes lifecycle, scenario, seed, legal actions and snapshots. Screen scraping alone supports visual comparison and is not a state oracle; an adapter that cannot enumerate legal commands cannot drive an autoplay traversal either.

In practice the transport is deliberately boring. One line-delimited JSON message per command, one per response, so a session is a file that can be diffed, replayed and attached to a report.

PUBLIC ARCHITECTURE CONTRACTarchitecture/oracle-session.jsonlJSON Lines
{"op":"hello","protocol":1,"client":"pomegranate-harness"}
{"ok":true,"engine":"reference","build":"1.1","capabilities":["reset","step","snapshot"]}
{"op":"reset","scenario":"map01-north-east","seed":731}
{"ok":true,"tick":0,"state_hash":"c3f55c...","legal":["move","turn","wait"]}
{"op":"step","sequence":1,"tick":0,"command":"move","args":{"x":38,"y":29}}
{"ok":true,"tick":36,"events":[{"type":"projectile_spawn","record":9}]}

A protocol illustration, not a literal trace from any project. The shape is what matters: the handshake declares capabilities, reset names a scenario and a seed, and every response carries the tick it landed on.

6 — A deterministic core that cannot draw

The simulation advances state from an input frame and returns a new state and the events that occurred. It has no renderer, no window, no wall clock and no floating-point drift where integers will do. This is not purity for its own sake: a core that cannot draw can be run thousands of times headlessly against oracle-derived expectations, and a divergence in it is a semantic bug rather than a graphics bug wearing a disguise.

PUBLIC ARCHITECTURE CONTRACTarchitecture/simulation.pyPython
from dataclasses import dataclass

@dataclass(frozen=True)
class InputFrame:
    tick: int
    actions: tuple[str, ...]

@dataclass(frozen=True)
class StepResult:
    state: "GameState"
    events: tuple["DomainEvent", ...]

class Simulation:
    def step(self, state: "GameState", frame: InputFrame) -> StepResult:
        next_state = self._apply_actions(state, frame.actions)
        next_state = self._advance_ai(next_state)
        next_state = self._resolve_combat(next_state)
        return StepResult(next_state, self._emit_events(state, next_state))

Rendering is absent from the contract, and the step is total: the same state and the same frame always produce the same result. Everything the presentation layer needs arrives as events rather than by reaching into the state.

7 — State hashing needs a declared canonicalization

A state hash is only meaningful if the serialization it hashes is canonical and the volatile fields are named. Hashing an object dump without controlling key order, formatting and clock-dependent values does not produce determinism; it produces nondeterminism expressed in hexadecimal, which is considerably harder to debug than the original problem.

PUBLIC ARCHITECTURE CONTRACTarchitecture/state_hash.pyPython
import hashlib
import json
from collections.abc import Mapping

VOLATILE_KEYS = {"render_time_ms", "wall_clock", "window_handle"}

def canonical_state_hash(state: Mapping[str, object]) -> str:
    stable = {
        key: value
        for key, value in state.items()
        if key not in VOLATILE_KEYS
    }
    payload = json.dumps(
        stable,
        sort_keys=True,
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()

The volatile set is an explicit, reviewable list rather than a filter somebody remembers to apply. Adding a field to the state without deciding whether it is volatile is then a visible change, not a silent source of flaky replays.

8 — Replay locates the first divergence

A replay that reports “failed” at the end of a trace is nearly useless. What matters is the first tick at which the reconstruction and the recorded trace stop agreeing, because that is where semantic drift starts and everything after it is consequence rather than cause.

PUBLIC ARCHITECTURE CONTRACTarchitecture/replay.pyPython
from dataclasses import dataclass

@dataclass(frozen=True)
class Divergence:
    tick: int
    expected_hash: str
    actual_hash: str
    command_sequence: int


def replay(session: "Session", trace: "Trace") -> Divergence | None:
    for command, expected in trace.frames:
        actual = session.execute(command)
        if actual.state_hash != expected.state_hash:
            return Divergence(
                tick=actual.tick,
                expected_hash=expected.state_hash,
                actual_hash=actual.state_hash,
                command_sequence=command.sequence,
            )
    return None

The return value is the first mismatch or nothing at all. Save-and-reload round trips use the same machinery: reload in a fresh process, continue the trace, and any divergence is a persistence bug caught by the same detector.

9 — Autoplay explores legal actions, not random input

Campaign traversal is a graph problem. Starting from a snapshot, the driver asks what the model or the adapter says is legal, previews each of those commands, and enqueues the results that are neither invalid nor terminal failures — visiting states by their hash so a loop is visited once. Random input spam finds crashes; legal-action traversal finds unreachable content, dead objectives and states the reconstruction can enter and not leave.

PUBLIC ARCHITECTURE CONTRACTarchitecture/autoplay.pyPython
from collections import deque


def traverse(initial: "Snapshot", driver: "Driver") -> set[str]:
    queue = deque([initial])
    visited: set[str] = set()

    while queue:
        state = queue.popleft()
        if state.state_hash in visited:
            continue
        visited.add(state.state_hash)

        for command in driver.legal_commands(state):
            result = driver.preview(state, command)
            if not result.invalid and not result.terminal_failure:
                queue.append(result.snapshot)

    return visited

A real traversal needs bounded depth, scenario heuristics and state abstraction, or the frontier explodes. The principle survives the bounding: exploration starts from legal commands the model supplies, so coverage is a statement about the game rather than about the fuzzer.

10 — Certification binds gates to an exact artefact

Evidence belongs to a hash, not to a project. A gate that passed against one package says nothing about the next package that resembles it, so the candidate identity and the gate state travel together in one manifest, and the manifest carries its own limits in the same document as its passes.

PUBLIC ARCHITECTURE CONTRACTarchitecture/certification.yamlYAML
candidate:
  commit: 0123456789abcdef
  package_sha256: 9a8b7c...
  engine:
    name: reference-engine
    version: 5.0.0

gates:
  parser_fixtures: pass
  clean_build: pass
  blank_profile_boot: pass
  save_load_roundtrip: pass
  replay_state_hash: pass
  campaign_traversal: partial
  linux_native: pass
  windows_native: pending
  human_visual_acceptance: pending

limits:
  - "MAP01 certified; later maps are not implied"
  - "Compatibility-layer smoke is not native certification"

Note the values that are not “pass”. Partial and pending are first-class states, and the limits list exists so that a later reader cannot mistake a scoped certification for a general one.

11 — Automated gates and human gates are different objects

A machine can prove that parsers refuse malformed input, that a build is byte-identical twice, that a blank profile boots, that a save round-trips through a fresh process, that a replay produces the recorded state hashes, that a traversal reaches the objectives it can reach, and that a package contains nothing it should not. A machine cannot decide that the result looks right, plays well, or is worth releasing. Those gates stay open, visibly, until a person signs them — and a project that quietly lets automation sign them has stopped measuring anything.

12 — Rights and provenance are gates, not paperwork

Two rules govern every reconstruction in this portfolio. Original media is never redistributed: where a reconstruction needs proprietary data, the player supplies media they legally hold and an importer converts it locally. And no protection is ever circumvented: where a protected runtime cannot be observed lawfully, the finding is recorded as blocked and the corresponding parity claim is not made. A blocked gate in public is worth more than a claim nobody can check.

13 — Failure modes this method exists to prevent

  • The tribute act: enough design decisions accumulate, each defensible alone, that the result is inspired by the original rather than being it. This is prevented by grading every value, not by taste.
  • The confident parser: malformed input is padded or guessed through, and the reconstruction is built on fiction that never raised an error.
  • The anecdotal oracle: somebody ran the original once and remembers what it looked like. Without scenarios, seeds and snapshots, that is a story.
  • Hexadecimal nondeterminism: a state hash over an uncanonicalized dump, which converts flakiness into an unreadable diff.
  • Evidence inheritance: gates that passed on an earlier build are quietly credited to a later one because the two resemble each other.
  • Gate creep: an automated pass is described as acceptance, or a packaged candidate as a release, and the vocabulary stops carrying information.