Artifact knowledge and reproducibility

A managed artifact is a representation produced by a recorded activity, such as an editing proxy or render. PostProject derives what it knows about that artifact from the activity graph and the fingerprint snapshots captured on the activity’s input and output edges.

The knowledge state is one of:

  • current — comparable input and output fingerprints still match, and every managed upstream artifact is current;

  • stale — an input changed or a managed upstream artifact is not current;

  • indeterminate — the production lacks a snapshot or comparable fingerprint evidence;

  • diverged — the artifact’s own fingerprint differs from the output captured when the activity committed.

This state is computed, not stored. Evaluation is knowledge-only: it does not read media files, resolve locations, compute fingerprints, enqueue work, or change the production. A caller records a new fingerprint observation explicitly before evaluation can account for changed content.

Required dependency relationships are captured with an activity input. A changed dependency path or fingerprint can therefore make a generated artifact stale without classifying the referencing source itself as stale.

When several conditions apply at once, the most severe state wins, in the order the questions are asked here:

        flowchart TD
    own["Does the artifact's own fingerprint differ<br/>from the output captured at commit?"]
    own -- "yes" --> diverged(["diverged"])
    own -- "no" --> changed["Did an input or a required dependency change,<br/>or is a managed upstream artifact stale or diverged?"]
    changed -- "yes" --> stale(["stale"])
    changed -- "no" --> missing["Is evidence missing, is an upstream artifact indeterminate,<br/>or was a traversal bound reached?"]
    missing -- "yes" --> indeterminate(["indeterminate"])
    missing -- "no" --> current(["current"])
    

Explanations and bounds

Every non-current evaluation includes structured reasons. Depending on the condition, a reason identifies the activity, input or output edge, representation, fingerprint algorithm and version, captured and current byte values, upstream state, or traversal limit. Applications should present these reasons instead of reducing the answer to an unexplained warning.

Transitive evaluation has explicit depth and representation-count bounds. If a bound is reached, the result is truncated and indeterminate rather than silently treating the unexplored graph as current.

To find stale artifacts across a production, or among the provenance descendants of one changed source, use the paginated stale-artifact query instead of evaluating every output individually.

Activities created before edge snapshots were introduced retain absent snapshots after migration. Their outputs are indeterminate until regenerated; PostProject never fabricates historical evidence from current values.

Explain an affected render

The runnable scene-and-texture example uses a small JSON fixture format. It extracts the scene’s exact texture reference, then records a render’s producing activity separately. Observing changed texture content makes the render stale, with reasons tracing the change through the scene. The example demonstrates recorded knowledge; it does not execute a scene renderer.

Three independent dimensions

Artifact knowledge does not include file availability or job state:

Dimension

Answers

Values

Knowledge

Does the artifact still match what produced it?

current · stale · indeterminate · diverged

Availability

Can its content be reached on this machine?

online · partial · offline · ambiguous · error

Work

Is work on it wanted or did it fail?

pending · failed · none

For example, a stale proxy may still be online, while a current render may be offline on this machine. Read resolution results and job state separately when the interface needs all three answers.

Recorded recipe completeness

The existing reproducibility operation checks whether the production records:

  • one unambiguous producing activity;

  • that activity’s kind and tool identity;

  • typed parameter metadata on the activity; and

  • every referenced input representation.

It returns every missing condition, not only a boolean. A positive report says the production has enough recorded knowledge to describe the work; it does not promise that the tool, input files, codecs, or execution environment are currently available, and it never runs the activity.

The C and C++ APIs expose owned evaluation and reproducibility results, Python provides immutable value objects through Production.evaluate_artifact() and Production.artifact_reproducibility(), and the CLI exposes artifact evaluate and artifact reproducibility with structured JSON output.

Across public surfaces

Evaluation and reproducibility are separate read-only questions. This example reports both and preserves the structured reasons or missing conditions:

static pp_error_code_t inspect_artifact(const pp_production_t *production,
                                        const pp_representation_id_t *artifact_id,
                                        pp_error_t **error) {
  pp_artifact_evaluation_t *evaluation = NULL;
  pp_artifact_reproducibility_t *report = NULL;
  pp_representation_id_t evaluated_id;
  pp_artifact_knowledge_state_t state = 0;
  uint32_t visited = 0;
  uint8_t truncated = 0;
  uint64_t reason_count = 0;
  pp_error_code_t status = pp_production_evaluate_artifact(
      production, *artifact_id, UINT32_C(64), UINT32_C(1000), &evaluation,
      error);
  if (status == PP_OK) {
    status = pp_artifact_evaluation_get(
        evaluation, &evaluated_id, &state, &visited, &truncated, &reason_count,
        error);
  }
  if (status == PP_OK) {
    printf("artifact state: %u, reasons: %llu\n", state,
           (unsigned long long)reason_count);
    status = pp_production_artifact_reproducibility(
        production, *artifact_id, &report, error);
  }
  if (status == PP_OK) {
    uint8_t reproducible = 0;
    uint8_t has_activity = 0;
    pp_activity_id_t activity_id;
    const char *activity_kind = NULL;
    uint64_t issue_count = 0;
    status = pp_artifact_reproducibility_get(
        report, &evaluated_id, &reproducible, &has_activity, &activity_id,
        &activity_kind, &issue_count, error);
    if (status == PP_OK) {
      printf("recorded recipe complete: %u, missing conditions: %llu\n", reproducible,
             (unsigned long long)issue_count);
    }
  }

  pp_artifact_reproducibility_release(report);
  pp_artifact_evaluation_release(evaluation);
  return status;
}
void inspect_artifact(const postproject::Production &production,
                      const postproject::RepresentationId &artifact_id) {
  const auto evaluation = production.evaluateArtifact(artifact_id, 64, 1000).value();
  std::cout << "artifact state: " << static_cast<std::uint32_t>(evaluation.state)
            << '\n';
  for (const auto &reason : evaluation.reasons) {
    std::cout << "reason: " << static_cast<std::uint32_t>(reason.kind()) << '\n';
  }

  const auto reproducibility = production.artifactReproducibility(artifact_id).value();
  std::cout << "recorded recipe complete: " << reproducibility.reproducible
            << ", missing conditions: " << reproducibility.issues.size()
            << '\n';
}
def inspect_artifact(production: Production, artifact_id: RepresentationId) -> None:
    evaluation = production.evaluate_artifact(
        artifact_id, max_depth=64, max_representations=1000
    )
    print(f"artifact state: {evaluation.state.name}")
    for reason in evaluation.reasons:
        print(f"reason: {reason.kind.name}")

    reproducibility = production.artifact_reproducibility(artifact_id)
    print(
        f"recorded recipe complete: {reproducibility.reproducible}, "
        f"missing conditions: {len(reproducibility.issues)}"
    )
fn inspect_artifact(production: &SqliteProduction, artifact_id: RepresentationId) -> Result<()> {
    let evaluation =
        production.evaluate_artifact(artifact_id, ArtifactEvaluationLimits::new(64, 1_000)?)?;
    println!("artifact state: {:?}", evaluation.state());
    for reason in evaluation.reasons() {
        println!("reason: {reason:?}");
    }

    let reproducibility = production.artifact_reproducibility(artifact_id)?;
    println!(
        "recorded recipe complete: {}, missing conditions: {}",
        reproducibility.is_reproducible(),
        reproducibility.issues().len()
    );
    Ok(())
}
postproject --json artifact evaluate production.pproj "$SEQUENCE_ID" \
  --max-depth 64 --max-representations 1000 |
  jq '{state, reasons, visited_representations, truncated}'
postproject --json artifact reproducibility production.pproj "$SEQUENCE_ID" |
  jq '{reproducible, issues}'

Staleness begins with an explicitly recorded change. After a new fingerprint observation of an input, the artifact evaluates as stale with reasons that name the changed input:

static pp_error_code_t explain_stale_proxy(
    pp_production_t *production, const pp_resource_id_t *original_resource,
    const pp_representation_id_t *original_id, const char *original_path,
    const pp_representation_id_t *proxy_id, pp_artifact_knowledge_state_t *state,
    uint32_t *out_reason_kinds, uint32_t *out_issue_kinds, pp_error_t **error) {
  pp_transaction_t *transaction = NULL;
  pp_read_session_t *view = NULL;
  pp_artifact_evaluation_t *evaluation = NULL;
  pp_artifact_reproducibility_t *report = NULL;
  pp_representation_id_t evaluated_id;
  uint32_t visited = 0;
  uint8_t truncated = 0;
  uint64_t reason_count = 0;
  uint8_t value[8];

  /* Record what the host observes now: the original's bytes changed. */
  pp_error_code_t status =
      pp_production_read_session(production, &view, error);
  if (status == PP_OK) {
    status = pp_read_session_begin_edit(view, NULL, &transaction, error);
  }
  pp_read_session_release(view);
  view = NULL;
  if (status == PP_OK && !host_fingerprint(original_path, value)) {
    status = PP_ERROR_IO;
  }
  if (status == PP_OK) {
    status = pp_transaction_record_resource_fingerprint(
        transaction, *original_resource, HOST_RESOURCE_ALGORITHM, 1, value,
        sizeof value, error);
  }
  if (status == PP_OK) {
    status = pp_transaction_record_representation_fingerprint(
        transaction, *original_id, HOST_REPRESENTATION_ALGORITHM, 1, value,
        sizeof value, error);
  }
  if (status == PP_OK) {
    pp_commit_receipt_t commit_receipt;
    status = pp_transaction_commit(transaction, &commit_receipt, error);
  }
  if (status == PP_OK) {
    status = pp_production_evaluate_artifact(
        production, *proxy_id, UINT32_C(64), UINT32_C(1000), &evaluation, error);
  }
  if (status == PP_OK) {
    status =
        pp_artifact_evaluation_get(evaluation, &evaluated_id, state, &visited,
                                   &truncated, &reason_count, error);
  }
  for (uint64_t i = 0; status == PP_OK && i < reason_count; ++i) {
    pp_artifact_reason_kind_t kind = 0;
    status = pp_artifact_evaluation_get_reason_kind(evaluation, i, &kind, error);
    pp_artifact_reason_t reason = {0};
    if (status == PP_OK) {
      status = pp_artifact_evaluation_get_reason(evaluation, i, kind, &reason, error);
    }
    if (status == PP_OK) {
      *out_reason_kinds |= reason.kind < 32 ? UINT32_C(1) << reason.kind : 0;
      printf("reason %u\n", kind);
      if (kind == PP_ARTIFACT_REASON_FINGERPRINT_CHANGED ||
          kind == PP_ARTIFACT_REASON_FINGERPRINT_EVIDENCE_MISSING) {
        printf("%s edge: %s v%u, snapshot %llu bytes, now %llu\n",
             reason.edge_kind == PP_ARTIFACT_EDGE_INPUT ? "input" : "output",
             reason.fingerprint_algorithm != NULL ? reason.fingerprint_algorithm
                                                  : "-",
             reason.fingerprint_version,
             (unsigned long long)reason.snapshot_value_length,
             (unsigned long long)reason.current_value_length);
      }
    }
  }
  if (status == PP_OK) {
    status = pp_production_artifact_reproducibility(production, *proxy_id,
                                                    &report, error);
  }
  if (status == PP_OK) {
    uint8_t reproducible = 0;
    uint8_t has_activity = 0;
    pp_activity_id_t activity_id = {0};
    const char *activity_kind;
    uint64_t issue_count = 0;
    status = pp_artifact_reproducibility_get(
        report, &evaluated_id, &reproducible, &has_activity, &activity_id,
        &activity_kind, &issue_count, error);
    /* Issues name what a regeneration would still be missing. */
    for (uint64_t i = 0; status == PP_OK && i < issue_count; ++i) {
      pp_artifact_reproducibility_issue_kind_t kind = 0;
      status = pp_artifact_reproducibility_get_issue_kind(report, i, &kind, error);
      pp_artifact_reproducibility_issue_t issue = {0};
      if (status == PP_OK) {
        status = pp_artifact_reproducibility_get_issue(report, i, kind, &issue, error);
      }
      if (status == PP_OK) {
        *out_issue_kinds |= issue.kind < 32 ? UINT32_C(1) << issue.kind : 0;
        printf("reproducibility issue %u\n", issue.kind);
      }
    }
  }

  pp_artifact_reproducibility_release(report);
  pp_artifact_evaluation_release(evaluation);
  pp_transaction_release(transaction);
  return status;
}
postproject::ArtifactEvaluation evaluate_after_change(
    postproject::Production &production,
    const postproject::Representation &original,
    const postproject::Fingerprint &new_resource_fingerprint,
    const postproject::Fingerprint &new_representation_fingerprint,
    const postproject::RepresentationId &proxy_id) {
  // Record what the host observed after the original was re-exported.
  auto transaction = production.readSession().value().edit().value();
  transaction.recordResourceFingerprint(original.resources.front().id,
                                        new_resource_fingerprint).value();
  transaction.recordRepresentationFingerprint(original.id,
                                              new_representation_fingerprint).value();
  transaction.commit().value();

  const auto evaluation = production.evaluateArtifact(proxy_id).value();
  if (evaluation.state == postproject::ArtifactKnowledgeState::stale) {
    for (const auto &reason : evaluation.reasons) {
      std::cout << "stale because " << static_cast<std::uint32_t>(reason.kind())
                << '\n';
      if (const auto *changed = std::get_if<postproject::ArtifactFingerprintChanged>(&reason.value())) {
        std::cout << changed->fingerprint_algorithm << " on "
                  << changed->representation_id.toString().value() << '\n';
      }
    }
  }

  // Reproducibility says whether the recorded activity suffices to redo it.
  const auto reproducibility = production.artifactReproducibility(proxy_id).value();
  for (const auto &issue : reproducibility.issues) {
    std::cout << "cannot reproduce: " << static_cast<std::uint32_t>(
        postproject::artifactReproducibilityIssueKind(issue).value())
              << '\n';
    if (const auto *ambiguous = std::get_if<postproject::ReproducibilityProducerAmbiguous>(&issue)) {
      std::cout << ambiguous->activityCount() << " recorded producers\n";
    }
  }
  return evaluation;
}
def record_fingerprints(
    production: Production, representation: Representation, path: Path
) -> None:
    # Observations in the caller's own fingerprint domain.
    with production.read_session() as view, view.edit() as transaction:
        (resource,) = view.representation(representation.id).resources
        digest = hashlib.sha256(path.read_bytes()).digest()
        transaction.record_resource_fingerprint(
            resource.id, Fingerprint("example-sha256", 1, digest)
        )
        transaction.record_representation_fingerprint(
            representation.id,
            Fingerprint(
                "example-sha256-representation", 1, hashlib.sha256(digest).digest()
            ),
        )
        transaction.commit()


def explain_proxy(
    production: Production, proxy_id: RepresentationId
) -> tuple[ArtifactEvaluation, ArtifactReproducibility]:
    evaluation = production.evaluate_artifact(
        proxy_id, max_depth=64, max_representations=1000
    )
    print(f"proxy is {evaluation.state.name}")
    for reason in evaluation.reasons:
        print(f"  {reason.kind.name}")
        if isinstance(reason, ArtifactFingerprintChanged):
            print(
                f"  {reason.fingerprint_algorithm}: "
                f"{reason.snapshot_value.hex()[:16]} -> {reason.current_value.hex()[:16]}"
            )

    # Missing recorded recipe information; execution is a separate check.
    reproducibility = production.artifact_reproducibility(proxy_id)
    for issue in reproducibility.issues:
        print(f"  recorded recipe missing: {issue.kind.name}")
        if isinstance(issue, ReproducibilityToolMissing):
            print(f"  record the tool used by activity {issue.activity_id}")
    return evaluation, reproducibility
fn record_changed_file(
    production: &mut SqliteProduction,
    asset_id: AssetId,
    representation_id: RepresentationId,
    path: &Path,
) -> Result<postproject_core::CommitReceipt> {
    // Record the new resource observation first.
    let view = production.read_session()?;
    let resource_id = view.read().resources(representation_id)?[0].id();
    let observed = fingerprint_file(path)?;
    {
        let mut transaction = production.begin_edit(view.decision_base())?;
        transaction.record_resource_fingerprint(resource_id, observed.fingerprint())?;
        transaction.commit()?;
    }

    // Then recompute the representation fingerprint from current resources.
    let view = production.read_session()?;
    let representation = view
        .read()
        .representations(asset_id)?
        .into_iter()
        .find(|representation| representation.id() == representation_id)
        .expect("representation belongs to the asset");
    let aggregate = fingerprint_representation(
        representation.content_structure(),
        &view.read().resources(representation_id)?,
    )?;
    let mut transaction = production.begin_edit(view.decision_base())?;
    transaction.record_representation_fingerprint(representation_id, &aggregate)?;
    transaction.commit()
}

fn evaluate_proxy(
    production: &SqliteProduction,
    proxy_id: RepresentationId,
) -> Result<ArtifactKnowledgeState> {
    // Evaluation reads recorded knowledge only; it never touches media files.
    let evaluation =
        production.evaluate_artifact(proxy_id, ArtifactEvaluationLimits::new(64, 1_000)?)?;
    for reason in evaluation.reasons() {
        match reason {
            ArtifactKnowledgeReason::FingerprintChanged {
                representation_id,
                algorithm,
                ..
            } => println!("{representation_id} changed ({algorithm})"),
            other => println!("reason: {other:?}"),
        }
    }

    let reproducibility = production.artifact_reproducibility(proxy_id)?;
    for issue in reproducibility.issues() {
        match issue {
            ArtifactReproducibilityIssue::ParametersMissing { activity_id } => {
                println!("activity {activity_id} recorded no parameters");
            }
            other => println!("reproducibility issue: {other:?}"),
        }
    }
    Ok(evaluation.state())
}
printf 'regraded camera original' > rushes/A001.mov
DECISION_BASE=$(postproject --json inspect provenance.pproj | jq -r .decision_base)
postproject --decision-base "$DECISION_BASE" media fingerprint provenance.pproj "$RESOURCE_ID" rushes/A001.mov
# Evaluation compares the recorded input snapshots with current fingerprints.
postproject --json artifact evaluate provenance.pproj "$PROXY_ID" |
  jq -r '.state, (.reasons[] | "\(.kind) \(.edge) \(.representation_id)")'
postproject --json artifact reproducibility provenance.pproj "$PROXY_ID" |
  jq -r '"reproducible: \(.reproducible)", (.issues[] | .kind)'