Evidence Model
Status & scope
- Module:
lens/evidence.py,lens/threshold.py,lens/models/evidence.py,lens/models/signal.py - Milestone: Phase 1 (complete)
Purpose
Frozen evidence blocks with deterministic SHA-256 hashes, and threshold-driven signal generation. Enforces Invariants #3 (blocks are evidence), #4 (frozen means frozen), and #5 (editions require frozen evidence).
Public API
create_evidence_block(spec, inputs, output, node_id, layer_timestamps) → EvidenceBlock
- Creates frozen evidence block from lens computation.
query_hash: SHA-256 of{lens_id, lens_version, lens_type, layers, inputs}— deterministic.result_hash: SHA-256 of output dict — deterministic.frozen=Truealways.- Pure function (except UUID generation for block ID).
evaluate(score, thresholds, signal_type, lens_id, lens_type, metadata) → Signal | None
- Threshold evaluation → Signal on crossing.
score >= confirm→ severity "high", threshold_crossed "confirm"score >= candidate→ severity "medium", threshold_crossed "candidate"score < candidate→ None (below threshold, no action — Invariant 7)- Pure function (except UUID for signal_id).
Dataclasses (all frozen=True)
EvidenceBlock
| Field | Type | Default | Notes |
|---|---|---|---|
| id | str | — | UUID |
| block_kind | str | "lens_output" | Block type |
| lens_id | str | "" | Source lens |
| lens_version | str | "" | Source lens version |
| lens_type | str | "" | Source lens type |
| query_hash | str | "" | SHA-256 of spec + inputs |
| result_hash | str | "" | SHA-256 of output |
| payload | dict | {} | The computation result |
| provenance | Provenance | None | None | Where/how produced |
| frozen | bool | True | Always True after creation |
| attestation | str | None | None | Human sign-off (set later) |
Provenance
| Field | Type | Default | Notes |
|---|---|---|---|
| lens_name | str | — | Human-readable lens name |
| lens_version | str | — | Semver |
| layers_used | tuple[str, ...] | () | Layer names |
| layer_timestamps | dict | {} | Layer name → last refresh |
| compute_timestamp | str | "" | ISO 8601 UTC |
| node_id | str | "" | Compute node identifier |
Signal
| Field | Type | Default | Notes |
|---|---|---|---|
| signal_id | str | — | UUID |
| signal_type | str | — | DES signal type name |
| severity | str | — | "low" | "medium" | "high" | "critical" |
| lens_id | str | "" | Source lens |
| lens_type | str | "" | Source lens type |
| threshold_crossed | str | "" | "confirm" | "candidate" |
| score | float | 0.0 | The score that triggered |
| evidence_id | str | "" | Link to evidence block |
| metadata | dict | {} | Additional context |
| timestamp | str | "" | ISO 8601 UTC |
Invariants Enforced
| Invariant | How Enforced |
|---|---|
| #3: Blocks are evidence | query_hash = SHA-256 of spec + inputs. Same inputs → same hash. |
| #4: Frozen means frozen | frozen=True on dataclass. result_hash locks content. |
| #5: Editions require frozen evidence | Evidence block is created frozen — cannot be modified after. |
| #6: AI assists, humans attest | attestation field starts as None — only set by human action. |
| #7: "No action" is a decision | evaluate() returns None below threshold — explicit no-signal. |
Deterministic Hashing
def _deterministic_hash(data: dict) -> str:
canonical = json.dumps(data, sort_keys=True, default=str)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
Same inputs → same hash. Always. This is the foundation of auditability.
DES Block Schema Mapping
When integrated with the platform, EvidenceBlock maps to the DES v2 Block schema:
| EvidenceBlock field | DES Block field | Notes |
|---|---|---|
| id | block_id | UUID |
| block_kind | block_kind | "lens_output" |
| lens_id | evidence_class | Lens identifier |
| query_hash | query_hash | Source of truth |
| result_hash | result_hash | Content lock |
| payload | materialization | The result data |
| frozen | lifecycle_stage | "frozen" |
| provenance | provenance | Embedded object |
Test Fixtures
| Test | Input | Expected | File |
|---|---|---|---|
| Deterministic hash | Same inputs twice | Same hash | tests/test_evidence.py |
| Different inputs → different hash | Different inputs | Different hash | tests/test_evidence.py |
| Evidence block is frozen | Create block | frozen=True, cannot mutate | tests/test_evidence.py |
| Query hash covers spec + inputs | Modify one layer | Hash changes | tests/test_evidence.py |
| Threshold: score above confirm | score=0.9, confirm=0.8 | Signal(severity="high") | tests/test_threshold.py |
| Threshold: score at candidate | score=0.6, candidate=0.5 | Signal(severity="medium") | tests/test_threshold.py |
| Threshold: score below | score=0.3, candidate=0.5 | None | tests/test_threshold.py |
| Signal has correct metadata | metadata dict | Preserved in signal | tests/test_threshold.py |
Current test count: 17 evidence tests + 13 threshold tests = 30 tests (all passing)
File Layout
lens/
evidence.py ← create_evidence_block(), _deterministic_hash()
threshold.py ← evaluate()
models/
evidence.py ← EvidenceBlock, Provenance
signal.py ← Signal
Integration Points
- Cortex:
create_block()+attach_block_to_result()in cortex tools wraps ourcreate_evidence_block(). Same hashing, different storage. - REST: Evidence blocks validate against
objects.ymlBlock schema. - Beacon: Renders evidence blocks in investigation detail — query hash, result hash, provenance.
- DES: Signals enter
Signal → SignalCluster → Investigation → Finding → Effectlifecycle.
DO NOT
- Allow mutation after creation — frozen dataclass enforces this
- Skip hashing — every evidence block must have both query_hash and result_hash
- Generate signals below candidate threshold — Invariant 7 says "no action" is explicit
- Set attestation in code — only human action sets this field (Invariant 6)
- Use non-deterministic serialization —
sort_keys=Trueis mandatory
Depends on: component.prism.universal-lens-parser
Realizes: product.lens
Required by: component.prism.bayesian-posterior-engine, component.prism.incremental-updates, component.prism.lens-families, component.prism.operational-lifecycle, component.prism.rendering-architecture, component.prism.semantic-adapter, component.prism.serialization