Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions README_KO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions tests/test_synthesize_priority.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading