Synthetic release controls¶
openmed.training.synthetic.governance decides whether a synthetic artifact, such as a generated dataset or a split derived from one, may be released. It checks disclosure, lineage, licenses, withdrawal, benchmark contamination, downstream regression, and subgroup quality in one local call, and it fails closed: a missing control is a failure, not a pass.
Every control consumes plain data. Lineage comes from the existing openmed.compliance.artifact_lineage manifests, and the other inputs are license records, generator and model digests, benchmark canary and overlap counts, and per-slice success counts. Decisions and reports carry digests, controlled identifiers, closed codes, counts, and rates. They never include record text, real records, license values, canary strings, or the names of suppressed groups, and validation makes no network calls.
These controls do not show that synthetic data is anonymous or that it is clinically equivalent to any real population. They are not legal advice.
Release blockers¶
validate_synthetic_release() returns a SyntheticReleaseDecision whose passed flag is true only when no control produced a finding. Each finding is a closed code with a count:
| Control | Blocking codes |
|---|---|
label | label_missing, intended_use_missing, intended_use_prohibited, generator_disclosure_missing, model_digest_missing |
lineage | lineage_missing, lineage_integrity_failed, lineage_artifact_absent, lineage_ancestor_missing, generator_not_in_lineage |
license | license_missing, license_conflict, license_not_permitted, license_redistribution_disallowed |
withdrawal | artifact_withdrawn |
contamination | benchmark_evidence_missing, benchmark_evidence_mismatch, benchmark_evidence_empty, required_benchmark_missing, benchmark_canary_detected, benchmark_overlap_exceeded |
regression | regression_evidence_missing, regression_evidence_insufficient, critical_regression |
subgroup | subgroup_report_missing, subgroup_evidence_insufficient, subgroup_disparity_exceeded |
assert_synthetic_release() raises SyntheticReleaseBlockedError with the codes only and attaches the decision. Malformed inputs raise SyntheticGovernanceError; error messages name fields or positions, never values.
Synthetic labels¶
SyntheticArtifactLabel is the machine-readable label. It binds the artifact's lineage node hash and content digest to:
content_kind:fully_syntheticorpartially_synthetic.intended_usesandprohibited_usesfrom the closedSyntheticUsevocabulary.clinical_decision_making,population_inference,real_data_substitution, andreidentificationare always prohibited and cannot be removed. An intended use that is also prohibited blocks release.generators: oneGeneratorDisclosureper generator with its identifier, version,method, code and configuration digest, lineage node, and model weight digests.statistical_model,neural_model, andhybridgenerators must disclose at least one model digest.
to_dict() always adds "synthetic": true and three fixed disclosures: anonymity_not_guaranteed, not_clinically_equivalent_to_real_population, and not_real_patient_data. from_mapping() and from_json() reject labels that are not marked synthetic, alter the disclosures, or carry unknown fields. label_digest commits to the canonical label.
Lineage and licenses¶
The artifact's node must be present in an ArtifactLineageManifest whose integrity check passes, every ancestor must be present, and each generator's lineage_node_hash must be an ancestor of the artifact.
Every node in the artifact's lineage closure, including the artifact itself, needs a LicenseRecord. A license passes when the permissive SPDX allowlist used by normalize_spdx_identifier() accepts it, or when it exactly matches (ignoring case) one of SyntheticReleasePolicy.permitted_data_licenses, which defaults to CC-BY-4.0 and CC0-1.0. Unknown identifiers, LicenseRef- values, expressions, and copyleft or share-alike licenses fail. Every record must also allow redistribution, and two different records for one node fail as a conflict. evidence_digest can point at a license and lineage manifest.
Withdrawal¶
propagate_withdrawal() takes a lineage manifest and WithdrawalNotice values. A notice carries a node hash, a closed WithdrawalReason, and an optional ISO effective date; free text is rejected. Propagation follows every parent reference, so a malformed edge can only widen a withdrawal. The returned WithdrawalEvidence lists each invalidated node with its type, its distance from the withdrawn node, the withdrawn hashes that reach it, and the reason codes. Notices for nodes outside the graph are counted as unresolved and still invalidate their own hash. Evidence is deterministic regardless of notice order and has an evidence_digest.
During validation, an artifact that is withdrawn directly or descends from a withdrawn node fails with artifact_withdrawn. Withdrawing a descendant, such as a model trained on the artifact, does not block the artifact itself.
Benchmark contamination¶
BenchmarkContaminationEvidence records, for one benchmark version, how many artifact records were checked, how many overlap the benchmark, how many canaries were scanned for, and how many were found. It names the artifact content digest it was measured on, and evidence for other content fails.
measure_benchmark_contamination() builds that evidence locally. Benchmark owners publish fingerprint_record() digests instead of records: text is NFKC-normalized, case-folded, and whitespace-collapsed before hashing, so the overlap is exact after normalization. Canary strings must have at least eight characters. Any canary hit blocks release, and overlap blocks release when its rate exceeds max_benchmark_overlap_rate (default 0.0). required_benchmarks lists benchmarks that must have evidence. Near-duplicate and memorization audits are separate.
Slices, suppression, and uncertainty¶
Downstream regression and subgroup quality use proportion metrics given as success and trial counts per slice. The whole set is the overall axis with the all group.
Slices with fewer trials than the declared minimum_cell_size (default 11, matching the common rule of not reporting cells of 1 to 10) are suppressed. When exactly one group on an axis is suppressed, the next smallest group on that axis is suppressed too, so the hidden group cannot be recovered by subtraction. Suppressed slices are counted, never named. Released slices report a coarse count band instead of exact counts:
| Band | Trials |
|---|---|
minimum_to_under_double | At least the minimum and less than twice it |
double_to_under_fourfold | At least twice and less than four times the minimum |
fourfold_or_more | At least four times the minimum |
build_subgroup_quality_report() reports each released slice with a Wilson score interval and, per axis, the gap between the highest and lowest released groups. gap_supported is true only when the two intervals do not overlap. Disparities are reported only, unless max_subgroup_gap is set; then a supported gap above it blocks release. A missing report, or one where every slice is suppressed, blocks release unless require_subgroup_report is false.
build_downstream_regression_report() compares the candidate trained with the artifact against a baseline for each task, metric, and slice, using Newcombe's hybrid score interval for the difference. A slice is critical when the candidate's point estimate drops by at least critical_regression_drop (default 0.05). Slices hidden only by complementary suppression still count toward critical regressions; slices below the minimum cell size cannot be assessed. Each task and metric needs an assessable overall slice, otherwise the evidence is insufficient.
Example¶
from openmed.compliance.artifact_lineage import (
ArtifactLineageNode,
build_artifact_lineage_manifest,
)
from openmed.training.synthetic.governance import (
DownstreamComparison,
GeneratorDisclosure,
LicenseRecord,
SubgroupMetric,
SyntheticArtifactLabel,
fingerprint_record,
measure_benchmark_contamination,
validate_synthetic_release,
)
generator = ArtifactLineageNode.create(
artifact_type="synthetic_generator", transformation="configure"
)
dataset = ArtifactLineageNode.create(
artifact_type="synthetic_dataset",
parents=[{"type": generator.artifact_type, "hash": generator.artifact_hash}],
transformation="generate",
)
lineage = build_artifact_lineage_manifest([generator, dataset])
content_digest = "sha256:" + "a" * 64 # digest of the released bytes
label = SyntheticArtifactLabel(
artifact_hash=dataset.artifact_hash,
content_digest=content_digest,
content_kind="fully_synthetic",
generators=[
GeneratorDisclosure(
generator_id="openmed.locale_phi",
generator_version="1.0.0",
method="template",
generator_digest="sha256:" + "b" * 64,
lineage_node_hash=generator.artifact_hash,
)
],
intended_uses=["model_training"],
)
evidence = measure_benchmark_contamination(
["Synthetic note: follow-up in two weeks."],
benchmark_id="openmed.privacy_corpus",
benchmark_digest="sha256:" + "c" * 64,
artifact_content_digest=content_digest,
benchmark_fingerprints=[fingerprint_record("Benchmark placeholder row.")],
canaries=["openmed-canary-7f3c9e21"],
)
decision = validate_synthetic_release(
label,
lineage,
licenses=[
LicenseRecord(node.artifact_hash, "Apache-2.0", redistribution_allowed=True)
for node in (generator, dataset)
],
benchmark_evidence=[evidence],
regression=[DownstreamComparison("pii_ner", "recall", 90, 100, 89, 100)],
subgroup_metrics=[SubgroupMetric("label_validity", "overall", "all", 95, 100)],
)
print(decision.passed, decision.codes)
Binding points¶
SyntheticReleaseDecision.to_capsule_check()returns thestatus/checked_count/evidence_digestshape of a release-capsule check, withpassorfailand a bare hexadecimal digest, so a capsule can commit to the decision. The label'scontent_digestis the artifact commitment a capsule records.SyntheticReleaseDecision.to_gate_check()returns anopenmed.eval.release_gates.GateChecknamedsynthetic_release_controlscarrying only finding codes and the evidence digest.LicenseRecord.evidence_digestcan hold the digest of a license and lineage manifest;GeneratorDisclosure.generator_digestandmodel_digestsare the same SHA-256 commitments a capsule pins for its generators.- Fidelity scores and split plans can feed
SubgroupMetricandDownstreamComparisoncounts, and a memorization finding can be recorded as amemorization_detectedwithdrawal.