Skip to content

symfonic.services.shadow.digest

digest

Canonical digests — the identity function for requests and payloads.

Two places need "is this the same request?" to mean the same thing: the deterministic stub (so one request always gets one answer) and the replay index (so a recorded answer is found again). Sharing one canonicaliser is what keeps a replay from silently missing a hit that the recorder produced.

The digest is also the comparator's only notion of identity, which makes one failure mode worse than being wrong: being coarse. A canonicaliser that reduced every non-JSON value to its type name would give two materially different provider requests the same digest, and the comparator would report parity for a replacement that did not behave like the legacy path. So values that carry state are canonicalised structurally — dataclasses, pydantic models, enums, temporal values, and any object exposing Python-level state — and a value whose state this module cannot see at all is refused rather than flattened. Fail closed: no digest is better than a digest that collides.

The honest limit: an object is digested by the state Python exposes. A type that keeps part of its state at C level (a socket keeps its peer address there) is digested by the visible part, which is coarser than the value but still not a bare type name. Only a value that exposes nothing is refused outright.

Nothing here executes user code. Reduction reads declared fields and instance state; it never calls __repr__, __str__, or a model's serialiser, because a repr can carry a memory address, a bearer token, or a customer name straight into an evidence file.

canonical_json

canonical_json(value: Any) -> str

A stable JSON rendering: sorted keys, no whitespace drift, no NaN.

Source code in src/symfonic/services/shadow/digest.py
def canonical_json(value: Any) -> str:
    """A stable JSON rendering: sorted keys, no whitespace drift, no NaN."""
    return json.dumps(_plain(value), sort_keys=True, separators=(",", ":"), allow_nan=False)

digest_of

digest_of(*parts: Any) -> str

A sha256 over the canonical rendering of every part, in order.

Source code in src/symfonic/services/shadow/digest.py
def digest_of(*parts: Any) -> str:
    """A sha256 over the canonical rendering of every part, in order."""
    hasher = hashlib.sha256()
    for part in parts:
        hasher.update(canonical_json(part).encode("utf-8"))
        hasher.update(b"\x1f")
    return hasher.hexdigest()