model-sourcing
Vet the concrete third-party model a study will be built on — this repository, this revision, this checkpoint — not the architecture family. Records a model dossier (source and version pin, licence and the file it was read from, intended use, pretrained-weight provenance, model t
Install
npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/model-sourcing
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
git clone https://github.com/Aperivue/medsci-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aperivue/medsci-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Model-Sourcing Skill
Purpose
/architecture-zoo answers a literature question — which family of model suits this task. That
question has a stable answer. The next question does not: which concrete artifact do I run?
A repository, a revision, a checkpoint. That is a provenance question, and the two facts a
careful researcher usually checks are the two that cannot answer it.
The licence tells you whether you may use it. The citation count tells you whether others did. Neither tells you whether the number you are about to report means what you will say it means.
The failure this skill exists for is the quietest one in the lane. A method developed and tuned against a benchmark family gets evaluated by the next person on that same family, and the resulting figure reads like validation while sitting much closer to a training-set score. Nothing in the repository says so. The licence is clean, the paper is peer-reviewed and highly cited, the task matches, the code runs on your GPU. The conflict lives in the relationship between two facts that are documented in different places — what the model was developed on, and what you are about to evaluate it on — and it becomes visible only when they are written down side by side.
Writing them down side by side is what the dossier is for.
When to use
- You have a concrete candidate (a GitHub repo, a Hugging Face checkpoint, a paper's released weights) and are about to build a study on it.
- You are writing the Methods paragraph that says which model you used, and it has to survive a reviewer asking what it was trained on.
- You inherited a pipeline whose model came from somewhere nobody recorded.
When NOT to use
- Choosing an architecture family →
/architecture-zoo(archetypes and the task-to-architecture logic; deliberately not a live leaderboard). - Building the training repo →
/model-scaffold. Designing the validation study →/model-validation. Computing held-out metrics →/model-evaluation. - Documenting a model you built →
/model-card(Model Card + Datasheet). - Auditing your own dataset before modelling →
/profile-imaging. - Evaluating an LLM/multimodal system on a clinical task →
/mllm-eval(which owns pretraining-contamination of public benchmarks for that setting).
Workflow
Step 1 — write the dossier
One JSON file recording what is known, with unknowns left unstated rather than guessed:
{
"model": "OrganSeg-3D v2.5.1",
"source": {"kind": "github", "url": "...", "version": "v2.5.1", "commit": "abc1234"},
"licence": {"spdx": "Apache-2.0", "verified_from": "LICENSE at commit abc1234"},
"intended_use": "research",
"weights": {"pretrained": false},
"task": {"model": "3d_ct_organ_segmentation", "study": "3d_ct_organ_segmentation"},
"reported_validation": [{"dataset": "ExampleBench", "metric": "Dice", "source": "J Ex 2021"}],
"developed_on": ["ExampleBench"],
"evaluation_arms": [{"name": "external", "dataset": "OtherCohort-2026"}],
"hardware": {"claimed": "any CUDA GPU", "verified_on": "GTX 1080 Ti", "verified": true}
}
Each field is read from the artifact, not from memory: the licence from the LICENSE file at the
pinned commit (a README badge is not the licence), developed_on from the paper's own account of
where the method was built and tuned, hardware.verified only after it has actually run.
developed_on is the field people skip, and it is the one the gate needs. A method that won a
challenge was tuned on that challenge.
Step 2 — gate it
python3 scripts/check_model_provenance.py --dossier model_dossier.json \
--out qc/model_provenance.json --strict
Stdlib-only, network-free — no repository is fetched and no licence resolved online, so the audit re-runs anywhere the JSON travels. Verdicts:
| Verdict | Severity | Fires when |
|---|---|---|
BENCHMARK_PROVENANCE_CONFLICT |
Major | an evaluation arm uses a dataset the model was developed or tuned on |
EVAL_DATA_IN_TRAINING |
Major | an evaluation arm's dataset is inside the pretraining corpus |
LICENCE_UNSTATED |
Major | no licence recorded — which is not the same as a permissive one |
LICENCE_INCOMPATIBLE |
Major | a non-commercial / research-only licence under commercial or deployment intent |
WEIGHTS_PROVENANCE_UNKNOWN |
Major | pretrained weights whose training corpus is not stated |
TASK_MISMATCH |
Minor | the model's task is not the study's task |
NO_VERSION_PIN |
Minor | no commit, tag or revision |
VALIDATION_UNREPORTED |
Minor | no reported validation (dataset + metric + source) |
HARDWARE_UNVERIFIED |
Minor | hardware support claimed but never executed |
LICENCE_UNVERIFIED |
Minor | a licence is named but the file it was read from is not |
The gate flags a relationship, not a reputation. A dossier that declares
developed_on: ExampleBench passes cleanly as long as no evaluation arm uses ExampleBench.
Being developed on a benchmark is not a defect; evaluating on it and calling that independent is.
The clean fixture exists to prove exactly that distinction.
Dataset names are matched as token sequences with a small family-alias table, so
MSD Task09 Spleen matches MSD and MS Cohort 2026 does not. Matching never falls back to
substring search.
Step 3 — turn a Major into a study decision
A BENCHMARK_PROVENANCE_CONFLICT is rarely a reason to abandon the model — it is usually the
best-engineered option precisely because it was tuned hard. It is a reason to change what the
arm is claimed to establish:
- Report that arm as a demonstration that the pipeline runs end to end, not as evidence the method works.
- Put the evidential weight on an arm whose data post-dates the model, and say so with dates.
- State the conflict in Methods and Limitations rather than leaving a reviewer to find it.
An EVAL_DATA_IN_TRAINING is different in kind: that arm produces a training-set score and cannot
be reported as validation at all.
Carry the dossier forward — /model-validation (arm design), /model-evaluation (what each arm
may claim), /model-card (provenance section), /write-paper (Methods + Limitations).
Outputs
model_dossier.json— the provenance record downstream skills and the Methods section read.qc/model_provenance.json— deterministic audit with verdicts.- The arm-by-arm decision from Step 3, written into the study record.
Anti-Hallucination
- Never infer a fact the dossier does not state. An unstated licence is
LICENCE_UNSTATED, never "probably MIT"; an unstated pretraining corpus is a Major finding, never an assumption. - Never record a licence from a badge, a model card summary, or memory — only from the licence file at the pinned revision, and record which file that was.
- Never mark hardware verified without executing it. A support matrix and what the stack actually runs is a different claim; a CUDA capability the compiler accepts may still be refused by a compiler in the same stack.
- Never report an arm as independent validation when the gate flags a provenance conflict.
- If a provenance fact cannot be established from the artifact, leave it unstated and let the gate say so.
Deterministic gate
scripts/check_model_provenance.py — 10 verdicts by set arithmetic over the dossier, stdlib-only
and network-free. Reproducible challenge:
bash ${CLAUDE_SKILL_DIR}/scripts/check_model_provenance_challenge/verify.sh.
Regression suite: bash ${CLAUDE_SKILL_DIR}/tests/test_model_provenance.sh.
Boundaries
architecture-zoo (which family?) -> model-sourcing (this skill: which artifact, and what may its
numbers claim?) -> profile-imaging / preprocess-imaging -> model-scaffold -> model-validation
-> model-evaluation -> model-card -> write-paper
Files (medsci-skills)
-
scripts
-
check_model_provenance_challenge
-
expected
-
clean.txt 186 B
Model provenance — OrganSeg-3D v2.5.1 (synthetic example) (2 evaluation arm(s)) no findings — every provenance fact the gate checks is stated and consistent Major 0 Minor 0 -
defect.txt 1.8 KB
Model provenance — SynthSeg-3D (synthetic example) (2 evaluation arm(s)) [Major] BENCHMARK_PROVENANCE_CONFLICT an evaluation arm uses a dataset the model was developed or tuned on; that arm does not independently test the model, no matter how held-out the split is for you. Report it as a demonstration of the pipeline and put the evidence on an arm whose data post-dates the model - internal (ExampleBench Task03 Liver) <- developed on ExampleBench [Major] EVAL_DATA_IN_TRAINING an evaluation arm's dataset appears in the corpus the weights were trained on; the resulting number is a training-set score - internal (ExampleBench Task03 Liver) in the training corpus (ExampleBench) [Minor] LICENCE_UNVERIFIED licence 'cc-by-nc-4.0' is named but the file it was read from is not recorded; a badge in a README is not the licence [Major] LICENCE_INCOMPATIBLE licence 'cc-by-nc-4.0' restricts use to non-commercial or research purposes while the declared intended use is 'commercial' [Minor] TASK_MISMATCH the model's task ('2d_xray_classification') is not the study's task ('3d_ct_organ_segmentation'); a transfer step and its own validation are required, not just a checkpoint [Minor] NO_VERSION_PIN no commit, tag or revision recorded; 'we used <model>' does not identify what ran and cannot be reproduced once the default branch moves [Minor] VALIDATION_UNREPORTED no reported validation (dataset + metric + source) recorded for the model as published; there is then no prior expectation to compare your own numbers against [Minor] HARDWARE_UNVERIFIED hardware support is claimed ('any CUDA GPU') but never executed; support matrices and what the stack actually runs on are different statements Major 3 Minor 5 -
unstated.txt 652 B
Model provenance — AnonNet (synthetic example) (1 evaluation arm(s)) [Major] WEIGHTS_PROVENANCE_UNKNOWN pretrained weights are used and the corpus behind them is not stated, so it cannot be established whether your evaluation data is inside them [Major] LICENCE_UNSTATED no licence recorded. An unstated licence is not a permissive one — it is an unanswered question about whether the work may be used or redistributed at all [Minor] NO_VERSION_PIN no commit, tag or revision recorded; 'we used <model>' does not identify what ran and cannot be reproduced once the default branch moves Major 2 Minor 1
-
-
fixture
-
dossier_clean.json 839 B
{ "model": "OrganSeg-3D v2.5.1 (synthetic example)", "source": {"kind": "github", "url": "https://example.invalid/org/organseg3d", "version": "v2.5.1", "commit": "abc1234def5678"}, "licence": {"spdx": "Apache-2.0", "verified_from": "LICENSE at commit abc1234def5678"}, "intended_use": "research", "weights": {"pretrained": false}, "task": {"model": "3d_ct_organ_segmentation", "study": "3d_ct_organ_segmentation"}, "reported_validation": [ {"dataset": "ExampleBench", "metric": "Dice", "source": "Journal of Examples 2021"} ], "developed_on": ["ExampleBench"], "evaluation_arms": [ {"name": "external", "dataset": "OtherCohort-2026"}, {"name": "modality_shift", "dataset": "OtherCohort-2026 MRI"} ], "hardware": {"claimed": "any CUDA GPU", "verified_on": "GTX 1080 Ti", "verified": true} } -
dossier_defect.json 626 B
{ "model": "SynthSeg-3D (synthetic example)", "source": {"kind": "github", "url": "https://example.invalid/org/synthseg3d"}, "licence": {"spdx": "CC-BY-NC-4.0"}, "intended_use": "commercial", "weights": {"pretrained": true, "trained_on": ["ExampleBench", "OpenOrgan-1k"]}, "task": {"model": "2d_xray_classification", "study": "3d_ct_organ_segmentation"}, "reported_validation": [], "developed_on": ["ExampleBench"], "evaluation_arms": [ {"name": "internal", "dataset": "ExampleBench Task03 Liver"}, {"name": "external", "dataset": "OtherCohort-2026"} ], "hardware": {"claimed": "any CUDA GPU"} } -
dossier_unstated.json 556 B
{ "model": "AnonNet (synthetic example)", "source": {"kind": "huggingface", "url": "https://example.invalid/models/anonnet"}, "intended_use": "research", "weights": {"pretrained": true}, "task": {"model": "3d_ct_organ_segmentation", "study": "3d_ct_organ_segmentation"}, "reported_validation": [{"dataset": "OtherCohort-2026", "metric": "Dice", "source": "preprint"}], "developed_on": [], "evaluation_arms": [{"name": "external", "dataset": "OtherCohort-2026"}], "hardware": {"claimed": "A100", "verified_on": "A100", "verified": true} }
-
-
problem.md 2.4 KB
# Challenge — the provenance conflict a licence check cannot see You have chosen an architecture and found a concrete implementation: a public repository, well cited, permissively licensed, whose task matches yours and whose code runs on your hardware. You plan to report an internal validation on a public benchmark and an external validation on your own cohort. Every check a careful person performs by hand passes. The licence is real and permissive. The paper is peer-reviewed and highly cited. The task matches. The code executes. **The one fact that decides how your internal number should be read is not in any of those places.** If the method was developed, tuned, or competed against the benchmark family you are about to evaluate it on, that arm is not an independent test of it — however scrupulously you held out your own split. It reads like validation and is closer to a training-set score, and nothing in the repository will tell you, because the conflict lives in the relationship between two facts that sit in different documents. ## Task Write a **model dossier** for the artifact — source and version pin, licence and the file it was read from, intended use, whether weights are pretrained and on what, the model's task versus the study's, its reported validation, what it was developed on, and your evaluation arms — then run the gate over it: ```bash python3 ../check_model_provenance.py --dossier fixture/dossier_defect.json --strict ``` ## What the fixtures show - `dossier_defect.json` — facts that **contradict each other**: an arm on the benchmark the model was developed on, that same arm inside the pretraining corpus, a non-commercial licence under commercial intent, a mismatched task, no pin, no reported validation, unverified hardware. - `dossier_unstated.json` — facts that are **absent**: no licence, pretrained weights of unknown provenance, no pin. A different failure and the more common one; an unstated licence is not a permissive licence. - `dossier_clean.json` — everything stated and consistent. It still records `developed_on: ExampleBench`, and **nothing fires**, because no evaluation arm uses ExampleBench. Being developed on a benchmark is not a defect; evaluating on it is. ## Verify ```bash bash verify.sh ``` Deterministic and network-free: no repository is fetched and no licence is resolved online. An unstated fact is a finding, never a value the gate guesses at. -
verify.sh 2.5 KB
#!/usr/bin/env bash # Deterministic verifier for the model-provenance challenge card. # Runs check_model_provenance.py on three synthetic dossiers and diffs stdout against # expected/. No network, no repository fetch, no licence resolution — every finding is # decided by set arithmetic over the dossier JSON. Exit 0 = all match and exit codes correct. # # Fixtures (synthetic only — invented model and dataset names, no real artifacts): # dossier_defect.json — facts that CONTRADICT each other: an evaluation arm on the very # benchmark the model was developed on, that same arm inside the # pretraining corpus, a non-commercial licence under commercial # intent, a task that is not the study's task, no version pin, no # reported validation, hardware claimed but never run. # dossier_unstated.json — facts that are ABSENT: no licence at all, pretrained weights whose # corpus is not stated, no version pin. A different failure mode from # the first, and the more common one. # dossier_clean.json — the same kind of artifact with every fact stated and consistent. # Note it still declares `developed_on: ExampleBench`; nothing fires, # because no evaluation arm uses ExampleBench. The gate flags the # RELATIONSHIP between provenance and evaluation, not the presence of # a benchmark in a model's history. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" DET="$HERE/../check_model_provenance.py" ok=1 for f in defect unstated clean; do got="$(python3 "$DET" --dossier "$HERE/fixture/dossier_$f.json")" if ! diff -u "$HERE/expected/$f.txt" <(printf '%s\n' "$got"); then echo "FAIL: $f-fixture output drifted from expected/$f.txt" >&2; ok=0 fi done for f in defect unstated; do python3 "$DET" --dossier "$HERE/fixture/dossier_$f.json" --strict --quiet >/dev/null 2>&1 && rc=0 || rc=$? [ "${rc:-0}" -eq 1 ] || { echo "FAIL: $f fixture should exit 1 under --strict (got ${rc:-0})" >&2; ok=0; } done python3 "$DET" --dossier "$HERE/fixture/dossier_clean.json" --strict --quiet >/dev/null 2>&1 && rc=0 || rc=$? [ "$rc" -eq 0 ] || { echo "FAIL: clean fixture should exit 0 under --strict (got $rc)" >&2; ok=0; } if [ "$ok" -eq 1 ]; then echo "PASS: model-provenance gate separates contradictory facts from absent ones and clears a fully stated dossier." fi [ "$ok" -eq 1 ] || exit 1
-
-
check_model_provenance.py 12.4 KB
#!/usr/bin/env python3 """Model-provenance gate for a third-party model you are about to build a study on. Choosing an architecture is a literature question and `/architecture-zoo` answers it. Choosing a *concrete artifact* — this repository, this checkpoint, this revision — is a provenance question, and the two facts a researcher usually checks (the licence, the citation count) are the two that cannot answer it. The failure this gate exists for is the quietest one. A method developed and tuned against a benchmark family will be evaluated by the next person *on that same benchmark*, and the resulting number will read like validation while being closer to a training-set score. Nothing in the repository says so: the licence is clean, the paper is highly cited, the task matches, the code runs. The conflict lives in the relationship between two facts that sit in different documents — what the model was developed on, and what you are about to evaluate it on — and it is visible only when they are written down side by side. That is what the dossier is for. The gate reads a declarative **model dossier** and decides each finding by set arithmetic over it. It never fetches a repository, never resolves a licence from the network, and never infers a fact the dossier does not state: an unstated fact is a finding, not something to guess at. CHECKS (verdicts): MAJOR 1. BENCHMARK_PROVENANCE_CONFLICT a dataset the model was developed/tuned/competed on also appears among your evaluation arms. That arm does not independently test the model, however held-out it is for you. 2. EVAL_DATA_IN_TRAINING an evaluation arm's dataset appears in the model's own training corpus — the strong form of the same problem. 3. LICENCE_UNSTATED no licence recorded. Not "probably permissive": unstated. 4. LICENCE_INCOMPATIBLE the licence forbids the declared intended use (a non-commercial or research-only licence under commercial or deployment intent). 5. WEIGHTS_PROVENANCE_UNKNOWN pretrained weights are used and the corpus they were trained on is not stated — so what is in them cannot be reasoned about, including whether your evaluation set is. MINOR (each is a fact to record, not necessarily a defect) 6. TASK_MISMATCH the model's declared task differs from the study's. 7. NO_VERSION_PIN no commit, tag or revision — "we used nnU-Net" is not a reproducible statement. 8. VALIDATION_UNREPORTED no reported validation (dataset + metric) recorded. 9. HARDWARE_UNVERIFIED hardware compatibility claimed but never executed. 10. LICENCE_UNVERIFIED a licence is named but its source file is not, so it came from a badge or a memory rather than from the artifact. DOSSIER (JSON) { "model": "nnU-Net v2", "source": {"kind": "github", "url": "...", "version": "v2.5.1", "commit": "abc1234"}, "licence": {"spdx": "Apache-2.0", "verified_from": "LICENSE at the pinned commit"}, "intended_use": "research", // research | commercial | clinical_deployment "weights": {"pretrained": false}, // if true, add "trained_on": [...] "task": {"model": "3d_ct_organ_segmentation", "study": "3d_ct_organ_segmentation"}, "reported_validation": [{"dataset": "MSD", "metric": "Dice", "source": "Nat Methods 2021"}], "developed_on": ["Medical Segmentation Decathlon"], "evaluation_arms": [{"name": "internal", "dataset": "MSD Task09 Spleen"}, {"name": "external", "dataset": "AMOS22"}], "hardware": {"claimed": "any CUDA GPU", "verified_on": "GTX 1080 Ti", "verified": true} } Dataset names are matched as **token sequences**, so "MSD Task09 Spleen" matches "MSD" while "MS" does not match "MSD", plus a small alias table for families that are routinely written two ways (MSD / Decathlon). Matching never falls back to substring search. INPUTS --dossier model dossier JSON (required). OUTPUT A findings table (stdout) and, with --out, a JSON artifact. Exit 1 under --strict when any Major finding exists. Stdlib-only. """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path # Families routinely written more than one way. Values are canonical token sequences. DATASET_ALIASES = { "decathlon": ["msd"], "medicalsegmentationdecathlon": ["msd"], "medical": ["msd"], # only via the multi-token form below } MULTI_TOKEN_ALIASES = { ("medical", "segmentation", "decathlon"): ["msd"], ("the", "medical", "segmentation", "decathlon"): ["msd"], } NONCOMMERCIAL_MARKERS = ("-nc-", "-nc", "noncommercial", "non-commercial", "research-only", "researchonly", "cc-by-nc") RESTRICTED_USES = {"commercial", "clinical_deployment", "clinical-deployment", "deployment", "product"} def _tokens(name: str) -> tuple[str, ...]: toks = tuple(t for t in re.split(r"[^a-z0-9]+", str(name).strip().lower()) if t) if toks in MULTI_TOKEN_ALIASES: return tuple(MULTI_TOKEN_ALIASES[toks]) if len(toks) == 1 and toks[0] in DATASET_ALIASES and toks[0] != "medical": return tuple(DATASET_ALIASES[toks[0]]) return toks def datasets_match(a: str, b: str) -> bool: """True when two dataset names denote the same corpus (or one is a family of the other). Token-sequence prefix, never substring: ('msd','task09','spleen') matches ('msd',), and ('ms',) does not match ('msd',). """ ta, tb = _tokens(a), _tokens(b) if not ta or not tb: return False short, long_ = (ta, tb) if len(ta) <= len(tb) else (tb, ta) return long_[:len(short)] == short def _norm(s) -> str: return str(s).strip().lower() if s is not None else "" def analyze(dossier_path: str) -> dict: d = json.loads(Path(dossier_path).read_text(encoding="utf-8")) claims: list[dict] = [] def claim(verdict: str, severity: str, detail: str, where: list[str] | None = None) -> None: claims.append({"verdict": verdict, "severity": severity, "detail": detail, "where": sorted(where or [])[:12], "n_where": len(where or [])}) arms = d.get("evaluation_arms") or [] arm_datasets = [(a.get("name") or "?", a.get("dataset") or "") for a in arms] # ---- the conflict neither the licence nor the citation count reveals ------ developed_on = d.get("developed_on") or [] hits = [f"{nm} ({ds}) <- developed on {dev}" for nm, ds in arm_datasets for dev in developed_on if datasets_match(ds, dev)] if hits: claim("BENCHMARK_PROVENANCE_CONFLICT", "Major", "an evaluation arm uses a dataset the model was developed or tuned on; that arm " "does not independently test the model, no matter how held-out the split is for " "you. Report it as a demonstration of the pipeline and put the evidence on an arm " "whose data post-dates the model", hits) # ---- the strong form: your eval set is in the weights --------------------- weights = d.get("weights") or {} pretrained = bool(weights.get("pretrained")) trained_on = weights.get("trained_on") or [] leaks = [f"{nm} ({ds}) in the training corpus ({tr})" for nm, ds in arm_datasets for tr in trained_on if datasets_match(ds, tr)] if leaks: claim("EVAL_DATA_IN_TRAINING", "Major", "an evaluation arm's dataset appears in the corpus the weights were trained on; " "the resulting number is a training-set score", leaks) if pretrained and not trained_on: claim("WEIGHTS_PROVENANCE_UNKNOWN", "Major", "pretrained weights are used and the corpus behind them is not stated, so it " "cannot be established whether your evaluation data is inside them") # ---- licence ------------------------------------------------------------- lic = d.get("licence") or d.get("license") or {} spdx = _norm(lic.get("spdx") or lic.get("name")) use = _norm(d.get("intended_use")) if not spdx: claim("LICENCE_UNSTATED", "Major", "no licence recorded. An unstated licence is not a permissive one — it is an " "unanswered question about whether the work may be used or redistributed at all") else: if not lic.get("verified_from"): claim("LICENCE_UNVERIFIED", "Minor", f"licence '{spdx}' is named but the file it was read from is not recorded; " "a badge in a README is not the licence") if use in RESTRICTED_USES and any(m in spdx for m in NONCOMMERCIAL_MARKERS): claim("LICENCE_INCOMPATIBLE", "Major", f"licence '{spdx}' restricts use to non-commercial or research purposes while " f"the declared intended use is '{use}'") # ---- task, pin, validation, hardware ------------------------------------- task = d.get("task") or {} tm, ts = _norm(task.get("model")), _norm(task.get("study")) if tm and ts and tm != ts: claim("TASK_MISMATCH", "Minor", f"the model's task ('{tm}') is not the study's task ('{ts}'); a transfer step and " "its own validation are required, not just a checkpoint") src = d.get("source") or {} if not (src.get("commit") or src.get("version") or src.get("revision") or src.get("tag")): claim("NO_VERSION_PIN", "Minor", "no commit, tag or revision recorded; 'we used <model>' does not identify what ran " "and cannot be reproduced once the default branch moves") if not (d.get("reported_validation") or []): claim("VALIDATION_UNREPORTED", "Minor", "no reported validation (dataset + metric + source) recorded for the model as " "published; there is then no prior expectation to compare your own numbers against") hw = d.get("hardware") or {} if hw.get("claimed") and not hw.get("verified"): claim("HARDWARE_UNVERIFIED", "Minor", f"hardware support is claimed ('{hw.get('claimed')}') but never executed; support " "matrices and what the stack actually runs on are different statements") majors = [c for c in claims if c["severity"] == "Major"] return { # the artifact names its own author: a qc filename is chosen by the caller and # cannot be trusted to identify what wrote it "detector": "check_model_provenance", "dossier": dossier_path, "model": d.get("model", ""), "n_arms": len(arms), "claims": claims, "summary": {"major": len(majors), "minor": len(claims) - len(majors)}, } def render(result: dict) -> str: lines = [f"Model provenance — {result['model'] or '(unnamed)'} " f"({result['n_arms']} evaluation arm(s))", ""] if not result["claims"]: lines.append(" no findings — every provenance fact the gate checks is stated and consistent") for c in result["claims"]: lines.append(f" [{c['severity']:<5}] {c['verdict']}") lines.append(f" {c['detail']}") for w in c["where"]: lines.append(f" - {w}") lines.append("") lines.append(f" Major {result['summary']['major']} Minor {result['summary']['minor']}") return "\n".join(lines) def main() -> int: ap = argparse.ArgumentParser(description="Audit a third-party model's provenance dossier.") ap.add_argument("--dossier", required=True) ap.add_argument("--out") ap.add_argument("--strict", action="store_true", help="exit 1 when a Major finding exists") ap.add_argument("--quiet", action="store_true") a = ap.parse_args() if not Path(a.dossier).exists(): print(f"input error: dossier not found: {a.dossier}", file=sys.stderr) return 2 try: result = analyze(a.dossier) except json.JSONDecodeError as exc: print(f"input error: dossier is not valid JSON: {exc}", file=sys.stderr) return 2 if not a.quiet: print(render(result)) if a.out: Path(a.out).parent.mkdir(parents=True, exist_ok=True) Path(a.out).write_text(json.dumps(result, indent=1), encoding="utf-8") return 1 if (a.strict and result["summary"]["major"]) else 0 if __name__ == "__main__": sys.exit(main())
-
-
tests
-
test_model_provenance.sh 4.8 KB
#!/usr/bin/env bash # Regression test for the model-provenance gate (model-sourcing). # Synthetic, PII-free JSON dossiers reproduce each verdict class. Stdlib-only (python3). set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT="$HERE/../scripts/check_model_provenance.py" CH="$HERE/../scripts/check_model_provenance_challenge" TMP="$(mktemp -d -t modprov_XXXX)" OUT="$TMP/out.json" trap 'rm -rf "$TMP"' EXIT fail=0 check() { local label="$1"; shift if "$@" >/dev/null 2>&1; then printf ' PASS %s\n' "$label" else printf ' FAIL %s\n' "$label"; fail=$((fail+1)); fi } has_verdict() { python3 -c " import json d=json.load(open('$OUT')) assert any(c['verdict']=='$1' for c in d['claims']), '$1 not found' "; } no_verdict() { python3 -c " import json d=json.load(open('$OUT')) assert not any(c['verdict']=='$1' for c in d['claims']), '$1 unexpectedly present' "; } [[ -f "$SCRIPT" ]] || { echo "ENV-ERR: script missing" >&2; exit 2; } # (1) contradictory facts python3 "$SCRIPT" --dossier "$CH/fixture/dossier_defect.json" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 1 (contradictory dossier)" test "$?" -eq 1 for v in BENCHMARK_PROVENANCE_CONFLICT EVAL_DATA_IN_TRAINING LICENCE_INCOMPATIBLE \ LICENCE_UNVERIFIED TASK_MISMATCH NO_VERSION_PIN VALIDATION_UNREPORTED HARDWARE_UNVERIFIED; do check "$v detected" has_verdict "$v" done # (2) absent facts -- a different failure mode python3 "$SCRIPT" --dossier "$CH/fixture/dossier_unstated.json" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 1 (unstated dossier)" test "$?" -eq 1 check "LICENCE_UNSTATED detected" has_verdict LICENCE_UNSTATED check "WEIGHTS_PROVENANCE_UNKNOWN detected" has_verdict WEIGHTS_PROVENANCE_UNKNOWN check "an unstated licence does not also read as incompatible" no_verdict LICENCE_INCOMPATIBLE # (3) fully stated dossier -> silent python3 "$SCRIPT" --dossier "$CH/fixture/dossier_clean.json" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 0 (clean dossier)" test "$?" -eq 0 check "being developed on a benchmark you do NOT evaluate on is not a finding" \ no_verdict BENCHMARK_PROVENANCE_CONFLICT check "no LICENCE_UNVERIFIED once the licence file is recorded" no_verdict LICENCE_UNVERIFIED check "no HARDWARE_UNVERIFIED once the claim has been executed" no_verdict HARDWARE_UNVERIFIED # (4) dataset names are matched as token sequences, never as substrings mk() { python3 - "$1" "$2" "$3" <<'PY' import json, sys out, dev, arm = sys.argv[1], sys.argv[2], sys.argv[3] json.dump({"model": "m", "source": {"commit": "c"}, "licence": {"spdx": "Apache-2.0", "verified_from": "LICENSE"}, "intended_use": "research", "weights": {"pretrained": False}, "task": {"model": "t", "study": "t"}, "reported_validation": [{"dataset": "x", "metric": "Dice", "source": "s"}], "developed_on": [dev], "evaluation_arms": [{"name": "a", "dataset": arm}], "hardware": {}}, open(out, "w")) PY } mk "$TMP/d.json" "MSD" "MSD Task09 Spleen" python3 "$SCRIPT" --dossier "$TMP/d.json" --out "$OUT" --quiet >/dev/null 2>&1 check "'MSD Task09 Spleen' matches the family 'MSD'" has_verdict BENCHMARK_PROVENANCE_CONFLICT mk "$TMP/d.json" "Medical Segmentation Decathlon" "MSD Task09 Spleen" python3 "$SCRIPT" --dossier "$TMP/d.json" --out "$OUT" --quiet >/dev/null 2>&1 check "the spelled-out family name resolves to the same corpus" has_verdict BENCHMARK_PROVENANCE_CONFLICT mk "$TMP/d.json" "MSD" "MS Cohort 2026" python3 "$SCRIPT" --dossier "$TMP/d.json" --out "$OUT" --quiet >/dev/null 2>&1 check "'MS Cohort 2026' does NOT match 'MSD' (no substring matching)" \ no_verdict BENCHMARK_PROVENANCE_CONFLICT mk "$TMP/d.json" "MSD" "AMOS22" python3 "$SCRIPT" --dossier "$TMP/d.json" --out "$OUT" --quiet >/dev/null 2>&1 check "an unrelated external cohort does not match" no_verdict BENCHMARK_PROVENANCE_CONFLICT # (5) a non-commercial licence is only a conflict under a restricted intended use python3 - "$TMP/nc.json" <<'PY' import json, sys json.dump({"model": "m", "source": {"commit": "c"}, "licence": {"spdx": "CC-BY-NC-4.0", "verified_from": "LICENSE"}, "intended_use": "research", "weights": {"pretrained": False}, "task": {"model": "t", "study": "t"}, "reported_validation": [{"dataset": "x", "metric": "Dice", "source": "s"}], "developed_on": [], "evaluation_arms": [{"name": "a", "dataset": "z"}], "hardware": {}}, open(sys.argv[1], "w")) PY python3 "$SCRIPT" --dossier "$TMP/nc.json" --out "$OUT" --quiet >/dev/null 2>&1 check "a non-commercial licence under research use is not a conflict" no_verdict LICENCE_INCOMPATIBLE # (6) the shipped challenge card reproduces check "challenge verify.sh passes" bash "$CH/verify.sh" echo if [[ "$fail" -eq 0 ]]; then echo "ALL PASS (model-provenance gate)"; else echo "$fail FAILURE(S)"; exit 1; fi
-
-
SKILL.md 9 KB
--- name: model-sourcing description: > Vet the concrete third-party model a study will be built on — this repository, this revision, this checkpoint — not the architecture family. Records a model dossier (source and version pin, licence and the file it was read from, intended use, pretrained-weight provenance, model task vs study task, reported validation, what the model was developed on, your evaluation arms) and gates it deterministically. Catches what a licence check and a citation count cannot: an evaluation arm sitting on the benchmark the model was developed or tuned on, so the arm reads like validation while being closer to a training-set score. Also an evaluation set inside a pretraining corpus, an unstated or use-incompatible licence, an unpinned revision, and a hardware claim never executed. It vets an artifact; it never downloads or runs one. triggers: source a model, vet a model, pick a model, model provenance, model dossier, pretrained weights, checkpoint, HuggingFace model, GitHub model, model licence, weight provenance, is this model independent, benchmark overlap, trained on my test set, data contamination, model version pin, third-party model, can I use this model tools: Read, Write, Edit, Bash, Grep, Glob model: inherit --- # Model-Sourcing Skill ## Purpose `/architecture-zoo` answers a literature question — which family of model suits this task. That question has a stable answer. The next question does not: *which concrete artifact do I run?* A repository, a revision, a checkpoint. That is a provenance question, and the two facts a careful researcher usually checks are the two that cannot answer it. The licence tells you whether you may use it. The citation count tells you whether others did. Neither tells you **whether the number you are about to report means what you will say it means.** The failure this skill exists for is the quietest one in the lane. A method developed and tuned against a benchmark family gets evaluated by the next person *on that same family*, and the resulting figure reads like validation while sitting much closer to a training-set score. Nothing in the repository says so. The licence is clean, the paper is peer-reviewed and highly cited, the task matches, the code runs on your GPU. The conflict lives in the **relationship** between two facts that are documented in different places — what the model was developed on, and what you are about to evaluate it on — and it becomes visible only when they are written down side by side. Writing them down side by side is what the dossier is for. ## When to use - You have a concrete candidate (a GitHub repo, a Hugging Face checkpoint, a paper's released weights) and are about to build a study on it. - You are writing the Methods paragraph that says which model you used, and it has to survive a reviewer asking what it was trained on. - You inherited a pipeline whose model came from somewhere nobody recorded. ## When NOT to use - Choosing an architecture *family* → `/architecture-zoo` (archetypes and the task-to-architecture logic; deliberately not a live leaderboard). - Building the training repo → `/model-scaffold`. Designing the validation study → `/model-validation`. Computing held-out metrics → `/model-evaluation`. - Documenting a model **you** built → `/model-card` (Model Card + Datasheet). - Auditing your own dataset before modelling → `/profile-imaging`. - Evaluating an LLM/multimodal system on a clinical task → `/mllm-eval` (which owns pretraining-contamination of public benchmarks for that setting). ## Workflow ### Step 1 — write the dossier One JSON file recording what is *known*, with unknowns left unstated rather than guessed: ```json { "model": "OrganSeg-3D v2.5.1", "source": {"kind": "github", "url": "...", "version": "v2.5.1", "commit": "abc1234"}, "licence": {"spdx": "Apache-2.0", "verified_from": "LICENSE at commit abc1234"}, "intended_use": "research", "weights": {"pretrained": false}, "task": {"model": "3d_ct_organ_segmentation", "study": "3d_ct_organ_segmentation"}, "reported_validation": [{"dataset": "ExampleBench", "metric": "Dice", "source": "J Ex 2021"}], "developed_on": ["ExampleBench"], "evaluation_arms": [{"name": "external", "dataset": "OtherCohort-2026"}], "hardware": {"claimed": "any CUDA GPU", "verified_on": "GTX 1080 Ti", "verified": true} } ``` Each field is read from the artifact, not from memory: the licence from the `LICENSE` file at the pinned commit (a README badge is not the licence), `developed_on` from the paper's own account of where the method was built and tuned, `hardware.verified` only after it has actually run. `developed_on` is the field people skip, and it is the one the gate needs. A method that won a challenge was tuned on that challenge. ### Step 2 — gate it ```bash python3 scripts/check_model_provenance.py --dossier model_dossier.json \ --out qc/model_provenance.json --strict ``` Stdlib-only, network-free — no repository is fetched and no licence resolved online, so the audit re-runs anywhere the JSON travels. Verdicts: | Verdict | Severity | Fires when | |---|---|---| | `BENCHMARK_PROVENANCE_CONFLICT` | Major | an evaluation arm uses a dataset the model was developed or tuned on | | `EVAL_DATA_IN_TRAINING` | Major | an evaluation arm's dataset is inside the pretraining corpus | | `LICENCE_UNSTATED` | Major | no licence recorded — which is not the same as a permissive one | | `LICENCE_INCOMPATIBLE` | Major | a non-commercial / research-only licence under commercial or deployment intent | | `WEIGHTS_PROVENANCE_UNKNOWN` | Major | pretrained weights whose training corpus is not stated | | `TASK_MISMATCH` | Minor | the model's task is not the study's task | | `NO_VERSION_PIN` | Minor | no commit, tag or revision | | `VALIDATION_UNREPORTED` | Minor | no reported validation (dataset + metric + source) | | `HARDWARE_UNVERIFIED` | Minor | hardware support claimed but never executed | | `LICENCE_UNVERIFIED` | Minor | a licence is named but the file it was read from is not | **The gate flags a relationship, not a reputation.** A dossier that declares `developed_on: ExampleBench` passes cleanly as long as no evaluation arm uses ExampleBench. Being developed on a benchmark is not a defect; evaluating on it and calling that independent is. The clean fixture exists to prove exactly that distinction. Dataset names are matched as **token sequences** with a small family-alias table, so `MSD Task09 Spleen` matches `MSD` and `MS Cohort 2026` does not. Matching never falls back to substring search. ### Step 3 — turn a Major into a study decision A `BENCHMARK_PROVENANCE_CONFLICT` is rarely a reason to abandon the model — it is usually the best-engineered option precisely because it was tuned hard. It is a reason to change **what the arm is claimed to establish**: 1. Report that arm as a demonstration that the pipeline runs end to end, not as evidence the method works. 2. Put the evidential weight on an arm whose data **post-dates** the model, and say so with dates. 3. State the conflict in Methods and Limitations rather than leaving a reviewer to find it. An `EVAL_DATA_IN_TRAINING` is different in kind: that arm produces a training-set score and cannot be reported as validation at all. Carry the dossier forward — `/model-validation` (arm design), `/model-evaluation` (what each arm may claim), `/model-card` (provenance section), `/write-paper` (Methods + Limitations). ## Outputs - `model_dossier.json` — the provenance record downstream skills and the Methods section read. - `qc/model_provenance.json` — deterministic audit with verdicts. - The arm-by-arm decision from Step 3, written into the study record. ## Anti-Hallucination - **Never infer a fact the dossier does not state.** An unstated licence is `LICENCE_UNSTATED`, never "probably MIT"; an unstated pretraining corpus is a Major finding, never an assumption. - **Never record a licence from a badge, a model card summary, or memory** — only from the licence file at the pinned revision, and record which file that was. - **Never mark hardware verified without executing it.** A support matrix and what the stack actually runs is a different claim; a CUDA capability the compiler accepts may still be refused by a compiler in the same stack. - **Never report an arm as independent validation when the gate flags a provenance conflict.** - If a provenance fact cannot be established from the artifact, leave it unstated and let the gate say so. ## Deterministic gate `scripts/check_model_provenance.py` — 10 verdicts by set arithmetic over the dossier, stdlib-only and network-free. Reproducible challenge: `bash ${CLAUDE_SKILL_DIR}/scripts/check_model_provenance_challenge/verify.sh`. Regression suite: `bash ${CLAUDE_SKILL_DIR}/tests/test_model_provenance.sh`. ## Boundaries ``` architecture-zoo (which family?) -> model-sourcing (this skill: which artifact, and what may its numbers claim?) -> profile-imaging / preprocess-imaging -> model-scaffold -> model-validation -> model-evaluation -> model-card -> write-paper ``` -
skill.yml 4.1 KB
schema_version: 2 name: model-sourcing layer: D owner_domain: model_validation maturity: official when_to_use: "Vet the CONCRETE third-party model a study will be built on — this repository, this revision, this checkpoint — before scaffolding or evaluation. Records a model dossier (source and version pin, licence and the file it was read from, intended use, pretrained-weight provenance, model task vs study task, reported validation, what the model was developed on, your evaluation arms) and gates it. Catches what a licence check and a citation count cannot: an evaluation arm that uses the benchmark the model was developed or tuned on, so the arm reads like validation while sitting closer to a training-set score; an evaluation set inside a pretraining corpus; an unstated or use-incompatible licence; an unpinned revision; a hardware claim never executed." when_NOT_to_use: "Choosing an architecture family (use architecture-zoo). Building the training repo (use model-scaffold). Designing the validation study or auditing split leakage (use model-validation). Computing held-out metrics (use model-evaluation). Documenting a model you built yourself (use model-card). Profiling your dataset before modelling (use profile-imaging). Evaluating an LLM/multimodal system on a clinical task, including benchmark contamination in that setting (use mllm-eval)." inputs: - "the concrete artifact under consideration: repository or checkpoint URL, revision, licence file" - "the model's published account of what it was developed, tuned, or competed on" - "the study's evaluation arms (arm name -> dataset) and the study's task" - "the declared intended use (research / commercial / clinical deployment)" outputs: - "model dossier JSON (the provenance record downstream skills and the Methods section read)" - "model-provenance audit JSON (deterministic verdicts)" - "arm-by-arm decision: which arms may be claimed as independent validation and which may not" deterministic_scripts: - scripts/check_model_provenance.py side_effects: - writes_decision_notes downstream_consumers: - model-validation - model-evaluation - model-card - write-paper - check-reporting forbidden_actions: - infer_a_provenance_fact_the_dossier_does_not_state - record_a_licence_from_a_badge_or_summary_instead_of_the_licence_file - mark_hardware_verified_without_executing_it - report_an_arm_as_independent_validation_when_a_provenance_conflict_is_flagged - download_or_execute_a_third_party_model_as_part_of_the_audit # v2.1 quality card purpose: "Establish what a third-party model's numbers are allowed to claim, before a study is built on it — by writing the provenance facts that live in different documents into one record and auditing the relationships between them, chiefly whether an evaluation arm sits on the benchmark the model was developed against." safety_boundaries: - "Audit only: never downloads, executes, fine-tunes, or benchmarks a model, and never fetches a repository or resolves a licence over the network." - "Every verdict is decided by set arithmetic over the dossier JSON; an unstated fact yields a finding rather than an inferred value." - "Stdlib-only, so the audit reproduces anywhere the dossier travels." known_limitations: - "The dossier is taken at face value: the gate cannot tell that a stated licence is wrong or that `developed_on` is incomplete, so the reading of the artifact is the researcher's responsibility and the gate audits the consequences." - "Dataset matching is token-sequence based with a small family-alias table; two names for the same corpus that share no leading token (a private cohort renamed between papers) will not be matched." - "A clean dossier is necessary, not sufficient: split disjointness (model-validation), preprocessing leakage (preprocess-imaging) and metric choice (model-evaluation) are separate gates." validation_commands: - "python3 scripts/check_model_provenance.py --dossier <dossier.json> --strict" - "bash scripts/check_model_provenance_challenge/verify.sh # deterministic, network-free" - "bash tests/test_model_provenance.sh" evidence_surface: ci_validator
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.