Skip to content

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=True always.
  • 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 our create_evidence_block(). Same hashing, different storage.
  • REST: Evidence blocks validate against objects.yml Block schema.
  • Beacon: Renders evidence blocks in investigation detail — query hash, result hash, provenance.
  • DES: Signals enter Signal → SignalCluster → Investigation → Finding → Effect lifecycle.

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=True is 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