core/workbench/field_evidence.py
Shay 53ff9359a8 feat(workbench): Wave M C3 field substrate — persist-first field evidence
Makes the CL(4,1) field geometry legible, honestly. The engine owns the field;
this surfaces ONLY the exact scalar invariants it computes — never the raw
multivector — so the workbench shows "this is the geometry, it's exact, it
can't fake coherence" without a decorative blob or any motion.

Persist-first (the C3 gating work was Python, not React): the honest scalars
were computed live per turn but discarded. Now captured per turn into the
journal so a read-only surface has real evidence.

Backend:
  - workbench/field_evidence.py: FieldEvidence computed from the engine result —
    exact versor_condition, field_valid (vs the 1e-6 ceiling), a content-
    addressed field_digest (sha256 of the engine-canonical array bytes), and
    cga_inner(before, after) as the exact transition value. Raw field bytes
    never cross the boundary: only floats + digests. validate() is fail-closed —
    field_valid can never disagree with versor_condition vs the ceiling (the
    wrong=0 analogue for the geometry). No engine math re-implemented
    (versor_condition / cga_inner imported from algebra; bytes via array_codec).
  - workbench/schemas.py: FieldEvidence dataclass; field_evidence on
    ChatTurnResult + TurnJournalEntrySchema. schema-snapshot.json regenerated.
  - workbench/journal.py + api.py: persisted at from_chat_turn; first-class read
    endpoint GET /trace/{turn_id}/field (trace facet, consistent with /pipeline).
  - workbench/replay.py: field_evidence classified CRITICAL — replay now also
    proves field determinism (digest + scalars must match on re-execution).

Frontend:
  - types/api.ts FieldEvidence + field_evidence passthrough; client/query hook;
    FieldInvariantCard (measured value vs ceiling, cga_inner transition, digests;
    honest missing_evidence; no blob, no motion); Trace route Field tab.

Honest-empty for pre-widening journal rows (missing_evidence). Deferred:
cross-turn field-coherence trends, session-level field persistence.

Validation: 138 workbench/practice Python tests (incl. non-vacuous field guards
+ replay field-determinism); 465/465 frontend incl. schemaDrift; pnpm build
clean; git diff --check clean. No generate.derivation / reliability_gate /
stream / field.propagate / vault.store imports.
2026-06-13 16:26:46 -07:00

169 lines
6.2 KiB
Python

"""C3 field-substrate evidence — exact scalar invariants for a turn's field.
The engine owns the CL(4,1) field; this module surfaces, read-only, only the
EXACT scalars the engine computes over it — ``versor_condition`` (the
``< 1e-6`` validity invariant) and the ``cga_inner`` transition value — plus a
content-addressed ``field_digest``. The raw multivector NEVER leaves this
module: only floats and a hash cross the boundary, so the workbench can show
"this is the geometry, it's exact, it can't fake coherence" without rendering a
decorative blob.
No engine math is re-implemented here: ``versor_condition`` and ``cga_inner``
are imported from :mod:`algebra` and the field bytes are encoded by the
engine-owned :func:`core.array_codec.encode_array`.
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
import numpy as np
from algebra import cga_inner, versor_condition
from core.array_codec import encode_array
from workbench.schemas import FieldEvidence
# The non-negotiable CORE invariant (CLAUDE.md; enforced at field/state.py:223
# as ``versor_condition(F) >= 1e-6`` -> raise). Surfaced so the UI can render
# the measured value against the bound rather than asserting validity blindly.
VERSOR_CONDITION_CEILING: float = 1e-6
def _field_digest(field_array: Any) -> str:
"""Content-addressed digest of a field multivector — provenance, not the blob.
Hashes the engine-canonical ``encode_array`` payload (dtype + shape + raw
bytes), so the digest fully determines the array yet exposes none of it.
"""
payload = encode_array(np.asarray(field_array))
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def missing_field_evidence(
*,
trace_hash: str | None,
reason: str,
) -> FieldEvidence:
"""Honest absence — old journal rows have no persisted field evidence."""
return FieldEvidence(
schema_version="field_evidence_v1",
status="missing_evidence",
missing_reason=reason,
trace_hash=trace_hash,
versor_condition=None,
versor_condition_ceiling=VERSOR_CONDITION_CEILING,
field_valid=None,
field_digest=None,
parent_field_digest=None,
transition_inner_product=None,
)
def validate(record: FieldEvidence) -> None:
"""Fail-closed honesty gate.
A recorded record must carry the scalars, and ``field_valid`` must agree
with ``versor_condition < ceiling`` EXACTLY — the field card can never claim
a valid field while the measured condition breaches the bound (the wrong=0
analogue for the geometry surface).
"""
if record.status != "recorded":
return
if record.versor_condition is None or record.field_valid is None:
raise ValueError(
"recorded field evidence missing versor_condition / field_valid"
)
if record.field_digest is None:
raise ValueError("recorded field evidence missing field_digest")
if record.field_valid != (record.versor_condition < record.versor_condition_ceiling):
raise ValueError(
"field_valid disagrees with versor_condition vs ceiling: "
f"valid={record.field_valid} condition={record.versor_condition:.3e} "
f"ceiling={record.versor_condition_ceiling:.3e}"
)
def field_evidence_from_result(result: Any) -> FieldEvidence:
"""Build validated field evidence from a live engine turn result.
Reads the engine's ``field_state_after`` / ``field_state_before`` and
``versor_condition``; emits only scalars + digests. Returns an honest
``missing_evidence`` record when the result carries no field state.
"""
trace_hash = str(getattr(result, "trace_hash", "") or "") or None
after = getattr(result, "field_state_after", None)
if after is None or getattr(after, "F", None) is None:
return missing_field_evidence(
trace_hash=trace_hash, reason="field_state_not_available"
)
measured = getattr(result, "versor_condition", None)
condition = (
float(measured) if measured is not None else float(versor_condition(after.F))
)
field_digest = _field_digest(after.F)
before = getattr(result, "field_state_before", None)
if before is not None and getattr(before, "F", None) is not None:
parent_field_digest: str | None = _field_digest(before.F)
transition_inner_product: float | None = float(cga_inner(before.F, after.F))
else:
parent_field_digest = None
transition_inner_product = None
record = FieldEvidence(
schema_version="field_evidence_v1",
status="recorded",
missing_reason=None,
trace_hash=trace_hash,
versor_condition=condition,
versor_condition_ceiling=VERSOR_CONDITION_CEILING,
field_valid=condition < VERSOR_CONDITION_CEILING,
field_digest=field_digest,
parent_field_digest=parent_field_digest,
transition_inner_product=transition_inner_product,
)
validate(record)
return record
def _coerce(raw: Any) -> FieldEvidence:
if isinstance(raw, FieldEvidence):
return raw
if not isinstance(raw, dict):
raise ValueError("field_evidence must be an object")
return FieldEvidence(
schema_version=raw["schema_version"],
status=raw["status"],
missing_reason=raw.get("missing_reason"),
trace_hash=raw.get("trace_hash"),
versor_condition=raw.get("versor_condition"),
versor_condition_ceiling=raw.get(
"versor_condition_ceiling", VERSOR_CONDITION_CEILING
),
field_valid=raw.get("field_valid"),
field_digest=raw.get("field_digest"),
parent_field_digest=raw.get("parent_field_digest"),
transition_inner_product=raw.get("transition_inner_product"),
)
def field_evidence_from_journal_entry(entry: Any) -> FieldEvidence:
"""Project a persisted journal row into the field-evidence read model."""
raw = getattr(entry, "field_evidence", None)
if raw is None:
return missing_field_evidence(
trace_hash=getattr(entry, "trace_hash", None),
reason="field_evidence_not_persisted",
)
record = _coerce(raw)
validate(record)
return record