AI Research Library

Training Lineage as a Governed Supply Chain

Rajesh Ranjan Mahapatra

A model release is defensible only when every behaviour-affecting input and every approval-bearing judgement closes into one verifiable, time-valid manifest.

© 2026 Rajesh Ranjan Mahapatra. 32 min read

Training Lineage as a Governed Supply Chain

A model release is defensible only when every behaviour-affecting input and every approval-bearing judgement closes into one verifiable, time-valid manifest.

TLDR

  1. The releasable object is not a weight file. It is the exact model bundle plus the causal ancestry, evaluation evidence, authority and recovery state that make its intended use defensible.
  2. The mechanism is release closure: content-addressed artefacts connected by typed dependency and decision edges, checked transitively before a promotion identity can act.
  3. A tokenizer, calibration value, evaluation rubric or data-use condition can invalidate a release without changing its headline model name. Promotion must therefore bind the whole effective system.
  4. Hashes and signatures prove identity and custody, not truth, rights or fitness. Full upstream lineage is also impossible for some acquired models, so unknowns need explicit consequence limits.
  5. Promote a signed release manifest, never a registry tag alone. Reject candidates whose evidence, approval or rollback package does not refer to the exact dependency closure being deployed.

The Friday release with one checksum

At 17:40 on Friday, two candidates for a document-triage model are waiting at the production gate. Both point to a weight file with the same SHA-256 digest. Both registry entries say version 7. Both show a macro F1 score of 0.923. The release manager is told that the candidates are identical.

They are not. Candidate A packages tokenizer version 1, the exact runtime image used during evaluation, a calibration value fitted on the held-out cohort, and an approval that names the resulting bundle digest. Candidate B packages tokenizer version 2 after a late normalisation fix. Its evaluation still refers to the old bundle. One training collection also carried a data-use approval that expired the previous day.

The weight checksum cannot distinguish these releases because the changed facts sit outside the weights. The headline score cannot distinguish them because it was produced against only one bundle. The registry tag cannot distinguish them because tags are names, not immutable evidence. Even a valid signature on Candidate B would show that an identified signer endorsed those bytes, not that the evidence remained applicable or the data remained authorised.

One weight digest, two release states Two candidate releases share the same weights. Candidate A binds tokenizer, evaluation, data permission and approval consistently. Candidate B changes the tokenizer, uses stale evaluation and has an expired data permission, so it is rejected. weights.safetensors sha256: 8b1c…72e9, identical in both candidates Candidate A tokenizer v1 evaluation binds bundle A data permission valid approval binds evidence A admit Candidate B tokenizer v2 evaluation binds bundle A data permission expired approval binds bundle A reject
Figure 1. The checksum is necessary but not sufficient. The scenario is synthetic. The intervention holds the weights constant while changing tokenizer, evidence applicability and data authority. Only the first candidate has a closed release record.

The core answer is that a model release is a dependency-and-decision closure. Every artefact that can alter behaviour, every piece of evidence used to justify promotion, and every authority condition that permits the action must be bound to the same release identity. If one strand changes, downstream evidence becomes suspect until the impact is calculated and the required gates are rerun.

This goes beyond preserving a historical story. The lineage graph must do operational work. It should answer, before promotion, whether the candidate is exactly what was evaluated. After a defect or withdrawal, it should calculate which releases are exposed. During rollback, it should reconstruct a complete prior package rather than swap only the weights.

Three practical questions expose the gap. First, identity: are these the exact bytes and configurations named by the candidate? Second, applicability: do the test results, rights and approvals still apply to those exact bytes, this purpose and this time? Third, recovery: can operators replace or withdraw the same effective package without leaving a tokenizer, calibration value or routing rule behind?

A conventional registry can answer the first question for weights and perhaps an image. A model card may answer parts of the second in prose. A deployment runbook may answer pieces of the third. The causal failure occurs when those answers are stored independently and joined by mutable names. Release closure converts the join into an invariant: every supporting record names immutable subjects, and the root manifest can be verified without trusting a human to remember which version went with which evidence.

Part I

A release is a causal graph

A source repository encourages a comforting picture: code enters a build, a binary comes out, and a version points to the result. Training systems have more causal inputs. Data selection, preprocessing, random seeds, base weights, adaptation layers, tokenisation, decoding defaults, calibration and runtime libraries can all alter the effective function. Final weights compress the consequences of many of these choices without preserving a readable account of them.

NIST makes this asymmetry explicit. Model architecture code is amenable to familiar versioning and reproducibility controls, while final weights arise only after collection, processing and training, where complete tracking is harder. Its guidance asks organisations to document what remains uncovered rather than silently treating gaps as known.1 That is the correct starting posture: unknown lineage is a recorded system state, not an empty field to be ignored.

Thought experiment one: the tokenizer transplant

Take one language model, one weight file and one evaluation set. Run it with tokenizer A. Then vary only the tokenizer, replacing A with tokenizer B while keeping the weights and prompts byte-for-byte identical. The model name and weight hash have not changed, yet token boundaries, sequence lengths and token IDs may change. The function presented to users is therefore different.

This thought experiment identifies the relevant unit. A weight file is an internal component. The deployed model is a composition of preprocessing, weights, inference code and post-processing. The exact boundary should follow causal influence rather than organisational ownership. A tokenizer owned by a platform team still belongs in the release closure if it can alter the candidate’s outputs.

The same rule applies to less obvious state. A calibration temperature can change which cases cross an automation threshold. A prompt template can change a generative classifier’s label distribution. A quantisation recipe can alter numerical behaviour even when the logical architecture is unchanged. A policy file can turn an advisory output into an automatically executed route. These items need not all be called “model artefacts”, but they are release ancestors when they influence the observable or authorised outcome.

The boundary should remain discriminating. Monitoring dashboards, team documents and unused datasets do not enter closure merely because they are nearby. The test is counterfactual: if this item were changed while all other named inputs stayed fixed, could the produced bundle, its behaviour, the evidence interpretation, release authority or recovery path change? A credible “yes” creates a typed dependency. A “no” leaves the item in ordinary operational inventory.

The model lineage braid Five curved strands for data, code and configuration, weights and runtime, evaluations, and release authority converge on one release seal. Checkpoints indicate transformations and attestations along each strand. Behaviour-affecting ancestry Promotion boundary Data Code and configuration Weights and runtime Evaluation evidence Release authority source and purposeprepared set commitbuild run base plus adapterbundle digest rubrictest result review roleapproval release r7 manifest closure passes Any broken strand blocks promotion
Figure 2. The model lineage braid. This authored conceptual figure is the paper’s signature view. Five unlike strands converge because promotion depends on both material ancestry and decision authority. The geometry is illustrative, with no measured quantities.

From inventory to ancestry

A bill of materials tells us what components are present. A lineage graph adds how one artefact was derived from others, by which activity, under whose control. The W3C PROV model supplies a durable abstraction: entities are used or generated by activities, and agents influence those activities.2 SLSA adds verifiable build provenance describing where, when and how an artefact was produced, with dependencies and the trusted build platform made explicit.3

Those foundations are useful, but a training release needs one further step. It must connect build ancestry to evaluations and decisions. in-toto statements bind typed claims to immutable subjects by digest, and its test-result and release predicates show how test evidence and final release composition can be represented separately.45 The paper’s proposed release manifest composes these ideas rather than replacing them.

Typed edges make lineage executable

A lineage link should state more than “related to”. was_trained_from, contains, tested, used_rubric, approved and can_rollback_to carry different intervention semantics. If a training source is withdrawn, the policy follows was_trained_from towards descendants that may require retraining. If an evaluation rubric changes, the policy follows used_rubric and may permit re-scoring without rebuilding weights. Generic links erase that distinction.

Edges also need producers and evidence. A statement that adapter A “came from” dataset D is weaker than an attested training activity that identifies A as output, D and code C as resolved inputs, recipe H as an external parameter, and builder B as the executing platform. The richer record lets a verifier reject an untrusted local run even when its claimed inputs match. Lineage becomes a control when relations have machine-checkable meaning and provenance of their own.

Transitive ancestry determines the impact radius A release at the top connects through a bundle, evidence and approval to deeper dependencies. A highlighted source dataset has paths to two releases, showing that an upstream change can expose descendants transitively. release r7 root manifest model bundle evidence pack approval training run tokenizer evaluation set rubric source Bwithdrawn trainer code oracle labels review role impact path
Figure 3. Impact follows transitive ancestry. A withdrawn source is not merely a data-team issue. Every descendant release reachable through preparation and training edges enters the impact set. The graph is conceptual.
Part II

Five strands, two ledgers

The braid contains five strands because training governance fails when unlike things are collapsed into one generic record. Data answers questions of origin, consent, licence, purpose, sensitivity and cohort. Code and configuration answer how a transformation ran. Weights and runtime identify the effective executable composition. Evaluation evidence states what was tested, against which rubric and population. Release authority records who could approve, what they approved, under which conditions and until when.

Standards already cover useful portions. SPDX 3.0.1 has distinct Dataset, AI and Build profiles, while CycloneDX’s machine-learning bill of materials represents models, datasets, configurations and dependencies.67 Model cards document intended uses and performance conditions; datasheets document dataset motivation, composition, collection and recommended use.89 These are complementary records. None should be stretched into pretending that it proves every other property.

Material provenance and decision provenance

Material provenance traces what the release was made from and how. Decision provenance traces why promotion was allowed: the intended use, evaluation subject, rubric, thresholds, exceptions, reviewer role, decision and expiry. A complete material graph can recreate a candidate that nobody was authorised to release. A complete decision log can show an approval whose subject cannot be reconstructed. A governed release needs both.

This distinction prevents a common category error. A cryptographic signature is evidence about identity and integrity. Sigstore, for example, binds short-lived signing certificates to identities and records signing events in transparency logs.10 Those are valuable chain-of-custody properties. They do not establish that a dataset’s licence label is correct, that an evaluation measures the intended capability, or that the signer held the relevant business authority at release time.

Decision provenance therefore needs a structured receipt rather than a free-text “approved” field. The receipt should identify the candidate digest, evidence digest, intended use, governing policy revision, reviewer or automated authority, decision, conditions, valid interval and unresolved exceptions. The signature authenticates the receipt. Policy determines whether that signer was entitled to make that claim. The release gate verifies both.

This matters when teams reorganise. A person’s identity can remain valid while their delegated role changes. Conversely, a new person may inherit the authorised role without invalidating scientific evidence. Keeping identity, role and decision subject separate allows precise revocation. It avoids two blunt responses: accepting every historically valid signature forever, or rerunning expensive model tests merely because an approver rotated.

Thought experiment two: the perfectly signed forbidden build

Start with Candidate A, whose artefacts, evidence and approval all align. Now vary only one feature: the data-use permission expires one hour before promotion. Do not alter a byte in the dataset, code, weights, evaluation or signed manifest. Every digest still verifies. The model’s measured behaviour is unchanged. Yet the release has become inadmissible for that purpose.

The experiment separates integrity from authority. A signature can prove that the manifest was not changed after an identified act. It cannot make the underlying act permissible. This is why a release manifest needs effective times and purpose constraints, and why promotion must evaluate them against the proposed release time rather than merely retain them for later audit.

Interventions reveal which evidence becomes stale Four horizontal experiment rows hold weights constant. Changing tokenizer triggers behaviour and re-evaluation impact. Expiring rights changes admissibility only. Changing the rubric changes the decision meaning. Rotating an authorised signer without changing role leaves the decision valid if policy permits. Hold weights constant, change one causal feature 1. Tokenizer A → B effective function may change rebuild, re-evaluate old score is stale 2. Data permission valid → expired behaviour unchanged block release authority is invalid 3. Evaluation rubric v4 → v5 same outputs, new criterion re-score or re-test approval meaning changed 4. Signer identity X → Y, same authorised role custody changes, authority may not verify role policy no automatic re-test
Figure 4. One-variable interventions separate failure classes. The outcomes are practitioner synthesis, not measured universal effects. They show why one “lineage complete” flag cannot replace typed edges and policies.

Presence is not correctness

Documentation can be present and still wrong. A large-scale audit of 1,858 datasets used in instruction and alignment collections reported licence omission above 70 per cent and licence error above 50 per cent on popular hosting sites.11 The result is bounded to the audited corpus, but it establishes an important negative control: metadata existence is not evidence of metadata validity.

Consequently, the manifest should preserve declared claims and concluded judgements separately. “Declared licence: X” records what a supplier or host states. “Concluded use decision: allowed for purpose U until time T, reviewed under policy P” records the consuming organisation’s bounded judgement. The latter should cite the former without overwriting it. When a source is corrected, the dependency graph can identify affected descendants and reopen the judgement.

Integrity and admissibility are independent axes A two by two field places release candidates according to cryptographic integrity and substantive admissibility. Only the upper-right quadrant is eligible for promotion. Two questions, neither substitutes for the other Integrity and custody evidence → Rights, fitness and authority ↑ Admissible, unverified approval and tests exist artefact identity is uncertain reject Verified and admissible exact subject, valid evidence current authority, recovery ready eligible Neither unknown bytes unknown permission or fitness reject Perfectly signed, invalid identity and custody verified rights or evidence expired reject low high low high
Figure 5. Integrity and admissibility form independent gates. The quadrants are conceptual. Cryptographic evidence moves a candidate horizontally; rights, evaluation fitness, authority and recovery move it vertically.
Part III
The argument in one passChart · flow
flowchart LR
  A["The Friday release with one<br/>checksum"] --> B["A release is a causal graph"]
  B --> C["Five strands, two ledgers"]
  C --> D["Release closure under change"]
  D -. "re-test" .-> A
  classDef first fill:#0a1424,stroke:#7B4CD8,stroke-width:2px,color:#e6f3ff
  classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff
  class A first
  class B,C,D step
A structural route through Training Lineage as a Governed Supply Chain, from The Friday release with one checksum to Sources. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Release closure under change

We can now state the mechanism precisely. Let a lineage graph be G = (V, E), where V is the set of immutable artefacts and decision records, and E is the set of typed dependency relations. Edges point from a release or derived artefact to what it depends on. For release root R, its closure is:

C(R) = { v ∈ V | R ↝ v }

The arrow means “reachable by zero or more typed dependency edges”. If the release points to a bundle, the bundle points to an adapter and tokenizer, and the adapter points to a prepared dataset and trainer run, all of those nodes belong to C(R). Direct links alone are insufficient because the most consequential defect may sit several transformations upstream.

A candidate is admitted for use u at time t only when six non-compensating predicates hold:

admit(R,t,u) = I(R) ∧ Cₒ(R) ∧ A(R,t,u) ∧ T(R,t) ∧ E(R,u) ∧ K(R)

I verifies artefact identity and attestations. Cₒ checks that the required closure is complete and typed. A checks rights and release authority for the intended use. T checks effective time, expiry and supersession. E requires evaluation evidence to bind the exact bundle and use. K requires a verified rollback or recovery package. Increasing one term cannot compensate for a zero elsewhere. A very accurate model with expired authority still fails.

Completeness is declared before the candidate arrives

No system can record every physical cause of a training run. Processor microcode, ambient temperature and the history of every infrastructure dependency could be expanded without end. Release closure avoids this regress by making completeness relative to a predeclared release schema and threat model. The schema says which artefact kinds and relations are mandatory for this class of model and use.

For a low-consequence batch classifier, the required kinds might be training data, code, recipe, weights, tokenizer, evaluation, approval and rollback. A safety-critical vision system may additionally require sensor calibration, compiler and accelerator revisions, simulation scenarios, hardware validation and dual approval. The candidate does not choose its own standard after seeing which records are available. The product owner and assurance function define the closure contract in advance.

This turns incompleteness into a deterministic failure. A missing dataset edge, unbound evaluator configuration or absent rollback package cannot be waived by a high aggregate score. A bounded exception is possible, but the exception itself becomes a signed decision node with scope, compensating controls and expiry. In other words, uncertainty may be accepted, but it may not remain invisible.

Practitioner synthesis Release closure is not a new provenance standard. It is a promotion policy that composes content addressing, typed provenance, test attestations, temporal authority and recovery into one hard gate.

Minimal worked example: five nodes that are enough

Consider a small sentiment classifier used only in a research sandbox. Its release root points to a model bundle, an evaluation result and an approval. The bundle points to one dataset snapshot, one training run and one tokenizer. The evaluation points back to the bundle digest and its held-out set. The approval points to the same bundle and evaluation digests.

In this bounded case, closure can be compact. It does not need a committee or a graph database. A canonical JSON manifest and a verifier in continuous integration are enough. Change the tokenizer, and the bundle digest changes. The old evaluation no longer binds. The verifier blocks promotion until a new evaluation and approval are attached. The control scales down because the invariant is structural, not bureaucratic.

Production-shaped worked example: document triage r7

Now expand the case into a synthetic service that classifies inbound documents into specialist queues. Release r7 contains base weights, an adapter, tokenizer, inference image and calibration state. The adapter depends on a prepared training collection, trainer commit, recipe and base-model revision. Its evidence pack includes a grouped held-out set, evaluation rubric, cohort metrics, security probes and a calibration review. A model-release authority approves the exact bundle and evidence digests. A prior package, r6, is verified as rollback-ready.

On the morning of promotion, the data office withdraws source B because its purpose record was attached to the wrong collection. A direct registry query against r7 will not find source B. A transitive lineage query will: source B contributed to prepared set P3, P3 fed adapter A7, A7 belongs to bundle B7, and B7 is the subject of release r7. The same query may find r5 and an experimental branch. The incident team receives an impact set rather than a repository-wide guess.

Next, the platform team asks whether a full retrain is always required. The graph alone cannot answer. The typed relation and artefact semantics matter. If source B’s rows were included in training, removing them requires a new prepared set and retraining. If source B appeared only in an unused candidate pool, the release may need no rebuild. If it appeared only in evaluation, the model can remain byte-identical while its evidence becomes invalid. Lineage finds affected descendants; policy decides the required intervention.

The evidence node also preserves denominator and exclusions. A score of 0.923 is uninterpretable unless the manifest resolves the 600 held-out cases, group-splitting rule, rubric version, cohort slices, failed cases and evaluator implementation. If three excluded document types later enter production scope, the old score remains historically true yet ceases to support the widened use. The intended-use edge is therefore as important as the metric value.

Recovery uses the same composition. Rolling back from r7 to r6 means restoring r6 weights, tokenizer, calibration, image and routing policy, then verifying the observed endpoint state. Replacing only the adapter could create a combination that never passed evaluation. The rollback package is a release closure in its own right, not a pointer to a favourite checkpoint.

Valid time and decision time can diverge Four time lanes show data permission, evaluation applicability, approval validity and release. The data permission ends before the planned release, while the evaluation and approval remain nominally current. A vertical release line therefore intersects an invalid data lane and promotion is blocked. Release decisions need a temporal intersection 26 Aug28 Aug29 Aug30 Aug Data permission Evaluation applies Approval applies Rollback ready valid for intended use expired bundle A and rubric v4 approval window r6 package verified planned release: blocked withdrawal precedes promotion
Figure 6. A release requires a common valid-time intersection. Dates and intervals are synthetic. Evidence and approval can remain unexpired while an upstream right or purpose condition has already failed.

The negative control: when a registry snapshot is enough

A serious mechanism needs a case in which it adds little. Imagine a deterministic teaching model trained from a tiny public dataset, built locally from a fully pinned script, used only in an isolated classroom, with no personal data, external licence conditions, consequential decisions or long-lived service. A repository commit, environment lockfile, dataset hash and test output may answer every plausible recovery question.

This negative control weakens any claim that all model work needs an elaborate enterprise lineage service. The thesis survives in narrower form: the release record must close over all behaviour-affecting and decision-bearing inputs. In a simple sandbox, that closure is small and ordinary version control may implement it. Governance complexity should follow causal and consequence complexity.

Explicit failure boundary: acquired closed models When an API provider does not disclose training ancestry, a consumer cannot manufacture full lineage. Govern the provider, served model identifier, contract, change notice, runtime configuration, local evaluation, intended use and fallback. Record upstream training provenance as unknown and limit consequences accordingly. Do not relabel supplier opacity as complete provenance.
Part IV

Operating the supply chain

A graph becomes governance only when it changes who can do what. Training jobs should write artefacts and attestations, but they should not possess production promotion authority. Evaluation services should attest to exact subjects and configurations. Release automation should verify closure against policy. A separate promotion identity should act only after all hard gates pass. This keeps a stochastic training process outside the deterministic authority kernel.

The operating sequence has four trust transitions. Ingestion resolves source identity and use conditions before data becomes eligible for preparation. Training consumes only resolved inputs and emits an attested candidate, never a mutable “latest” output. Evaluation retrieves the candidate by digest and writes evidence against that subject. Promotion reconstructs the transitive closure, evaluates current policy, obtains or verifies the approval receipt, and then writes the release root to the production registry.

Each transition should fail closed on ambiguity. If the artefact store times out after accepting a write, the job must read back the digest instead of creating a duplicate identity. If an evaluation cannot resolve its rubric revision, it should produce an incomplete evidence record rather than inherit a default. If promotion cannot determine whether an approval was superseded, the outcome is “not admitted”, not “probably approved”. These are ordinary distributed-systems controls applied to model governance.

The root manifest need not duplicate every standard record. It can reference a SLSA build attestation for the trainer output, SPDX or CycloneDX descriptions for components, dataset documentation, in-toto test results and a local decision predicate. What matters is stable subject binding and a verifier that understands each accepted predicate type. This allows interoperable evidence without reducing the release to the least expressive common schema. It also permits gradual adoption: begin with a small internal manifest, then replace home-grown claims with standard attestations while preserving the closure policy.

NIST SP 800-218A recommends collecting, safeguarding and maintaining provenance for all components of each release, including model derivatives, libraries, frameworks and pipelines. It also calls for known and unknown provenance of training, testing, fine-tuning and alignment data to be recorded.1 NIST’s generative-AI profile separately identifies non-transparent integration of upstream components as a value-chain risk.12 The operational lesson is to capture lineage during execution, not reconstruct it when a release review or incident begins.

Immutable nodes, mutable judgements

Artefact nodes should be content-addressed and immutable. Corrections create new nodes and supersession edges. Judgements are also versioned records, not editable fields on a model card. A data-use decision can be superseded. An evaluation rubric can be replaced. An approval can expire or be withdrawn. Retaining the old node preserves what was knowable at the earlier decision time while the current policy follows the superseding edge.

This design supports two distinct questions. “What did we release on that date?” follows the historical manifest and its then-applicable records. “Can we release the same bytes now?” re-evaluates current rights, policy and evidence. The answers may differ without contradiction.

Immutability does not imply indiscriminate replication. A manifest should contain digests, classifications, stable identifiers and controlled references, not raw personal records or sensitive prompts. Detailed evidence can remain in an entitled store while its digest proves what was reviewed. Retention policy may delete underlying material when lawful obligations require it, but the resulting tombstone should state what was removed, by which authority and how that limits future replay.

Change propagation is typed

A dependency graph should calculate a candidate impact radius, then a rule table should decide the response. Weight or tokenizer changes usually demand rebuild and re-evaluation. A rubric change may permit deterministic re-scoring when stored predictions and labels are sufficient, otherwise it demands a rerun. A signer rotation may require only signature-policy verification. A revoked training source can demand retraining, release withdrawal and incident review.

Test the mechanism through failure injection. Remove one transitive edge and expect closure failure. Replace a tokenizer while preserving the old evaluation and expect exact-subject failure. Backdate a rights expiry and expect temporal failure. Sign an approval with a valid identity that lacks the release role and expect authority failure. Mark rollback as unverified and expect recovery failure. A lineage platform that draws attractive graphs but passes these corruptions is an observability tool, not a release control.

Changed ancestor Rebuild Re-evaluate Re-approve Consider withdrawal Reason
Training rows removedYesYesYesIf already releasedWeights no longer derive from an admissible set.
Tokenizer or normaliserBundleYesYesOn material regressionThe effective input function changed.
Evaluation rubricNoRe-score or rerunYesIf the old decision failsThe meaning of “pass” changed.
Runtime libraryImageRisk-basedIf behaviour or control changesFor exploitable defectsExecution or security semantics may change.
Approver identityNoNoVerify delegated roleNormally noCustody changed, not model behaviour.
Data-use decision withdrawnIf training data affectedDepends on roleYesYesAuthority can fail with unchanged bytes.
Figure 7. Typed changes produce different recovery obligations. This is a practitioner decision matrix, not a universal compliance rule. Local policy should refine each cell by consequence, contract and system boundary.

Unknowns should narrow release authority

Unknown provenance should not be treated as an automatic universal ban, because acquired models and historical datasets often contain irreducible gaps. It should affect what the release may do. A model with opaque upstream data may be acceptable for reversible drafting with source verification, yet inappropriate for an irreversible eligibility decision. The manifest should make that constraint machine-readable so a downstream deployment cannot silently widen the use.

This produces a useful asymmetry. Known, verified lineage can expand the set of defensible uses only when evaluation and authority also support them. Unknown lineage can immediately narrow that set. Absence of evidence should reduce authority before it becomes an incident explanation.

Executable release manifest

The accompanying standard-library Python program implements the paper’s mechanism on synthetic data. It creates a positive release with fifteen reachable artefacts and a negative release in which the tokenizer changes, the evidence and approval remain bound to the old bundle, and the training-data permission expires before promotion.

The verifier checks referential integrity, artefact and graph digests, required artefact kinds, transitive reachability, valid-time conditions, known provenance for critical data, bundle composition, exact evaluation binding, authorised approval and rollback readiness. It deliberately simulates signer roles rather than real cryptography. A production implementation should use verifiable attestations and an organisational policy engine.

The negative case is intentionally more demanding than corrupting a file. It creates a legitimate new tokenizer and recomputes the bundle and manifest graph digests. Basic integrity therefore passes for the new bytes. The failure appears because the evaluation and approval still name the prior bundle, while the data-use condition has expired. This demonstrates the central claim in executable form: a cryptographically tidy candidate can remain an inadmissible release.

Assumptions and expected result SHA-256 identifies immutable records and graph state but does not establish truth, rights or fitness. The positive candidate passes. The negative candidate fails for stale evidence, stale approval and expired data authority.
PASS candidate-r7: release closure complete (15 artefacts) FAIL candidate-r8: 3 blocking issue(s) - approval:7: decision does not bind the exact model bundle - data:train-v3: rights or applicability expired before release - evaluation:7: evidence does not bind the exact model bundle

The key verification pattern is small enough to inspect:

assert digest(graph_material(manifest)) == manifest["manifest_digest"]
closure = reachable(manifest)
release_time = iso(release["release_time"])

for artefact_id in closure:
    artefact = artefacts[artefact_id]
    assert digest(artefact_material(artefact)) == artefact["digest"]
    assert provenance_is_acceptable(artefact)
    assert valid_at(artefact, release_time)

assert evaluation["subject_digest"] == bundle["digest"]
assert approval["candidate_digest"] == bundle["digest"]
assert approval["evidence_digest"] == evaluation_artifact["digest"]
assert approval["signer_role"] in release["authorised_roles"]
assert rollback["status"] == "verified-ready"

A full runnable copy is available as Inspect the implementation in this paper. The complete source is also included below so the article remains self-contained.

Open the complete runnable Python program
#!/usr/bin/env python3
"""Synthetic release-closure verifier for an AI model supply chain.

Assumptions:
- SHA-256 identifies immutable records but does not establish truth, rights, or fitness.
- Signatures are represented by signer roles; production use must verify real signatures.
- Dates are ISO-8601 UTC timestamps.
- The example uses synthetic artefacts and no private endpoints or data.
"""
from __future__ import annotations

import copy
import hashlib
import json
from datetime import datetime, timezone
from typing import Any


Manifest = dict[str, Any]
Artefact = dict[str, Any]
Edge = tuple[str, str, str]


def canonical_bytes(value: Any) -> bytes:
    """Return a deterministic JSON encoding for hashing."""
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")


def digest(value: Any) -> str:
    return hashlib.sha256(canonical_bytes(value)).hexdigest()


def iso(value: str) -> datetime:
    parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)


def artefact_material(artefact: Artefact) -> dict[str, Any]:
    """Everything except the digest is part of the immutable claim."""
    return {key: value for key, value in artefact.items() if key != "digest"}


def refresh_digest(artefact: Artefact) -> None:
    artefact["digest"] = digest(artefact_material(artefact))


def node(kind: str, payload: dict[str, Any], **metadata: Any) -> Artefact:
    artefact: Artefact = {"kind": kind, "payload": payload, **metadata}
    refresh_digest(artefact)
    return artefact


def graph_material(manifest: Manifest) -> dict[str, Any]:
    """Bind the root, typed edges, and every artefact record into one graph digest."""
    return {
        "root": manifest["root"],
        "artefacts": {
            artefact_id: artefact["digest"]
            for artefact_id, artefact in sorted(manifest["artefacts"].items())
        },
        "edges": sorted([list(edge) for edge in manifest["edges"]]),
    }


def seal_manifest(manifest: Manifest) -> Manifest:
    manifest["manifest_digest"] = digest(graph_material(manifest))
    return manifest


def make_positive_manifest() -> Manifest:
    release_time = "2026-08-30T09:00:00Z"
    artefacts: dict[str, Artefact] = {
        "data:train-v3": node(
            "training_data",
            {"source": "synthetic-labelled-cases", "rows": 2400, "purpose": "document-triage"},
            critical=True,
            provenance_known=True,
            valid_from="2026-01-01T00:00:00Z",
            valid_to="2027-01-01T00:00:00Z",
        ),
        "data:eval-v2": node(
            "evaluation_data",
            {"source": "synthetic-held-out-cases", "rows": 600, "group_split": True},
            critical=True,
            provenance_known=True,
            valid_from="2026-01-01T00:00:00Z",
            valid_to="2027-01-01T00:00:00Z",
        ),
        "code:trainer-a31": node("training_code", {"git_commit": "a31c9e7", "entry": "train.py"}),
        "config:recipe-v4": node(
            "training_recipe",
            {"seed": 17, "epochs": 3, "learning_rate": 0.0002, "base_model": "base:1"},
        ),
        "base:1": node("base_weights", {"name": "synthetic-base", "revision": "1"}),
        "adapter:7": node("adapter_weights", {"format": "safetensors", "rank": 16}),
        "tokenizer:1": node("tokenizer", {"vocabulary": "synthetic-v1", "normaliser": "nfc"}),
        "runtime:5": node("runtime_image", {"image": "triage-runtime", "revision": "5"}),
        "calibration:3": node("calibration", {"method": "temperature", "temperature": 1.41}),
        "rubric:4": node(
            "evaluation_rubric",
            {"intended_use": "document-triage", "minimum_macro_f1": 0.90, "maximum_critical_fn": 2},
        ),
        "rollback:6": node("rollback_bundle", {"release": "r6", "status": "verified-ready"}),
    }

    bundle_payload = {
        "components": {
            "weights": artefacts["adapter:7"]["digest"],
            "base": artefacts["base:1"]["digest"],
            "tokenizer": artefacts["tokenizer:1"]["digest"],
            "runtime": artefacts["runtime:5"]["digest"],
            "calibration": artefacts["calibration:3"]["digest"],
        }
    }
    artefacts["bundle:7"] = node("model_bundle", bundle_payload)

    evaluation_payload = {
        "subject_digest": artefacts["bundle:7"]["digest"],
        "intended_use": "document-triage",
        "rubric_digest": artefacts["rubric:4"]["digest"],
        "result": "PASS",
        "macro_f1": 0.923,
        "critical_false_negatives": 1,
        "evaluated_at": "2026-08-28T14:00:00Z",
    }
    artefacts["evaluation:7"] = node("evaluation_evidence", evaluation_payload)

    approval_payload = {
        "candidate_digest": artefacts["bundle:7"]["digest"],
        "evidence_digest": artefacts["evaluation:7"]["digest"],
        "decision": "APPROVE",
        "signer_role": "model-release-authority",
        "valid_until": "2026-09-15T00:00:00Z",
    }
    artefacts["approval:7"] = node("release_decision", approval_payload)

    release_payload = {
        "release_id": "candidate-r7",
        "release_time": release_time,
        "intended_use": "document-triage",
        "required_kinds": [
            "training_data",
            "evaluation_data",
            "training_code",
            "training_recipe",
            "base_weights",
            "adapter_weights",
            "tokenizer",
            "runtime_image",
            "calibration",
            "evaluation_rubric",
            "evaluation_evidence",
            "release_decision",
            "rollback_bundle",
        ],
        "authorised_roles": ["model-release-authority"],
    }
    artefacts["release:7"] = node("release", release_payload)

    edges: list[Edge] = [
        ("release:7", "packages", "bundle:7"),
        ("release:7", "is_supported_by", "evaluation:7"),
        ("release:7", "is_authorised_by", "approval:7"),
        ("release:7", "can_rollback_to", "rollback:6"),
        ("bundle:7", "contains", "adapter:7"),
        ("bundle:7", "contains", "base:1"),
        ("bundle:7", "contains", "tokenizer:1"),
        ("bundle:7", "contains", "runtime:5"),
        ("bundle:7", "contains", "calibration:3"),
        ("adapter:7", "was_trained_from", "data:train-v3"),
        ("adapter:7", "was_produced_by", "code:trainer-a31"),
        ("adapter:7", "used_recipe", "config:recipe-v4"),
        ("evaluation:7", "tested", "bundle:7"),
        ("evaluation:7", "used", "data:eval-v2"),
        ("evaluation:7", "used", "rubric:4"),
        ("approval:7", "considered", "evaluation:7"),
        ("approval:7", "approved", "bundle:7"),
    ]
    return seal_manifest({"root": "release:7", "artefacts": artefacts, "edges": edges})


def targets(manifest: Manifest, source: str, relation: str) -> list[str]:
    return [target for edge_source, edge_relation, target in manifest["edges"]
            if edge_source == source and edge_relation == relation]


def reachable(manifest: Manifest) -> set[str]:
    outgoing: dict[str, list[str]] = {}
    for source, _relation, target in manifest["edges"]:
        outgoing.setdefault(source, []).append(target)
    seen: set[str] = set()
    stack = [manifest["root"]]
    while stack:
        current = stack.pop()
        if current in seen:
            continue
        seen.add(current)
        stack.extend(outgoing.get(current, []))
    return seen


def one_target(manifest: Manifest, source: str, relation: str, failures: list[str]) -> str | None:
    matches = targets(manifest, source, relation)
    if len(matches) != 1:
        failures.append(f"{source}: expected one {relation!r} edge, found {len(matches)}")
        return None
    return matches[0]


def verify_release(manifest: Manifest) -> list[str]:
    failures: list[str] = []
    artefacts: dict[str, Artefact] = manifest["artefacts"]
    root_id = manifest["root"]

    if manifest.get("manifest_digest") != digest(graph_material(manifest)):
        failures.append("manifest graph digest mismatch")

    if root_id not in artefacts or artefacts[root_id].get("kind") != "release":
        return sorted(set(failures + ["root release is missing or mistyped"]))

    for source, relation, target in manifest["edges"]:
        if source not in artefacts:
            failures.append(f"edge {relation!r} has missing source {source}")
        if target not in artefacts:
            failures.append(f"edge {relation!r} has missing target {target}")

    for artefact_id, artefact in artefacts.items():
        if digest(artefact_material(artefact)) != artefact.get("digest"):
            failures.append(f"{artefact_id}: artefact digest mismatch")

    closure = reachable(manifest)
    release = artefacts[root_id]["payload"]
    release_time = iso(release["release_time"])

    present_kinds = {artefacts[item]["kind"] for item in closure if item in artefacts}
    for required in release["required_kinds"]:
        if required not in present_kinds:
            failures.append(f"release closure lacks required kind {required}")

    for artefact_id in closure:
        if artefact_id not in artefacts:
            continue
        artefact = artefacts[artefact_id]
        if artefact.get("critical") and not artefact.get("provenance_known", False):
            failures.append(f"{artefact_id}: critical artefact has unknown provenance")
        valid_from = artefact.get("valid_from")
        valid_to = artefact.get("valid_to")
        if valid_from and release_time < iso(valid_from):
            failures.append(f"{artefact_id}: not yet valid at release time")
        if valid_to and release_time >= iso(valid_to):
            failures.append(f"{artefact_id}: rights or applicability expired before release")

    bundle_id = one_target(manifest, root_id, "packages", failures)
    evidence_id = one_target(manifest, root_id, "is_supported_by", failures)
    approval_id = one_target(manifest, root_id, "is_authorised_by", failures)
    rollback_id = one_target(manifest, root_id, "can_rollback_to", failures)
    if not all([bundle_id, evidence_id, approval_id, rollback_id]):
        return sorted(set(failures))
    if not all(item in artefacts for item in [bundle_id, evidence_id, approval_id, rollback_id]):
        return sorted(set(failures))

    bundle = artefacts[bundle_id]
    component_ids = targets(manifest, bundle_id, "contains")
    expected_components = {artefacts[item]["digest"] for item in component_ids if item in artefacts}
    declared_components = set(bundle["payload"]["components"].values())
    if expected_components != declared_components:
        failures.append(f"{bundle_id}: declared components do not match typed contains edges")

    evidence = artefacts[evidence_id]["payload"]
    if evidence["subject_digest"] != bundle["digest"]:
        failures.append(f"{evidence_id}: evidence does not bind the exact model bundle")
    if evidence["intended_use"] != release["intended_use"]:
        failures.append(f"{evidence_id}: evidence was produced for a different intended use")
    if evidence["result"] != "PASS":
        failures.append(f"{evidence_id}: evaluation result is not PASS")

    approval = artefacts[approval_id]["payload"]
    if approval["candidate_digest"] != bundle["digest"]:
        failures.append(f"{approval_id}: decision does not bind the exact model bundle")
    if approval["evidence_digest"] != artefacts[evidence_id]["digest"]:
        failures.append(f"{approval_id}: decision does not bind the exact evidence")
    if approval["decision"] != "APPROVE":
        failures.append(f"{approval_id}: decision is not APPROVE")
    if approval["signer_role"] not in release["authorised_roles"]:
        failures.append(f"{approval_id}: signer role is not authorised")
    if release_time >= iso(approval["valid_until"]):
        failures.append(f"{approval_id}: approval expired before release")

    rollback = artefacts[rollback_id]["payload"]
    if rollback.get("status") != "verified-ready":
        failures.append(f"{rollback_id}: rollback package has not been verified")

    return sorted(set(failures))


def make_negative_manifest(positive: Manifest) -> Manifest:
    broken = copy.deepcopy(positive)
    artefacts: dict[str, Artefact] = broken["artefacts"]

    # Build a legitimate new tokenizer, bundle, and release root.
    artefacts["tokenizer:2"] = node("tokenizer", {"vocabulary": "synthetic-v2", "normaliser": "nfkc"})
    bundle_payload = copy.deepcopy(artefacts["bundle:7"]["payload"])
    bundle_payload["components"]["tokenizer"] = artefacts["tokenizer:2"]["digest"]
    artefacts["bundle:8"] = node("model_bundle", bundle_payload)

    release_payload = copy.deepcopy(artefacts["release:7"]["payload"])
    release_payload["release_id"] = "candidate-r8"
    artefacts["release:8"] = node("release", release_payload)
    broken["root"] = "release:8"

    new_edges: list[Edge] = []
    for source, relation, target in broken["edges"]:
        if source == "release:7":
            source = "release:8"
            if relation == "packages" and target == "bundle:7":
                target = "bundle:8"
        elif source == "bundle:7":
            source = "bundle:8"
            if relation == "contains" and target == "tokenizer:1":
                target = "tokenizer:2"
        new_edges.append((source, relation, target))
    broken["edges"] = new_edges

    # The data-use permission expires before promotion, and the record is re-digested.
    artefacts["data:train-v3"]["valid_to"] = "2026-08-29T00:00:00Z"
    refresh_digest(artefacts["data:train-v3"])

    # Evaluation and approval deliberately remain bound to bundle:7. The new graph is
    # resealed, so integrity passes while evidence, authority, and time still fail.
    return seal_manifest(broken)


def print_result(label: str, manifest: Manifest) -> None:
    failures = verify_release(manifest)
    if not failures:
        closure_size = len(reachable(manifest))
        print(f"PASS {label}: release closure complete ({closure_size} artefacts)")
        return
    print(f"FAIL {label}: {len(failures)} blocking issue(s)")
    for failure in failures:
        print(f"  - {failure}")


if __name__ == "__main__":
    positive = make_positive_manifest()
    negative = make_negative_manifest(positive)
    print_result("candidate-r7", positive)
    print_result("candidate-r8", negative)
How to adapt the manifest to a real platform

Replace synthetic payload digests with digests issued by the artefact store. Emit SLSA or in-toto attestations from trusted build and evaluation services. Store data-use and approval decisions as signed predicates with effective times. Resolve organisational roles through workload identity rather than user-supplied strings. Evaluate release policy in the promotion service, and give training jobs no credential that can write to the production registry.

Do not put confidential records into a widely replicated manifest. Record stable identifiers, digests, classification and controlled references. The evidence store can enforce entitlement while the manifest proves which evidence was considered.

How the claim changes under pressureChart · state
stateDiagram-v2
  [*] --> Claim
  state "Thought experiment one: the<br/>tokenizer transplant" as Claim
  state "From inventory to ancestry" as Test
  state "Typed edges make lineage<br/>executable" as Revision
  Claim --> Test: expose mechanism
  Test --> Decision: evidence survives
  Test --> Revision: rival survives
  Revision --> Claim: narrow or rebuild
  Decision --> [*]
The revision loop begins with Thought experiment one, the tokenizer… and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Promotion decision instrument

Use the following hard-gate record for each candidate. A “no” is not averaged into a score. It blocks promotion or requires a formally authorised exception whose scope and expiry become part of the manifest.

The release closure gate Six nested gates check identity, closure, authority, time, evidence and recovery. A coral side path shows that failure at any gate leads to reject or bounded exception. A teal path reaches promotion only after all six pass. Release closure gate 1. Identityimmutable digest and trusted attestation 2. Closureall required ancestors and typed edges present 3. Authorityrights, purpose and authorised decision role 4. Timevalidity, expiry and supersession at release 5. Evidenceexact bundle, use, rubric and thresholds 6. Recoveryverified rollback and withdrawal path promote Any failed gate reject, repair, or bounded exception
Figure 8. Promotion is conjunctive, not scored. Each layer is a hard predicate. The figure is a decision instrument derived from the release-closure mechanism; it contains no measured quantities.
IdentityCan every artefact and attestation be resolved by immutable digest, with the expected producer identity?
ClosureDoes transitive traversal reach every required data, code, configuration, model, evidence, decision and recovery kind?
AuthorityAre source rights, purpose and release roles applicable to this use, with unknowns explicitly bounded?
TimeAre all rights, evaluations, policies and approvals effective and unsuperseded at the proposed release instant?
EvidenceDoes the evaluation bind the exact bundle, intended use, rubric, cohorts, exclusions and acceptance thresholds?
RecoveryCan the system withdraw or roll back the whole effective package without reviving expired authority?
Open hypothesis Teams that enforce exact evidence-to-bundle binding at promotion will detect more consequential release defects before deployment than teams that retain equivalent metadata only for retrospective audit. The claim needs controlled operational comparison, not assumption.

Compact glossary

Artefact
An immutable, content-addressed dataset, code, configuration, weight, runtime, evidence or decision record.
Attestation
A typed claim by an identified producer about an artefact or activity, bound to a subject digest.
Release closure
The transitive set of behaviour-affecting and decision-bearing ancestors required to admit a release.
Effective model bundle
The weights plus every runtime component and configuration that can alter observable behaviour.
Decision provenance
The evidence, policy, role, conditions and time that explain why promotion was permitted.
Impact radius
The descendant set potentially affected when an upstream artefact or judgement changes.

Promote the closure, not the checkpoint

A checkpoint is evidence that training produced bytes. It is not yet evidence that those bytes are the exact system evaluated for a permitted use, approved by the right authority, valid at the release time and recoverable as one package.

The architecture decision is therefore simple: make the release manifest the object of promotion. The registry may still store weights and tags, but the production gate should accept only an immutable root whose transitive closure passes identity, completeness, authority, time, evidence and recovery checks. A changed ancestor opens only the affected paths, while a failed hard gate blocks the release.

This changes lineage from an audit appendix into a control surface. It gives builders a reproducible ancestry, reviewers an exact subject, incident teams a computable impact radius and operators a complete rollback unit. Most importantly, it makes a modest claim: not that the model is universally safe or correct, but that this particular release has a traceable, testable and authorised reason to exist.

The evidence conversationChart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: The Friday release with one checksum
  M->>E: Thought experiment one: the tokenizer transplant
  E-->>M: compare rival explanations
  E->>D: Sources
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in Training Lineage as a Governed Supply Chain. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Sources

  1. NIST SP 800-218A, Secure Software Development Practices for Generative AI and Dual-Use Foundation Models. Official guidance on model lineage, release provenance and known or unknown data provenance.
  2. W3C PROV-DM. Official data model for entities, activities, agents and provenance relations.
  3. SLSA v1.2 provenance. Official specification for verifiable source and build ancestry.
  4. in-toto Statement v1. Official attestation layer binding a typed predicate to immutable subjects.
  5. in-toto test-result predicate and release predicate. Official schemas for test and release claims.
  6. SPDX Specification 3.0.1 profiles. Official Dataset, AI and Build profile conformance points.
  7. CycloneDX AI/ML-BOM. Official capability description for models, datasets, configurations and dependencies.
  8. Mitchell et al., Model Cards for Model Reporting. Primary paper on intended-use and performance documentation.
  9. Gebru et al., Datasheets for Datasets. Primary paper on dataset motivation, composition, collection and recommended uses.
  10. Sigstore signing overview. Official documentation for identity-bound signing and transparency logging.
  11. Longpre et al., A large-scale audit of dataset licensing and attribution in AI. Peer-reviewed audit of 1,858 datasets.
  12. NIST AI 600-1, Generative Artificial Intelligence Profile. Official profile identifying value-chain and component-integration risk.