Assembling Examiner-Ready Filing Packages

An examiner-ready filing package is a single, self-describing bundle that carries the actuarial memorandum, the rule-matrix version, the assumption-set hashes, the scenario seeds, and the sealed evaluation records — organised so a reviewer can open it and navigate the whole submission without any access to your systems. This page shows the focused assembly technique that turns scattered artifacts into one checksummed manifest with a table of contents, and it sits inside the wider Actuarial Audit Trail Architecture discipline within Regulatory Architecture & Compliance Mapping.

The Problem

By the time a Principle-Based Reserving submission is ready, the evidence for it is spread across a dozen places: the memorandum sits in a document store, the rule matrix is a tagged commit in version control, assumption hashes live in a validation log, scenario seeds are buried in run metadata, and the sealed evaluation records sit in append-only storage. That is fine while you own the machines, but a VM-31 PBR Actuarial Report is read by someone who does not — an examiner who receives a package, opens it on an air-gapped review workstation, and must be able to walk from the memorandum’s Section 3 claim to the exact assumption hash and scenario seed that produced the number, then verify none of it was altered in transit. Assembly is the step that makes that possible: it gathers every referenced artifact, writes a navigable table of contents, records a checksum for each file, and refuses to produce a package if anything the memorandum points to is missing. It is a distinct concern from how each record was hash-chained or written to WORM storage — those guarantee integrity at rest; assembly guarantees the bundle is complete, ordered, and openable on its own.

A Minimal Working Example

The routine below walks a submission directory, records a SHA-256 checksum for every artifact, checks that each required item is present, and writes a manifest.json with a human-readable table of contents. It bundles the result into a single archive an examiner can open offline. It has no third-party dependencies and runs as-is:

from __future__ import annotations

import hashlib
import json
import zipfile
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path

# Each entry an examiner must be able to open, in the order they should read it.
REQUIRED_ARTIFACTS: dict[str, str] = {
    "actuarial_memorandum": "01_actuarial_memorandum.pdf",
    "rule_matrix_version": "02_rule_matrix_v.json",
    "assumption_set_hashes": "03_assumption_hashes.json",
    "scenario_seeds": "04_scenario_seeds.json",
    "sealed_evaluation_records": "05_sealed_evaluation_records.ndjson",
}

INLINE_MAX_BYTES = 25 * 1024 * 1024  # inline small artifacts, reference large ones


@dataclass(frozen=True)
class TocEntry:
    """One artifact as it appears in the package table of contents."""

    section: str
    filename: str
    sha256: str
    size_bytes: int
    storage: str  # "inline" or "reference"


def _checksum(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(1 << 20), b""):
            digest.update(block)
    return digest.hexdigest()


def build_toc(source_dir: Path) -> list[TocEntry]:
    entries: list[TocEntry] = []
    missing: list[str] = []
    for section, filename in REQUIRED_ARTIFACTS.items():
        artifact = source_dir / filename
        if not artifact.is_file():
            missing.append(f"{section} -> {filename}")
            continue
        size = artifact.stat().st_size
        entries.append(
            TocEntry(
                section=section,
                filename=filename,
                sha256=_checksum(artifact),
                size_bytes=size,
                storage="inline" if size <= INLINE_MAX_BYTES else "reference",
            )
        )
    if missing:
        raise FileNotFoundError("filing package incomplete: " + "; ".join(missing))
    return entries


def assemble_package(source_dir: Path, out_path: Path, filing_id: str) -> str:
    toc = build_toc(source_dir)
    manifest = {
        "filing_id": filing_id,
        "assembled_at_utc": datetime.now(timezone.utc).isoformat(),
        "table_of_contents": [asdict(e) for e in toc],
    }
    manifest_bytes = json.dumps(manifest, indent=2, sort_keys=True).encode()
    manifest_checksum = hashlib.sha256(manifest_bytes).hexdigest()

    with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as pkg:
        pkg.writestr("manifest.json", manifest_bytes)
        pkg.writestr("manifest.sha256", manifest_checksum)
        for entry in toc:
            if entry.storage == "inline":
                pkg.write(source_dir / entry.filename, arcname=entry.filename)
    return manifest_checksum


if __name__ == "__main__":
    root = Path("submission_2024Q4")
    seal = assemble_package(root, Path("filing_2024Q4.zip"), filing_id="NPR-2024Q4-0007")
    print("package sealed with manifest checksum", seal)
Filing assembly flow: scattered artifacts to a sealed, navigable examiner package Five source artifacts feed a checksum and completeness-check step, which emits a manifest table of contents, which is sealed into a single examiner-ready package archive. Actuarial memorandum Rule-matrix version Assumption-set hashes Scenario seeds Sealed eval records Checksum + completeness SHA-256 per file Manifest table of contents ordered + checksummed Examiner package one navigable bundle opens without system access
Assembly gathers scattered artifacts, checksums each, and seals them into one manifest-indexed package.

How It Works, Block by Block

REQUIRED_ARTIFACTS is an ordered contract. A plain dictionary maps each logical section — memorandum, rule matrix, assumption hashes, scenario seeds, sealed records — to the filename it must occupy, and the numeric filename prefixes (01_, 02_) fix the reading order. Because insertion order is preserved, the table of contents an examiner sees is the sequence you intend them to read, not whatever order the filesystem happened to enumerate. Changing what a complete package contains is a one-line edit here, and every downstream check inherits it.

build_toc fails closed on a missing artifact. The loop records which required sections have no file on disk and, if any are absent, raises FileNotFoundError with the exact section-to-filename pairs that are missing. This is the assembly-time equivalent of a pre-flight check: a package that references a memorandum but never bundled the assumption hashes it cites is rejected before it is written, not after an examiner opens it and finds a dangling reference. Missing-artifact detection is the single most valuable thing assembly does, because the failure it prevents — an incomplete submission — is exactly the one that is invisible until someone external tries to navigate it.

Each file carries its own SHA-256. _checksum streams the file in one-megabyte blocks so a hundred-megabyte scenario file never has to sit in memory, and stores the digest per entry. These per-file checksums let an examiner verify each artifact independently: if the memorandum’s hash in the manifest matches the memorandum in the bundle, that file is byte-identical to what you sealed, regardless of how the archive was transported.

The manifest is itself checksummed. After serialising the table of contents with sorted keys and stable indentation, the routine hashes the manifest bytes and writes that digest as manifest.sha256 alongside the manifest. This is the root of trust for the whole package: an examiner verifies the manifest checksum once, then trusts every per-file checksum inside it. Sorting keys and fixing indentation make the manifest bytes deterministic, so the same table of contents always yields the same root checksum.

Large artifacts are referenced, not inlined. The storage field marks any file over INLINE_MAX_BYTES as a reference rather than embedding it, and the archive only writes inline files. A multi-gigabyte seriatim scenario dump does not belong inside every copy of the package; the manifest still records its name and checksum, so the examiner can request that one file separately and verify it against the manifest when it arrives. Small, always-read artifacts like the memorandum are inlined so the core package is openable on its own.

Edge Cases and Production Hardening

A silently truncated artifact passes a presence check but not a checksum. build_toc confirms a file exists, but existence is not integrity — a partially written scenario file is present and wrong. Guard the boundary by verifying each artifact’s checksum against the value recorded upstream when it was sealed, before you trust it into the package:

def verify_against_ledger(entries: list[TocEntry], sealed_hashes: dict[str, str]) -> None:
    for entry in entries:
        expected = sealed_hashes.get(entry.filename)
        if expected is None:
            raise KeyError(f"{entry.filename} was never sealed upstream")
        if expected != entry.sha256:
            raise ValueError(
                f"{entry.filename} checksum {entry.sha256[:12]} != sealed {expected[:12]}"
            )

The sealed_hashes come from the tamper-evident ledger, so a file that changed between sealing and assembly is caught here rather than presented to an examiner as authentic.

The manifest checksum must cover the manifest as the examiner will read it. If you hash a Python dict but write differently ordered JSON into the archive, the examiner’s recomputed digest will not match and your genuine package looks tampered. Always hash the exact bytes you write — serialise once, hash those bytes, and write those same bytes — as the example does. Never re-serialise between hashing and writing.

Inline thresholds interact with archive limits. Inlining every artifact seems safest but produces packages too large to transmit and slow to open on a review workstation. The reference path keeps the core bundle small and navigable while preserving verifiability for bulk files; document in the manifest how a referenced file is obtained, so the examiner is never left with a checksum for a file they cannot locate. Pair the manifest with the upstream Hash-Chaining Audit Logs for Actuarial Filings so a referenced record’s integrity is anchored in a chain the examiner can independently walk.

Compliance Note

This assembly step is the delivery vehicle for a VM-31 PBR Actuarial Report, which requires the appointed actuary to present a report that documents each reported value and lets a reviewer trace it to its source assumptions and methods. A manifest that binds the memorandum to the assumption-set hashes and scenario seeds that produced each figure is precisely the navigable evidence VM-31 contemplates. The same package answers to the outcomes-analysis and documentation expectations of the Federal Reserve’s SR 11-7 guidance on model risk management, which calls for validation evidence that an independent party can review without reconstructing the modelling environment. The memorandum you bundle should itself follow the layout described in Structuring the VM-20 Actuarial Memorandum, so its section numbering lines up with the table of contents your assembler emits.

Frequently Asked Questions

What makes a filing package examiner-ready rather than just complete?

Completeness means every required artifact is present; examiner-ready means the package is also self-navigating and self-verifying without your systems. That requires an ordered table of contents, a per-file checksum so each artifact can be independently confirmed byte-for-byte, and a checksummed manifest that ties them together, all inside one bundle a reviewer can open on an isolated workstation.

How should assembly handle very large scenario files?

Reference them in the manifest instead of inlining them. Record the filename, size, and SHA-256 for the large file so its integrity is still verifiable, but keep it out of the core archive so the package stays small enough to transmit and quick to open. Inline only the small, always-read artifacts such as the memorandum and the assumption hashes.

Why checksum the manifest itself and not only the individual files?

The manifest checksum is the single root of trust: an examiner verifies it once, then relies on the per-file checksums it contains. Without it, an altered manifest could list a tampered file’s new hash and appear internally consistent. Hashing the exact manifest bytes you write, with sorted keys and fixed formatting, makes that root digest deterministic and reproducible.

How is package assembly different from hash-chaining and WORM storage?

Hash-chaining and WORM storage guarantee that individual records cannot be altered once written; they secure evidence at rest. Assembly is a separate concern that gathers those records plus the memorandum, rule matrix, assumption hashes, and seeds into one ordered, checksummed bundle a reviewer can navigate offline. A package can be perfectly WORM-stored and still be unusable if it is incomplete or has no table of contents.

Up a level: Actuarial Audit Trail Architecture · Regulatory Architecture & Compliance Mapping


Regulatory references: NAIC Valuation Manual VM-31 (PBR Actuarial Report); Board of Governors of the Federal Reserve System SR 11-7 (Guidance on Model Risk Management).