The artifact-provenance graph (artifact_provenance,
artifact_links) tells us "artifact Y was produced from inputs X1,
X2, X3 by skill S at time T", but nothing actually verifies that the
claim holds. An attacker (or a buggy skill) can write a provenance
row pointing at unrelated inputs. This task ships a scanner that, for
artifacts whose skill is deterministic / re-runnable, recomputes the
output from the recorded inputs and compares hashes. When the
recomputed hash diverges from the stored hash, the artifact is
flagged as lifecycle='provenance_break' and a Senate review task is
spawned. The scanner runs in two modes: strict for deterministic
skills (recomputes), soft for non-deterministic skills (LLM
generations — checks that the recorded inputs include all PMIDs
referenced in the output, and that no input was modified after the
output's created_at).
Effort: thorough
scidex/atlas/provenance_integrity.py:classify_skill(skill_id) -> Mode returns'strict' | 'soft' | 'skip' from a registry table:CREATE TABLE skill_provenance_mode (
skill_id TEXT PRIMARY KEY,
mode TEXT NOT NULL CHECK (mode IN ('strict','soft','skip')),
rationale TEXT
); Seed strict-mode for: pubmed-search, openalex-works,
compute_content_hash-emitters; soft-mode for: theorist,
skeptic, hypothesis-generation; skip for personas whose
outputs are uniquely contextual.
- scan_artifact(artifact_id) -> ScanResult — per mode:
- strict: re-invoke the skill with stored inputs;
compare output hash. (Re-invocation goes through
scidex/forge/executor.py:isolated_run.)
- soft: parse all PMID/DOI/UUID references from the
output; assert each appears in the provenance inputs;
assert no input row's last_updated_at > output.created_at.
migrations/20260428_provenance_integrity.sql:CREATE TABLE provenance_integrity_scan (
id BIGSERIAL PRIMARY KEY,
artifact_id TEXT NOT NULL,
scan_mode TEXT NOT NULL,
scanned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
verdict TEXT NOT NULL CHECK (verdict IN
('verified','soft_verified','divergent',
'missing_inputs','reinvoke_failed','skipped')),
recomputed_hash TEXT,
stored_hash TEXT,
details JSONB
);
CREATE INDEX idx_pis_verdict_bad ON provenance_integrity_scan
(artifact_id) WHERE verdict IN ('divergent','missing_inputs'); (Plus the skill_provenance_mode table above.)
verdict='skipped'verdict='divergent' artifactslifecycle='provenance_break', lose their Elo bonusq-trust-signed-artifact-attestations if any (setattestation_status='quarantined'), and a Senate task isverdict='missing_inputs'audit_chain (event_kind='provenance_integrity_scan')GET /api/atlas/provenance/scan/{artifact_id} returnstests/test_provenance_integrity.py:divergent, lifecycle moves);skipped;skill_provenance_mode with explicit rationales (commitq-gov-lifecycle-state-machine-enforcement).
q-trust-audit-log-hash-chain — chain-append target.scidex/forge/executor.isolated_run — re-invocation.q-gov-lifecycle-state-machine-enforcement — handlesprovenance_break lifecycle transition.Result: FAIL
Verified by: MiniMax-M2 via task 2bcb16bc-b48f-425d-95f0-bff62cc9a9b9
scidex/forge/executor.py:isolated_run but the actual function lives in scidex/senate/cgroup_isolation.py:isolated_run() and is called via self._cgroup.isolated_run() from ForgeExecutorartifact_provenance table exists with 25 rows (action_kind='spawn_link' entries linking hypotheses to analyses)lifecycle_state column exists on artifacts table (NOT lifecycle as spec says) with values: active, deprecated, frozen, superseded, validated — provenance_break is NOT a valid stateq-gov-lifecycle-state-machine-enforcement dependency may or may not exist (lifecycle_state machine doesn't have provenance_break state)