Skip to content
Open
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
100 changes: 100 additions & 0 deletions docs/EVIDENCE_CLAIM_DIVERGENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Evidence–Claim Divergence

## A reproducible framework for measuring calibration in autonomous systems

**Status:** T2 research instrument. This document separates the scientific proposal from the reference measurement substrate implemented in `verifiable/ecd.py`.

## Abstract

Reliable evaluation of advanced AI systems requires more than measuring task accuracy or benchmark performance. Existing evaluation methods primarily assess whether a system reaches a correct outcome, while providing limited insight into whether the system's observable claims remain consistent with independently verifiable evidence throughout execution.

Evidence–Claim Divergence (ECD) is an observable phenomenon: structural disagreement between externally observable system claims and independently verifiable execution evidence. Hallucination Distance (HD) is a family of empirical estimators over observable claims and evidence graphs. The Evidence–Reasoning Benchmark (ERB) is the proposed evaluation protocol for deterministic traces, evidence graphs, controlled perturbations, and public datasets. AEGIS-Ω is the reference measurement substrate that reduces measurement variance with deterministic replay, canonical serialization, provenance tracking, and reproducible evidence generation.

## Formal objects

Let `C ∈ 𝒞` represent observable claims emitted by an autonomous system, including textual assertions, API calls, state declarations, externally visible outputs, and structured metadata.

Let `E ∈ ℰ` represent an immutable evidence graph constructed from independently observed execution artifacts, including telemetry, logs, execution traces, provenance records, attestations, and verified state transitions.

ECD is defined as:

```text
ECD(C, E) = inf over M∈𝓜 of d(M(C), E)
```

where `M` maps heterogeneous claims into graph space and `d` is a graph distance over attributed evidence graphs. ECD is therefore a latent structural property of the claim/evidence boundary rather than a directly observed scalar.

## Reference Hallucination Distance estimator

Because ECD is latent, the reference implementation estimates it as:

```text
HD = ω1 D_exec + ω2 M_omit + ω3 A_unsupported + ω4 C_contradict + ω5 E_calib
```

The implemented components are:

| Component | Meaning |
| --- | --- |
| `D_exec` | Claim value disagrees with verified evidence for the same subject and predicate. |
| `M_omit` | Claim provides no declared evidence lineage. |
| `A_unsupported` | Claim cites evidence that is missing or unverified. |
| `C_contradict` | Claims emit multiple values for the same subject and predicate. |
| `E_calib` | Expressed confidence differs from observed claim support. |

The implementation reports `(HD, Q_evidence)` instead of folding instrumentation quality into the metric:

```text
Q_evidence = |E_verified| / |E_total|
```

## Temporal dynamics

Hallucination Delta is computed over deterministic ticks, not wall-clock time:

```text
HD_Δ = ΔHD / Δticks
```

Positive values indicate increasing structural divergence. Negative values indicate convergence toward evidence.

## Reference implementation

The current reference implementation is `verifiable/ecd.py`. It is deterministic, uses integer parts-per-million confidence values, represents scores as exact rational numbers, and can hash an estimator result into the existing AEGIS-Ω `LineageChain` for reproducible witness generation.

Run the checks with:

```bash
python3 verifiable/test_ecd.py
```

The tests cover perfect alignment, controlled unsupported/contradictory claim insertion, deterministic witness hashing, tamper evidence, and positive Hallucination Delta under evidence divergence.

## Evidence–Reasoning Benchmark protocol

ERB should evaluate Hallucination Distance over trajectories containing:

- observable claims;
- evidence graph nodes;
- deterministic sequence ticks;
- induced perturbations;
- reference labels.

The benchmark has three tracks:

1. **Track A:** estimate Hallucination Distance.
2. **Track B:** predict Hallucination Delta across long execution horizons.
3. **Track C:** infer structural evidence dependencies.

## Metrological properties

Any estimator in the HD family should satisfy:

- **Sensitivity:** small structural perturbations produce measurable changes.
- **Monotonicity:** increasing corruption should not decrease measured divergence.
- **Repeatability:** independent implementations produce statistically equivalent estimates over identical evidence.
- **Observer invariance:** equivalent evidence graphs yield equivalent measurements regardless of implementation.

## Scope and limitations

This framework measures claim/evidence alignment. It does not directly measure truthfulness, intent, consciousness, or internal reasoning. Estimator behavior depends on the graph distance, evidence quality depends on instrumentation, and weights require empirical calibration before promotion beyond T2.
2 changes: 2 additions & 0 deletions verifiable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ one.
| `chain.py` | The domain-agnostic envelope — `canon()` (RFC 8785 → bytes, rejects float), `sha256_hex()`, `StageRecord`, `LineageChain` (append / `terminal_hash` / `certify`). The genomics proof inlines this for zero-dependency portability; here it is shared infra. |
| `compliance_pipeline.py` | A **regulated decision-audit** pipeline (`INTAKE → EXTRACT → SCORE → DECISION`) — AEGIS's stated market: EU AI Act Article 12 tamper-evident decision records. Integer scorecard, adverse-action reason codes, integer threshold. |
| `test_generality.py` | The proof. Exit 0 = all four claims hold. |
| `ecd.py` | Deterministic reference estimator for Evidence–Claim Divergence / Hallucination Distance over claims and evidence nodes. |
| `test_ecd.py` | Estimator checks for alignment, unsupported claims, contradiction, deterministic witness hashing, tamper evidence, and Hallucination Delta. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire the ECD checks into CI

This adds test_ecd.py as the estimator check suite, but I checked the verifiable workflow (.github/workflows/verifiable-proofs.yml:50-87) and the session certifier proof list (verifiable/certify_all.py:41-47), and neither invokes it. As a result, PRs changing verifiable/ecd.py still get a green CI/session certificate even if these new checks fail; please add python3 test_ecd.py to the workflow and PROOFS list, updating the pinned session certificate as needed.

Useful? React with 👍 / 👎.


## What is proven

Expand Down
162 changes: 162 additions & 0 deletions verifiable/ecd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Evidence–Claim Divergence reference estimator (T2 research instrument).

This module gives the ECD / Hallucination Distance paper a small, deterministic,
stdlib-only reference implementation over observable claims and evidence nodes. It
is intentionally conservative: it measures claim/evidence alignment, not truth,
intent, or model quality.
"""
from __future__ import annotations

from dataclasses import dataclass
from fractions import Fraction
from typing import Iterable

from chain import LineageChain


@dataclass(frozen=True)
class Claim:
claim_id: str
subject: str
predicate: str
value: str
confidence_ppm: int = 1_000_000
evidence_ids: tuple[str, ...] = ()


@dataclass(frozen=True)
class EvidenceNode:
evidence_id: str
subject: str
predicate: str
value: str
verified: bool = True


@dataclass(frozen=True)
class HDWeights:
execution: int = 1
omission: int = 1
unsupported: int = 1
contradiction: int = 1
calibration: int = 1


@dataclass(frozen=True)
class HDResult:
execution_divergence: Fraction
evidence_omission: Fraction
unsupported_assertions: Fraction
contradictory_claims: Fraction
calibration_mismatch: Fraction
hallucination_distance: Fraction
evidence_quality: Fraction
verified_evidence: int
total_evidence: int

def as_payload(self) -> dict:
return {
"execution_divergence_ppm": ppm(self.execution_divergence),
"evidence_omission_ppm": ppm(self.evidence_omission),
"unsupported_assertions_ppm": ppm(self.unsupported_assertions),
"contradictory_claims_ppm": ppm(self.contradictory_claims),
"calibration_mismatch_ppm": ppm(self.calibration_mismatch),
"hallucination_distance_ppm": ppm(self.hallucination_distance),
"evidence_quality_ppm": ppm(self.evidence_quality),
"verified_evidence": self.verified_evidence,
"total_evidence": self.total_evidence,
}


def ppm(x: Fraction) -> int:
return int(x * 1_000_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve sub-ppm HD in witnesses

For ERB-sized traces with more than 1,000,000 weighted opportunities, a real non-zero divergence below one ppm is floored to 0 here before measurement_chain() hashes as_payload(). That lets distinct exact Fraction results, including clean versus one tiny mismatch, produce identical witness payloads despite the estimator keeping exact rationals; serialize numerator/denominator pairs or another lossless form for chain payloads.

Useful? React with 👍 / 👎.



def _claim_key(claim: Claim) -> tuple[str, str]:
return claim.subject, claim.predicate


def _evidence_key(node: EvidenceNode) -> tuple[str, str]:
return node.subject, node.predicate


def estimate_hd(
claims: Iterable[Claim],
evidence: Iterable[EvidenceNode],
weights: HDWeights = HDWeights(),
) -> HDResult:
claim_list = sorted(claims, key=lambda c: c.claim_id)
evidence_list = sorted(evidence, key=lambda e: e.evidence_id)
total_claims = len(claim_list)
total_evidence = len(evidence_list)
verified_evidence = sum(1 for node in evidence_list if node.verified)

if total_claims == 0:
zero = Fraction(0, 1)
quality = Fraction(verified_evidence, total_evidence) if total_evidence else zero
return HDResult(zero, zero, zero, zero, zero, zero, quality, verified_evidence, total_evidence)

evidence_by_id = {node.evidence_id: node for node in evidence_list}
verified_by_key: dict[tuple[str, str], list[EvidenceNode]] = {}
for node in evidence_list:
if node.verified:
verified_by_key.setdefault(_evidence_key(node), []).append(node)

Comment on lines +90 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Duplicate evidence ids overwrite 🐞 Bug ☼ Reliability

estimate_hd() silently overwrites earlier EvidenceNode entries when multiple nodes share the same
evidence_id, which can change which evidence is treated as "supporting" and therefore change HD
results.
Because ties are not rejected, results can become sensitive to the iteration order of malformed
inputs (e.g., when evidence is supplied from an unordered iterable) despite the module’s
deterministic intent.
Agent Prompt
### Issue description
`evidence_by_id` is built with a dict comprehension keyed by `evidence_id`, so duplicates are silently dropped (last one wins). This is a data-integrity hazard and can make outputs depend on which duplicate happens to be retained.

### Issue Context
The module explicitly positions itself as a deterministic reference estimator, so ambiguous identifiers should be rejected early and loudly.

### Fix Focus Areas
- verifiable/ecd.py[5-8]
- verifiable/ecd.py[90-106]

### Suggested fix
- Validate uniqueness:
  - after sorting, scan `claim_list` for duplicate `claim_id` and raise `ValueError` if found
  - scan `evidence_list` for duplicate `evidence_id` and raise `ValueError` if found
- (Alternative) If duplicates are intended, change `evidence_by_id` to map IDs to a list and define a deterministic selection rule; but rejection is usually better for a measurement substrate.
- Add a targeted test: duplicate evidence IDs should raise `ValueError`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

execution_mismatches = 0
omissions = 0
unsupported = 0
calibration_error = Fraction(0, 1)
seen_values: dict[tuple[str, str], set[str]] = {}

for claim in claim_list:
key = _claim_key(claim)
supporting = [evidence_by_id[eid] for eid in claim.evidence_ids if eid in evidence_by_id and evidence_by_id[eid].verified]
candidates = supporting or verified_by_key.get(key, [])
matching = [node for node in candidates if _evidence_key(node) == key and node.value == claim.value]

if not claim.evidence_ids:
omissions += 1
elif not supporting:
unsupported += 1
Comment on lines +121 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count mixed bad citations as unsupported

When a claim cites both one valid verified evidence id and another missing or unverified id, supporting is non-empty, so this branch leaves unsupported_assertions at zero and the HD can still report perfect alignment. The new estimator docs define A_unsupported as a claim citing evidence that is missing or unverified (docs/EVIDENCE_CLAIM_DIVERGENCE.md:41), so a bad lineage entry can currently be hidden just by appending any good evidence id; check each cited id rather than only whether any support exists.

Useful? React with 👍 / 👎.

if not matching:
execution_mismatches += 1

observed_correct = Fraction(1, 1) if matching else Fraction(0, 1)
expressed = Fraction(claim.confidence_ppm, 1_000_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject confidence values outside ppm range

When a caller supplies an impossible confidence_ppm such as 1_500_000 or a negative value, this code treats it as a valid confidence fraction instead of rejecting it. Since the docs define confidence as integer parts-per-million, out-of-range inputs can create calibration mismatches above 1 or penalize a fully supported claim even though the claim/evidence pair is aligned; validate 0 <= confidence_ppm <= 1_000_000 before using it in the estimator.

Useful? React with 👍 / 👎.

calibration_error += abs(expressed - observed_correct)
Comment on lines +126 to +128

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Out-of-range confidence accepted 🐞 Bug ≡ Correctness

estimate_hd() converts Claim.confidence_ppm directly into a Fraction with no bounds checks, so
negative values or values > 1_000_000 produce expressed confidence outside [0,1].
This can inflate calibration_mismatch (and overall HD) beyond the intended normalized scale, making
results hard to interpret and compare.
Agent Prompt
### Issue description
`Claim.confidence_ppm` is treated as parts-per-million probability, but it is not validated. Out-of-range values yield expressed confidence outside [0,1], distorting calibration and HD.

### Issue Context
The docs and code use PPM as a deterministic probability representation; that implies a fixed domain of valid values.

### Fix Focus Areas
- verifiable/ecd.py[19-27]
- verifiable/ecd.py[126-128]

### Suggested fix
- Add validation (preferably in `Claim.__post_init__()`):
  - require `0 <= confidence_ppm <= 1_000_000`
  - raise `ValueError` on violation
- Add a small test case that constructing a claim with `confidence_ppm=-1` or `1_000_001` raises `ValueError`.
- (If you explicitly want to allow out-of-range), document that behavior and its impact on calibration/HD scaling.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

seen_values.setdefault(key, set()).add(claim.value)

contradictory_groups = sum(1 for values in seen_values.values() if len(values) > 1)
possible_groups = len(seen_values) or 1

execution = Fraction(execution_mismatches, total_claims)
omission = Fraction(omissions, total_claims)
unsupported_rate = Fraction(unsupported, total_claims)
contradiction = Fraction(contradictory_groups, possible_groups)
calibration = calibration_error / total_claims
quality = Fraction(verified_evidence, total_evidence) if total_evidence else Fraction(0, 1)

weight_total = weights.execution + weights.omission + weights.unsupported + weights.contradiction + weights.calibration
hd = (
weights.execution * execution
+ weights.omission * omission
+ weights.unsupported * unsupported_rate
+ weights.contradiction * contradiction
+ weights.calibration * calibration
) / weight_total
Comment on lines +141 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Unchecked weight_total division 🐞 Bug ≡ Correctness

In estimate_hd(), caller-supplied HDWeights can sum to 0 (or include negative values), causing a
ZeroDivisionError or semantically invalid/negative HD output.
This makes the estimator fragile for any downstream experiment that tunes weights.
Agent Prompt
### Issue description
`estimate_hd()` divides by `weight_total` without validating it. If weights sum to 0, the function raises `ZeroDivisionError`; if weights are negative, the returned HD can be negative or otherwise outside the intended scale.

### Issue Context
Weights are part of the public API and are likely to be tuned by downstream users.

### Fix Focus Areas
- verifiable/ecd.py[39-45]
- verifiable/ecd.py[141-148]

### Suggested fix
- Add validation (either in `HDWeights.__post_init__()` or at the start of `estimate_hd()`):
  - require all weights to be non-negative integers (or at least `>= 0`)
  - require `weight_total > 0`
  - raise `ValueError` with a clear message if invalid.
- (Optional) Add a small test that passing all-zero weights raises `ValueError` (instead of crashing with `ZeroDivisionError`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return HDResult(execution, omission, unsupported_rate, contradiction, calibration, hd, quality, verified_evidence, total_evidence)


def hd_delta(previous: HDResult, current: HDResult, elapsed_ticks: int) -> Fraction:
if elapsed_ticks <= 0:
raise ValueError("elapsed_ticks must be positive")
return (current.hallucination_distance - previous.hallucination_distance) / elapsed_ticks


def measurement_chain(result: HDResult) -> LineageChain:
chain = LineageChain()
chain.append("ECD_ESTIMATE", result.as_payload())
return chain
57 changes: 57 additions & 0 deletions verifiable/test_ecd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Deterministic checks for the Evidence–Claim Divergence estimator."""
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

from ecd import Claim, EvidenceNode, estimate_hd, hd_delta, measurement_chain, ppm


def test_perfect_alignment_has_zero_hd_and_full_quality():
evidence = [EvidenceNode("e1", "run", "exit_code", "0")]
claims = [Claim("c1", "run", "exit_code", "0", evidence_ids=("e1",))]
result = estimate_hd(claims, evidence)
assert result.hallucination_distance == 0
assert result.evidence_quality == 1


def test_unsupported_and_contradictory_claims_increase_hd():
evidence = [EvidenceNode("e1", "run", "exit_code", "0")]
clean = estimate_hd([Claim("c1", "run", "exit_code", "0", evidence_ids=("e1",))], evidence)
corrupted = estimate_hd([
Claim("c1", "run", "exit_code", "0", evidence_ids=("e1",)),
Claim("c2", "run", "exit_code", "1", confidence_ppm=900_000, evidence_ids=("missing",)),
], evidence)
assert corrupted.hallucination_distance > clean.hallucination_distance
assert corrupted.unsupported_assertions > 0
assert corrupted.contradictory_claims > 0


def test_measurement_chain_is_reproducible_and_tamper_evident():
result = estimate_hd([Claim("c1", "run", "exit_code", "0", evidence_ids=("e1",))], [EvidenceNode("e1", "run", "exit_code", "0")])
chain_a = measurement_chain(result)
chain_b = measurement_chain(result)
assert chain_a.terminal_hash() == chain_b.terminal_hash()
chain_a.records[0].output["hallucination_distance_ppm"] = 1
assert chain_a.certify()["is_valid"] is False


def test_hd_delta_uses_deterministic_ticks():
evidence = [EvidenceNode("e1", "run", "exit_code", "0")]
before = estimate_hd([Claim("c1", "run", "exit_code", "0", evidence_ids=("e1",))], evidence)
after = estimate_hd([Claim("c1", "run", "exit_code", "1", confidence_ppm=1_000_000, evidence_ids=("e1",))], evidence)
assert ppm(hd_delta(before, after, elapsed_ticks=2)) > 0


if __name__ == "__main__":
tests = [
test_perfect_alignment_has_zero_hd_and_full_quality,
test_unsupported_and_contradictory_claims_increase_hd,
test_measurement_chain_is_reproducible_and_tamper_evident,
test_hd_delta_uses_deterministic_ticks,
]
for test in tests:
test()
print(f"ECD estimator checks passed: {len(tests)}")
Loading