From c9275606145529c77c142652818ccb98b9509957 Mon Sep 17 00:00:00 2001 From: Victor Kuznetsov Date: Tue, 25 Aug 2026 20:04:25 -0700 Subject: [PATCH] Stop gating C2PA confidence on a trust anchor that never ships High-confidence C2PA attribution required signingCredential.trusted, a status code the reader emits only when a trust anchor list is loaded. None ships, so from 0.27.0 through 0.30.0 the branch was unreachable in production for every vendor: an intact, cryptographically bound manifest scored the same medium as a fallback parse that validated nothing, which collapsed the one distinction the official reader exists to draw. A hand-built info dict stamping that code kept the branch green in the suite. Confidence now follows the binding. Signer trust and certificate expiry stay visible as their own dimensions and as caveats, because a trust list that was never configured is a missing input, not a finding against the credential. Every committed provenance fixture with a reader result and an intact binding now reaches high confidence, and test_no_committed_fixture_reports_a_trusted_signer guards the reachability itself rather than a synthesized status set. Revocation joins binding and signature failures as disqualifying. It arrives only on signer_validity, so a check reading the other two returned a confident AI verdict off a credential the issuer had disowned, with an empty integrity_clashes -- quieter than a hash mismatch on the same file. Expiry stays non-disqualifying: it does not imply the signed bytes changed, and a signature genuinely made outside validity already arrives as claimSignature.outsideValidity. The rule now lives in one place. _validation_fields maps status codes to the four dimensions and names the failures that moved one; c2pa_info_has_invalid_credential maps dimensions to disqualified. The ingredient-reachability walk and the user-visible reason both consume that path instead of re-classifying raw codes, so adding this one rule no longer means editing three layers in lockstep. Co-Authored-By: Claude Opus 5 --- .claude/rules/development.md | 14 +++ docs/cli.md | 8 +- docs/module-internals.md | 48 +++++++-- docs/python-api.md | 23 ++++- docs/supported-signals.md | 8 +- src/remove_ai_watermarks/_internal/c2pa.py | 50 ++++++++-- src/remove_ai_watermarks/identify.py | 59 ++++++----- tests/test_identify.py | 109 +++++++++++++++++++-- tests/test_metadata_internals.py | 27 ++--- 9 files changed, 273 insertions(+), 73 deletions(-) diff --git a/.claude/rules/development.md b/.claude/rules/development.md index 9db0237..90aa39a 100644 --- a/.claude/rules/development.md +++ b/.claude/rules/development.md @@ -65,6 +65,15 @@ that is what actually happened. Use availability checks only for paths that actually load large models. +A verdict branch must be reachable with the dependency stack as shipped. High-confidence +C2PA attribution required `signingCredential.trusted`, a code the reader emits only when a +trust anchor list is loaded, and none ships -- so the branch was dead in production for +every vendor while a hand-built info dict stamping that code kept it green from 0.27.0 +through 0.30.0. Assert reachability against the committed fixtures rather than a synthesized +status set; `TestIdentifyRealSamples::test_no_committed_fixture_reports_a_trusted_signer` +is that guard. Read a capability the stack does not have as a missing input, never as a +negative finding. + ## One measurement, one gate seam A detector is split into a trust-level-blind scan and a verdict that applies the @@ -98,6 +107,11 @@ record is byte-identical, and a green test suite does not establish that on its change that is meant to FIX detection is the exception that proves the rule: the diff must then be exactly the files you intended to change, named in advance. +When the baseline is a published release rather than the previous commit, get it with +`uv run --isolated --no-project --with 'remove-ai-watermarks[heif]==' python