3. Subsystem Model Version Locking

Why version locking belongs in the manifest, not in the deck

A full-vehicle crash model is assembled from independently developed subsystem decks: body-in-white, chassis, powertrain, restraints, and barriers. Each of these is revised on its own cadence, and the assembly deck only references them through *INCLUDE statements. If the assembly manifest records a subsystem by name alone, the deck silently picks up whatever revision happens to sit at that path on the build machine. Two analysts running the same assembly can therefore produce different results without changing a single line of the assembly file. Version locking closes that gap by binding every subsystem to an explicit revision identifier and then proving, before the run is released, that the assembled deck contains exactly those revisions and nothing else.

The mechanism has two halves. The lock is a manifest entry that pairs a subsystem role with a concrete revision token — a file name suffix, a directory name, a repository tag, or a content hash. The verification is a check that resolves every include actually present in the assembled deck back to a manifest entry and confirms the revision matches. Locking without verification is documentation; verification without locking has nothing to compare against. Both are required for the release gate to mean anything.

Manifest structure for locked subsystems

A workable manifest is a small, machine-readable table. The fields below are the minimum needed to make the lock unambiguous and auditable; the revision field is the lock itself, and expected_path is what the resolver will compare against after include expansion.

FieldPurposeExample value
roleSubsystem identity used in reportsbody
revisionThe lock: exact revision tokenr14.2
expected_pathResolved path the include must land onsubsystems/body/biw_r14.2.k
sha256Content hash, catches same-name edits9f3c…a71e
ownerWho approves a revision changebody-cae

The sha256 field matters more than it first appears. Revision tokens live in file names and are easy to keep while editing content in place — a common failure mode when a hotfix is applied to a file without bumping its suffix. The hash makes that invisible edit visible at release time.

Keep the manifest under version control alongside the assembly deck. A lock that is not itself revision-controlled can be edited after the fact, which defeats the audit trail the release gate depends on.

Resolving includes and matching them to locks

Verification starts from the assembled deck, not from the manifest. The order matters: if you iterate over manifest entries and check that each file exists, you will never notice an extra include that no manifest entry covers — an unlocked subsystem that slipped into the deck. The correct direction is to expand every *INCLUDE in the deck, resolve each to an absolute path, and then look up that path in the manifest.

The resolution step must handle nested includes, because subsystem decks frequently include their own sub-assemblies (a chassis deck pulling in suspension and steering, for example). A single-pass scan of the top-level deck will miss those. The check below performs a recursive expansion, records the include chain for each resolved file so that a mismatch can be traced to its origin, and classifies every resolved path into one of three outcomes: locked and matching, locked but mismatched, or not locked at all.

import hashlib, json, os, re

INCLUDE_RE = re.compile(r'^\*INCLUDE\s*,\s*(\S+)', re.IGNORECASE)

def expand_includes(deck_path, base_dir, seen=None, chain=None):
    """Recursively resolve *INCLUDE statements. Returns {abs_path: include_chain}."""
    seen = seen if seen is not None else {}
    chain = chain or []
    with open(deck_path, encoding="utf-8", errors="replace") as fh:
        for line in fh:
            m = INCLUDE_RE.match(line.strip())
            if not m:
                continue
            target = m.group(1).strip().strip('"')
            resolved = os.path.normpath(
                target if os.path.isabs(target) else os.path.join(base_dir, target))
            if resolved in seen:
                continue
            seen[resolved] = chain + [deck_path]
            if os.path.isfile(resolved):
                expand_includes(resolved, os.path.dirname(resolved), seen, chain + [deck_path])
    return seen

def sha256_of(path):
    h = hashlib.sha256()
    with open(path, "rb") as fh:
        for chunk in iter(lambda: fh.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()

def verify_locks(deck_path, manifest_path, base_dir):
    manifest = json.load(open(manifest_path, encoding="utf-8"))
    locks = {os.path.normpath(e["expected_path"]): e for e in manifest["subsystems"]}
    resolved = expand_includes(deck_path, base_dir)

    report = {"matched": [], "mismatched": [], "unlocked": [], "missing": []}
    for path, chain in resolved.items():
        entry = locks.get(path)
        if entry is None:
            report["unlocked"].append({"path": path, "via": chain})
            continue
        actual = sha256_of(path) if os.path.isfile(path) else None
        if actual is None:
            report["missing"].append({"path": path, "via": chain})
        elif actual != entry["sha256"]:
            report["mismatched"].append(
                {"path": path, "role": entry["role"],
                 "locked": entry["revision"], "actual_sha256": actual, "via": chain})
        else:
            report["matched"].append({"path": path, "role": entry["role"],
                                      "revision": entry["revision"]})
    return report

if __name__ == "__main__":
    rep = verify_locks("assembly/main_deck.k", "assembly/manifest.json", "assembly")
    print(json.dumps(rep, indent=2))
    ok = not (rep["mismatched"] or rep["unlocked"] or rep["missing"])
    print("RELEASE GATE:", "PASS" if ok else "BLOCK")

The gate condition is deliberately strict: any mismatched, unlocked, or missing entry blocks release. A missing file is treated as a failure rather than a warning because an include that cannot be resolved means the deck as written is not the deck that will run — the solver will either error out or, worse, pick up a stale file from a search path.

Reading the verification result

The three failure classes point to different root causes, and the include chain in each record is what makes the diagnosis fast. A mismatched entry with a correct-looking path almost always means the file was edited without a revision bump; the fix is to either restore the locked content or, if the edit is legitimate, raise a revision change through the owner and update both revision and sha256. An unlocked entry is a coverage gap: a subsystem was added to the deck but never registered in the manifest, and the chain shows which parent deck pulled it in. A missing entry usually traces to a path that resolves on the developer machine but not in the build tree, which is exactly the class of problem a path audit catches earlier — here it surfaces as a release blocker.

The diagram below shows how a single assembled deck fans out into locked subsystems, with one branch failing the hash comparison and one include arriving with no lock at all.

Assembled deck main_deck.k recursive *INCLUDE Path resolver normalize + expand record include chain Lock manifest role · revision expected_path sha256 · owner body biw_r14.2.k · r14.2 hash match chassis chassis_r09.k · r09 hash match powertrain pt_r05.k · locked r05 sha256 mismatch restraints rst_r11.k · r11 hash match barrier barrier_dev.k no manifest entry Release gate 1 mismatch 1 unlocked BLOCK 3 of 5 locked OK Green = locked and hash-verified · Red = locked revision but content changed · Purple = include present with no lock

Closing the loop before the next run

Once the report is clean, the manifest itself becomes the release record: the set of revision and sha256 values that produced the deck is fixed and auditable. Two habits keep it that way. First, treat any revision change as a manifest edit that goes through the subsystem owner, so the lock cannot drift silently. Second, re-run the verification as the first step of every release build rather than as a manual pre-check — a gate that depends on someone remembering to run it is not a gate. When the report returns PASS, the assembled deck is provably composed of the locked revisions, and downstream checks such as unit consistency and mass reconciliation can proceed against a known configuration instead of an unknown one.