diff --git a/README.md b/README.md index 3f19f09..de00f89 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,33 @@ That last row is the whole point: an unsigned real photo is `UNVERIFIED`, never --- +## Verdict synthesis — `synthesize(signals)` + +The five signals above are collapsed into one verdict by `synthesize()`, and +**the order of its checks IS the honesty policy** — it is not an implementation +detail. It was in `__all__` (a public API promise) with no documentation, which +meant the one function encoding the policy was the one you had to read source +to find. + +| Priority | Verdict | Fires when | Why this order | +|---|---|---|---| +| 1 | `TAMPERED` | any signal points to TAMPERED | A broken integrity signal outranks everything: if the bytes were altered, nothing else read from them can be trusted. | +| 2 | `CONFLICTING` | AUTHENTIC **and** SYNTHETIC both present | Disagreement is reported as disagreement. Silently picking a winner would manufacture a certainty that the evidence does not support. | +| 3 | `SYNTHETIC` | any SYNTHETIC signal | An AI-origin assertion is positive evidence. | +| 4 | `AUTHENTIC-SIGNED` | any AUTHENTIC signal | A provenance signature is present. ⚠️ PoC: the crypto chain is **not** yet verified. | +| 5 | `UNVERIFIED` | no usable signal | **The honest default.** No signal means *unknown* — it is **NOT** evidence of fakery. | + +Row 5 is the point of the package. A provenance tool that returns "fake" when +it simply cannot tell is worse than no tool, because the absence of a signal is +overwhelmingly common: most authentic media carries no C2PA manifest either. + +```python +from provmirror import verify, synthesize + +signals = verify("photo.jpg") +print(synthesize(signals)) # e.g. "UNVERIFIED" +``` + ## Usage ```bash diff --git a/README_KO.md b/README_KO.md index 7a7604b..5aac440 100644 --- a/README_KO.md +++ b/README_KO.md @@ -74,6 +74,31 @@ --- +## 판정 종합 — `synthesize(signals)` + +위 다섯 신호는 `synthesize()`가 하나의 판정으로 합칩니다. 그리고 **그 검사 순서가 +곧 정직 정책**입니다 — 구현 세부가 아닙니다. `__all__`에 올라 공개를 약속해놓고 +문서가 0건이었던 탓에, 정책을 담은 바로 그 함수가 소스를 읽어야만 보이는 상태였습니다. + +| 우선순위 | 판정 | 발화 조건 | 왜 이 순서인가 | +|---|---|---|---| +| 1 | `TAMPERED` | TAMPERED 신호 존재 | 무결성이 깨졌으면 모든 것에 우선한다. 바이트가 변조됐다면 거기서 읽은 다른 값은 믿을 수 없다. | +| 2 | `CONFLICTING` | AUTHENTIC **과** SYNTHETIC 동시 존재 | 불일치는 불일치로 보고한다. 조용히 한쪽을 고르면 증거가 뒷받침하지 않는 확신을 만들어낸다. | +| 3 | `SYNTHETIC` | SYNTHETIC 신호 존재 | AI 생성 주장은 적극적 증거다. | +| 4 | `AUTHENTIC-SIGNED` | AUTHENTIC 신호 존재 | 출처 서명이 있다. ⚠️ PoC: 암호 체인은 **아직 검증하지 않는다**. | +| 5 | `UNVERIFIED` | 쓸 만한 신호 없음 | **정직한 기본값.** 신호가 없다는 건 *모른다*는 뜻이지 **가짜라는 증거가 아니다**. | + +5행이 이 패키지의 존재 이유입니다. 판별할 수 없을 때 "가짜"라고 답하는 출처 도구는 +없느니만 못합니다 — 신호 부재는 압도적으로 흔하기 때문입니다. 진짜 매체 대부분도 +C2PA 매니페스트를 달고 있지 않습니다. + +```python +from provmirror import verify, synthesize + +signals = verify("photo.jpg") +print(synthesize(signals)) # 예: "UNVERIFIED" +``` + ## 사용법 ```bash diff --git a/tests/test_synthesize_priority.py b/tests/test_synthesize_priority.py new file mode 100644 index 0000000..a8fecb3 --- /dev/null +++ b/tests/test_synthesize_priority.py @@ -0,0 +1,50 @@ +"""The documented verdict priority IS the honesty policy — pin it. + +`synthesize` sat in `__all__` with zero documentation, so the one function +encoding the policy was the one you had to read source to find. Now that the +README states the order, this test makes the README a claim the code must keep. +""" +import dataclasses + +import pytest + +from provmirror import Signal, synthesize +from provmirror import AUTHENTIC, SYNTHETIC, TAMPERED + + +def _sig(direction): + kw = {} + for f in dataclasses.fields(Signal): + kw[f.name] = direction if f.name == "direction" else "test" + return Signal(**kw) + + +@pytest.mark.parametrize("directions,expected", [ + ([TAMPERED, AUTHENTIC, SYNTHETIC], "TAMPERED"), + ([TAMPERED], "TAMPERED"), + ([AUTHENTIC, SYNTHETIC], "CONFLICTING"), + ([SYNTHETIC], "SYNTHETIC"), + ([AUTHENTIC], "AUTHENTIC-SIGNED"), + ([], "UNVERIFIED"), +]) +def test_documented_priority_order(directions, expected): + assert synthesize([_sig(d) for d in directions]) == expected + + +def test_no_signal_is_unknown_never_fake(): + """The row the package exists for. Absence of a provenance signal is + overwhelmingly common in authentic media too, so answering 'fake' when the + tool simply cannot tell would be worse than having no tool.""" + verdict = synthesize([]) + assert verdict == "UNVERIFIED" + assert "SYNTHETIC" not in verdict and "TAMPERED" not in verdict + + +def test_readme_table_matches_the_code(): + import re + from pathlib import Path + for name in ("README.md", "README_KO.md"): + txt = (Path(__file__).resolve().parents[1] / name).read_text(encoding="utf-8") + rows = re.findall(r'^\|\s*[1-5]\s*\|\s*`([A-Z-]+)`\s*\|', txt, re.M) + assert rows == ["TAMPERED", "CONFLICTING", "SYNTHETIC", + "AUTHENTIC-SIGNED", "UNVERIFIED"], f"{name}: {rows}"