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.
This commit is contained in:
Shay 2026-06-13 16:26:46 -07:00
parent 4991f61133
commit 53ff9359a8
15 changed files with 726 additions and 13 deletions

View file

@ -86,6 +86,7 @@ export type ChatTurnSummary = {
mutation_state: "none" | "transient" | "proposal_only" | "ratified";
leeway_evidence?: LeewayEvidence | null;
pipeline_record?: CognitivePipelineRecord | null;
field_evidence?: FieldEvidence | null;
};
```
@ -112,6 +113,7 @@ export type TraceDetail = {
exhausted: boolean | null;
};
pipeline_record?: CognitivePipelineRecord | null;
field_evidence?: FieldEvidence | null;
raw?: unknown;
};
```
@ -159,6 +161,35 @@ pre-widening rows. The Trace route renders the record as a deterministic stage
rail, DAG, and selected-stage detail inspector; those views are projections of
`stages` and `edges` only, not replay-derived cognition.
## FieldEvidence (C3 field substrate)
```ts
export type FieldEvidence = {
schema_version: "field_evidence_v1";
status: "recorded" | "missing_evidence";
missing_reason: string | null;
trace_hash: string | null;
versor_condition: number | null; // exact, measured over field_state_after
versor_condition_ceiling: number; // 1e-6 — the CLAUDE.md invariant bound
field_valid: boolean | null; // versor_condition < ceiling
field_digest: string | null; // sha256 of the canonical field bytes
parent_field_digest: string | null; // sha256 of field_state_before, or null
transition_inner_product: number | null; // cga_inner(before, after), or null
};
```
`field_evidence` is the geometry under a turn: only the engine's EXACT scalar
invariants and a content-addressed digest cross the boundary — never the raw
CL(4,1) multivector. It is required for newly journaled turns and nullable for
pre-widening rows; the UI renders `missing_evidence` when absent and never
reconstructs the field. `field_valid` is consistency-checked against the
ceiling at construction, so the Field tab can never claim a valid field while
`versor_condition` breaches `1e-6` (the wrong=0 analogue for the geometry). The
first-class read endpoint is `GET /trace/{turn_id}/field`; it returns a
`FieldEvidence` directly. The Trace route's **Field** tab renders it as the
measured value against the ceiling, the `cga_inner` transition, and the digests
— inspectable exact numbers and invariant status, no decorative geometry.
---
# Proposal

View file

@ -38,10 +38,10 @@ the Replay Moment makes hash-equality felt. But it is blind to the two most
it is invisible.
2. **It shows outputs and evidence but not cognition itself.** The
`CognitiveTurnPipeline` stages, the contemplation *process*, the CL(4,1)
field substrate, `versor_condition`, identity continuity. C1 and a first
C4 run-level identity projection now exist; C2/C3 remain the major blind
spots. For an audience that lives inside opaque models, *legible
deterministic cognition* is the wow.
field substrate, `versor_condition`, identity continuity. C1 (pipeline),
C2-a (contemplation), C3-a (field substrate), and a first C4 run-level
identity projection now exist. For an audience that lives inside opaque
models, *legible deterministic cognition* is the wow.
Everything below closes those two gaps on top of a mastery polish.
@ -160,13 +160,24 @@ Detailed brief pack: `docs/handoff/wave-M-phaseB-calibration-briefs-2026-06-13.m
ratification boundary, and grounded-after scenes remain report-authored
evidence. This is the first process trace; the fuller Calibration/Proposal
integrated loop is still open.
- **C3 — Field substrate (honest, read-only, hard):** `GET /field/state`
over real `FieldState` + `versor_condition` for a turn, rendered as
**inspectable exact numbers and invariant status** — `versor_condition <
1e-6` as a live "field is valid" assertion, `cga_inner` coherence as exact
values. **NOT** a decorative 3D blob; no force-directed/nondeterministic
motion. The honesty is the impressiveness: "this is the geometry, it's
exact, it can't fake coherence."
- **C3 — Field substrate (honest, read-only, hard):** real `FieldState` +
`versor_condition` for a turn, rendered as **inspectable exact numbers and
invariant status** — `versor_condition < 1e-6` as a live "field is valid"
assertion, `cga_inner` as an exact transition value. **NOT** a decorative 3D
blob; no force-directed/nondeterministic motion. The honesty is the
impressiveness: "this is the geometry, it's exact, it can't fake coherence."
**C3-a implementation note (2026-06-13):** BUILT persist-first. Per-turn
`FieldEvidence` (exact `versor_condition`, `field_valid` vs the `1e-6`
ceiling, a content-addressed `field_digest`, and `cga_inner(before, after)`)
is computed in `workbench/field_evidence.py` from the engine result and
persisted on the journal entry at `from_chat_turn` — the raw multivector
never crosses the boundary, only scalars + digests. The read endpoint is a
trace facet (`GET /trace/{turn_id}/field`, consistent with `/pipeline`) and
the surface is the Trace route's **Field** tab. `field_valid` is
consistency-checked against the ceiling at construction, so it can never claim
validity while `versor_condition` breaches `1e-6` (the wrong=0 analogue).
Honest `missing_evidence` for pre-widening journal rows. Deferred: cross-turn
field-coherence trends and session-level field persistence.
- **C4 — Identity continuity (L10/L11):** surface the engine-identity hash,
lineage chain, reboot-verification status — "the same continuous life
across restart," the deepest telos.

View file

@ -0,0 +1,153 @@
"""C3 field-substrate evidence — non-vacuous guards.
Each test fails under a specific violation of the honesty contract: a field
card that claimed ``field_valid`` while the measured ``versor_condition``
breaches the ``< 1e-6`` ceiling, a digest that leaked the raw multivector, or a
recorded record missing its scalars.
"""
from __future__ import annotations
import json
from types import SimpleNamespace
import numpy as np
import pytest
import workbench.field_evidence as fe
from field.state import FieldState
from workbench.schemas import FieldEvidence, to_data
def _versor_field() -> FieldState:
"""A valid Cl(4,1) versor field (scalar identity) — versor_condition == 0."""
f = np.zeros(32, dtype=np.float64)
f[0] = 1.0
return FieldState(F=f)
def _non_versor_field() -> FieldState:
"""A field that breaches the versor manifold — versor_condition >= 1e-6."""
f = np.zeros(32, dtype=np.float64)
f[0] = 2.0
return FieldState(F=f)
def _result(*, after: FieldState | None, before: FieldState | None = None) -> SimpleNamespace:
return SimpleNamespace(
trace_hash="trace-abc",
field_state_after=after,
field_state_before=before,
# versor_condition deliberately absent so the module computes it from F
# via the real engine operator (the honest path).
)
class TestFromResult:
def test_valid_field_records_field_valid_true(self) -> None:
record = fe.field_evidence_from_result(_result(after=_versor_field()))
assert record.status == "recorded"
assert record.field_valid is True
assert record.versor_condition is not None
assert record.versor_condition < fe.VERSOR_CONDITION_CEILING
assert record.field_digest is not None and record.field_digest.startswith("sha256:")
assert record.transition_inner_product is None # no before on first turn
def test_breaching_field_records_field_valid_false_not_a_lie(self) -> None:
# The honest negative: a field off the versor manifold must report
# field_valid False, never silently True.
record = fe.field_evidence_from_result(_result(after=_non_versor_field()))
assert record.status == "recorded"
assert record.versor_condition is not None
assert record.versor_condition >= fe.VERSOR_CONDITION_CEILING
assert record.field_valid is False
def test_transition_inner_product_present_when_before_exists(self) -> None:
record = fe.field_evidence_from_result(
_result(after=_versor_field(), before=_versor_field())
)
assert record.transition_inner_product is not None
assert isinstance(record.transition_inner_product, float)
assert record.parent_field_digest is not None
def test_missing_field_state_is_honest_not_fabricated(self) -> None:
record = fe.field_evidence_from_result(_result(after=None))
assert record.status == "missing_evidence"
assert record.missing_reason == "field_state_not_available"
assert record.field_valid is None
assert record.versor_condition is None
def test_no_raw_multivector_crosses_the_boundary(self) -> None:
record = fe.field_evidence_from_result(
_result(after=_versor_field(), before=_non_versor_field())
)
serialized = json.dumps(to_data(record))
# No base64 array payload, no array literal — only scalars + digests.
assert "b64" not in serialized
assert "2.0, 0.0" not in serialized
assert "[1.0," not in serialized
class TestValidateHonestyGate:
def test_field_valid_disagreeing_with_ceiling_raises(self) -> None:
# The wrong=0 analogue: claiming a valid field above the ceiling.
bad = FieldEvidence(
schema_version="field_evidence_v1",
status="recorded",
missing_reason=None,
trace_hash="x",
versor_condition=1e-3, # breaches the ceiling
versor_condition_ceiling=fe.VERSOR_CONDITION_CEILING,
field_valid=True, # ...but claims valid
field_digest="sha256:0",
parent_field_digest=None,
transition_inner_product=None,
)
with pytest.raises(ValueError, match="field_valid disagrees"):
fe.validate(bad)
def test_recorded_without_scalars_raises(self) -> None:
bad = FieldEvidence(
schema_version="field_evidence_v1",
status="recorded",
missing_reason=None,
trace_hash="x",
versor_condition=None,
versor_condition_ceiling=fe.VERSOR_CONDITION_CEILING,
field_valid=None,
field_digest=None,
parent_field_digest=None,
transition_inner_product=None,
)
with pytest.raises(ValueError, match="missing versor_condition"):
fe.validate(bad)
def test_missing_evidence_is_not_validated(self) -> None:
# Honest absence is allowed to carry null scalars.
record = fe.missing_field_evidence(trace_hash="x", reason="not_persisted")
fe.validate(record) # must not raise
class TestDigest:
def test_digest_is_content_addressed(self) -> None:
a = _versor_field().F
b = _non_versor_field().F
assert fe._field_digest(a) == fe._field_digest(a.copy())
assert fe._field_digest(a) != fe._field_digest(b)
class TestFromJournalEntry:
def test_absent_field_evidence_reads_as_missing(self) -> None:
entry = SimpleNamespace(trace_hash="t", field_evidence=None)
record = fe.field_evidence_from_journal_entry(entry)
assert record.status == "missing_evidence"
assert record.missing_reason == "field_evidence_not_persisted"
assert record.trace_hash == "t"
def test_persisted_dict_is_coerced_and_validated(self) -> None:
source = fe.field_evidence_from_result(_result(after=_versor_field()))
entry = SimpleNamespace(trace_hash="t", field_evidence=to_data(source))
record = fe.field_evidence_from_journal_entry(entry)
assert record.status == "recorded"
assert record.field_valid is True
assert record.field_digest == source.field_digest

View file

@ -58,6 +58,7 @@
"checkpoint_emitted",
"leeway_evidence",
"pipeline_record",
"field_evidence",
"turn_id"
],
"CognitivePipelineEdge": [
@ -175,6 +176,18 @@
"cases",
"source_digest"
],
"FieldEvidence": [
"schema_version",
"status",
"missing_reason",
"trace_hash",
"versor_condition",
"versor_condition_ceiling",
"field_valid",
"field_digest",
"parent_field_digest",
"transition_inner_product"
],
"IdentityContinuity": [
"status",
"engine_identity",
@ -330,7 +343,8 @@
"trace_integrity",
"journal_digest",
"leeway_evidence",
"pipeline_record"
"pipeline_record",
"field_evidence"
],
"TurnJournalSummarySchema": [
"turn_id",

View file

@ -22,6 +22,7 @@ import type {
CalibrationClass,
ServingMetrics,
CognitivePipelineRecord,
FieldEvidence,
TurnJournalEntry,
TurnJournalSummary,
ContemplationRunDetail,
@ -186,6 +187,12 @@ export async function fetchTracePipeline(turnId: number): Promise<CognitivePipel
);
}
export async function fetchTraceField(turnId: number): Promise<FieldEvidence> {
return apiFetch<FieldEvidence>(
`/trace/${encodeURIComponent(String(turnId))}/field`,
);
}
export async function fetchAuditEvents(
limit?: number,
offset?: number,

View file

@ -26,6 +26,7 @@ import {
fetchServingMetrics,
fetchTraceTurn,
fetchTracePipeline,
fetchTraceField,
fetchTraceTurns,
fetchContemplationRun,
fetchContemplationRuns,
@ -55,6 +56,7 @@ import type {
CalibrationClass,
ServingMetrics,
CognitivePipelineRecord,
FieldEvidence,
TurnJournalEntry,
TurnJournalSummary,
EvalLaneSummary,
@ -210,6 +212,16 @@ export function useTracePipeline(turnId?: number | null) {
});
}
export function useTraceField(turnId?: number | null) {
return useQuery<FieldEvidence, WorkbenchApiError>({
queryKey: ["api", "trace", "field", turnId ?? null],
queryFn: () => fetchTraceField(turnId as number),
enabled: typeof turnId === "number",
staleTime: 30_000,
refetchOnWindowFocus: false,
});
}
export function useAuditEvents(limit?: number, offset?: number) {
return useQuery<{ items: AuditEvent[] }, WorkbenchApiError>({
queryKey: ["api", "audit", "events", limit ?? null, offset ?? null],

View file

@ -2,8 +2,14 @@ import { useEffect, useMemo, useState } from "react";
import { Eye } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { WorkbenchApiError } from "../../api/client";
import { useTracePipeline, useTraceTurn, useTraceTurns } from "../../api/queries";
import {
useTraceField,
useTracePipeline,
useTraceTurn,
useTraceTurns,
} from "../../api/queries";
import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
import { FieldInvariantCard } from "../../design/components/FieldInvariantCard/FieldInvariantCard";
import { DagViewer, type DagEdgeInput, type DagNodeInput } from "../../design/components/Dag";
import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable";
import { Panel } from "../../design/components/Panel/Panel";
@ -28,6 +34,7 @@ import { LoadingState } from "../../design/components/states/LoadingState";
import type {
CognitivePipelineRecord,
CognitivePipelineStage,
FieldEvidence,
TraceIntegrity,
TurnJournalEntry,
TurnJournalSummary,
@ -38,6 +45,7 @@ import { useEvidenceSubject } from "../evidenceContext";
const TRACE_TABS: readonly Tab[] = [
{ id: "pipeline", label: "Pipeline" },
{ id: "field", label: "Field" },
{ id: "surfaces", label: "Surfaces" },
{ id: "grounding", label: "Grounding" },
{ id: "verdicts", label: "Verdicts" },
@ -404,6 +412,36 @@ function PipelineTab({
);
}
function FieldTab({
record,
isLoading,
error,
turnId,
}: {
record?: FieldEvidence | null;
isLoading: boolean;
error: unknown;
turnId: number;
}) {
if (isLoading) {
return <LoadingState label="Loading field invariant..." />;
}
if (error) {
return (
<ErrorState
whatFailed={errorMessage(error)}
mutationStatus="No trace mutation occurred."
reproducer={`curl /trace/${turnId}/field`}
retrySafety="Retry: safe"
/>
);
}
if (!record) {
return <LoadingState label="Loading field invariant..." />;
}
return <FieldInvariantCard record={record} />;
}
function TraceRow({
turn,
selected,
@ -561,11 +599,17 @@ function TraceDetail({
pipelineRecord,
pipelineLoading,
pipelineError,
fieldEvidence,
fieldLoading,
fieldError,
}: {
turn: TurnJournalEntry;
pipelineRecord?: CognitivePipelineRecord | null;
pipelineLoading: boolean;
pipelineError: unknown;
fieldEvidence?: FieldEvidence | null;
fieldLoading: boolean;
fieldError: unknown;
}) {
const [activeTab, setActiveTab] = useState("pipeline");
return (
@ -588,6 +632,14 @@ function TraceDetail({
turnId={turn.turn_id}
/>
) : null}
{activeTab === "field" ? (
<FieldTab
record={fieldEvidence}
isLoading={fieldLoading}
error={fieldError}
turnId={turn.turn_id}
/>
) : null}
{activeTab === "surfaces" ? <SurfacesTab turn={turn} /> : null}
{activeTab === "grounding" ? <GroundingTab turn={turn} /> : null}
{activeTab === "verdicts" ? <VerdictsTab turn={turn} /> : null}
@ -608,6 +660,7 @@ export function TraceRoute() {
const turnsQuery = useTraceTurns();
const turnQuery = useTraceTurn(selectedTurnId);
const pipelineQuery = useTracePipeline(selectedTurnId);
const fieldQuery = useTraceField(selectedTurnId);
const turns = turnsQuery.data ?? [];
const filteredTurns = useMemo(() => {
@ -718,6 +771,9 @@ export function TraceRoute() {
pipelineRecord={pipelineQuery.data}
pipelineLoading={pipelineQuery.isLoading}
pipelineError={pipelineQuery.isError ? pipelineQuery.error : null}
fieldEvidence={fieldQuery.data}
fieldLoading={fieldQuery.isLoading}
fieldError={fieldQuery.isError ? fieldQuery.error : null}
/>
) : null}
</section>

View file

@ -0,0 +1,74 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { FieldInvariantCard } from "./FieldInvariantCard";
import type { FieldEvidence } from "../../../types/api";
function recorded(overrides: Partial<FieldEvidence> = {}): FieldEvidence {
return {
schema_version: "field_evidence_v1",
status: "recorded",
missing_reason: null,
trace_hash: "trace-abc",
versor_condition: 6.5e-13,
versor_condition_ceiling: 1e-6,
field_valid: true,
field_digest: "sha256:f92fa3b99b6086fdaaaa",
parent_field_digest: null,
transition_inner_product: null,
...overrides,
};
}
describe("FieldInvariantCard", () => {
it("shows the exact versor_condition and a valid assertion", () => {
render(<FieldInvariantCard record={recorded()} />);
expect(screen.getByText("field valid")).toBeInTheDocument();
expect(screen.getByTestId("field-versor-condition").textContent).toContain(
"6.500e-13",
);
});
it("renders the cga_inner transition value when a prior field exists", () => {
render(
<FieldInvariantCard
record={recorded({
parent_field_digest: "sha256:beef",
transition_inner_product: 1.0,
})}
/>,
);
expect(screen.getByTestId("field-transition")).toBeInTheDocument();
});
it("never claims valid when the field breaches the ceiling", () => {
render(
<FieldInvariantCard
record={recorded({ versor_condition: 1e-3, field_valid: false })}
/>,
);
expect(screen.queryByText("field valid")).not.toBeInTheDocument();
expect(screen.getByText("field breach")).toBeInTheDocument();
});
it("is honest about absent evidence", () => {
render(
<FieldInvariantCard
record={recorded({
status: "missing_evidence",
missing_reason: "field_evidence_not_persisted",
versor_condition: null,
field_valid: null,
field_digest: null,
})}
/>,
);
expect(screen.getByTestId("field-missing")).toBeInTheDocument();
expect(screen.getByText(/No field evidence/)).toBeInTheDocument();
});
it("does not render any raw multivector array", () => {
const { container } = render(<FieldInvariantCard record={recorded()} />);
expect(container.textContent).not.toMatch(/\[0,\s*0,\s*0/);
expect(container.textContent).not.toContain("b64");
});
});

View file

@ -0,0 +1,111 @@
import type { FieldEvidence } from "../../../types/api";
import { DigestBadge } from "../DigestBadge/DigestBadge";
/**
* C3 field-substrate surface the geometry that can't fake coherence.
*
* Renders only the EXACT scalar invariants the engine computed over a turn's
* CL(4,1) field: `versor_condition` measured against the `< 1e-6` ceiling, the
* `cga_inner` transition value, and a content-addressed `field_digest`. There
* is deliberately no multivector blob and no motion the honesty is the
* impressiveness: these are the real numbers, exact, and they decide validity.
*/
function formatCondition(value: number): string {
// Tiny invariant values (e.g. 6.5e-13) read clearest in scientific notation.
return value.toExponential(3);
}
function formatInner(value: number): string {
return Number.isInteger(value) ? value.toFixed(1) : value.toExponential(6);
}
function digestPayload(value: string | null): string | null {
if (!value) return null;
return value.replace(/^sha256:/, "");
}
export function FieldInvariantCard({ record }: { record: FieldEvidence }) {
if (record.status !== "recorded") {
return (
<section
data-testid="field-missing"
className="rounded-md border border-[var(--color-state-warning-border)] bg-[var(--color-state-warning-bg)] p-3 text-sm text-[var(--color-state-warning-text)]"
>
<h3 className="m-0 text-xs font-semibold uppercase tracking-wide">
missing_evidence
</h3>
<p className="m-0 mt-2">
No field evidence was persisted for this turn
{record.missing_reason ? ` (${record.missing_reason})` : ""}.
</p>
</section>
);
}
const condition = record.versor_condition ?? 0;
const valid = record.field_valid === true;
// Literal class strings (not interpolated) so Tailwind's JIT emits them.
const cardClass = valid
? "rounded-md border border-[var(--color-state-success-border)] bg-[var(--color-state-success-bg)] p-3"
: "rounded-md border border-[var(--color-state-danger-border)] bg-[var(--color-state-danger-bg)] p-3";
const headClass = valid
? "m-0 text-xs font-semibold uppercase tracking-wide text-[var(--color-state-success-text)]"
: "m-0 text-xs font-semibold uppercase tracking-wide text-[var(--color-state-danger-text)]";
return (
<section className="flex flex-col gap-3" data-testid="field-invariant">
<div className={cardClass}>
<h3 className={headClass}>{valid ? "field valid" : "field breach"}</h3>
<p className="m-0 mt-2 flex flex-wrap items-baseline gap-2 font-mono text-sm tabular-nums text-[var(--color-text-primary)]">
<span data-testid="field-versor-condition">versor_condition = {formatCondition(condition)}</span>
<span aria-hidden className="text-[var(--color-text-secondary)]">
{valid ? "<" : "≥"}
</span>
<span className="text-[var(--color-text-secondary)]">
{record.versor_condition_ceiling.toExponential(0)}
</span>
</p>
<p className="m-0 mt-1 text-xs text-[var(--color-text-secondary)]">
{valid
? "The field state stays on the versor manifold; geometric coherence holds by construction."
: "The field state breaches the versor ceiling — the engine cannot claim a valid field here."}
</p>
</div>
<dl className="grid grid-cols-[auto_1fr] items-center gap-x-3 gap-y-2 text-sm">
<dt className="text-[var(--color-text-secondary)]">field_digest</dt>
<dd className="m-0">
{digestPayload(record.field_digest) ? (
<DigestBadge digest={digestPayload(record.field_digest) as string} truncate={12} />
) : (
<span className="text-[var(--color-text-secondary)]"></span>
)}
</dd>
<dt className="text-[var(--color-text-secondary)]">parent_field_digest</dt>
<dd className="m-0">
{digestPayload(record.parent_field_digest) ? (
<DigestBadge
digest={digestPayload(record.parent_field_digest) as string}
truncate={12}
/>
) : (
<span className="text-[var(--color-text-secondary)]">first turn no prior field</span>
)}
</dd>
<dt className="text-[var(--color-text-secondary)]">cga_inner(before, after)</dt>
<dd className="m-0 font-mono text-sm tabular-nums text-[var(--color-text-primary)]">
{record.transition_inner_product === null ? (
<span className="font-sans text-[var(--color-text-secondary)]">
first turn no transition
</span>
) : (
<span data-testid="field-transition">{formatInner(record.transition_inner_product)}</span>
)}
</dd>
</dl>
</section>
);
}

View file

@ -95,6 +95,25 @@ export interface CognitivePipelineRecord {
edges: CognitivePipelineEdge[];
}
/**
* C3 field-substrate evidence exact scalar invariants for a turn's CL(4,1)
* field. Geometry that can't fake coherence: only scalars + a content-addressed
* digest cross the boundary, never the raw multivector. `field_valid` is the
* live `versor_condition < versor_condition_ceiling` (1e-6) assertion.
*/
export interface FieldEvidence {
schema_version: "field_evidence_v1";
status: PipelineEvidenceStatus;
missing_reason: string | null;
trace_hash: string | null;
versor_condition: number | null;
versor_condition_ceiling: number;
field_valid: boolean | null;
field_digest: string | null;
parent_field_digest: string | null;
transition_inner_product: number | null;
}
export type LeewayLicense = "PROPOSE" | "SERVE" | "blocked" | "unknown";
export type ClaimDisclosure = "approximate" | "verified" | "proposal_only" | "none";
@ -130,6 +149,7 @@ export interface ChatTurnResult {
checkpoint_emitted: boolean;
leeway_evidence?: LeewayEvidence | null;
pipeline_record?: CognitivePipelineRecord | null;
field_evidence?: FieldEvidence | null;
}
export interface TurnJournalSummary {
@ -163,6 +183,7 @@ export interface TurnJournalEntry {
journal_digest: string;
leeway_evidence?: LeewayEvidence | null;
pipeline_record?: CognitivePipelineRecord | null;
field_evidence?: FieldEvidence | null;
}
export type TurnEvidence = ChatTurnResult | TurnJournalEntry;

View file

@ -20,6 +20,10 @@ from core.epistemic_state import (
)
from workbench import calibration, readers
from workbench.journal import DEFAULT_JOURNAL_DIR, TurnJournal, TurnJournalEntry
from workbench.field_evidence import (
field_evidence_from_journal_entry,
field_evidence_from_result,
)
from workbench.pipeline_record import (
cognitive_pipeline_record_from_result,
pipeline_record_from_journal_entry,
@ -338,6 +342,24 @@ class WorkbenchApi:
404, error("not_found", f"trace pipeline not found: {turn_id}")
)
return ApiResponse(200, ok(pipeline_record_from_journal_entry(entry)))
if method == "GET" and path.startswith("/trace/") and path.endswith("/field"):
raw_turn_id = unquote(
path.removeprefix("/trace/").removesuffix("/field").strip("/")
)
try:
turn_id = int(raw_turn_id)
except ValueError:
return ApiResponse(
404,
error("not_found", f"trace field not found: {raw_turn_id}"),
)
try:
entry = self._journal.get_entry(turn_id)
except FileNotFoundError:
return ApiResponse(
404, error("not_found", f"trace field not found: {turn_id}")
)
return ApiResponse(200, ok(field_evidence_from_journal_entry(entry)))
if method == "GET" and path.startswith("/trace/"):
raw_turn_id = unquote(path.removeprefix("/trace/"))
try:
@ -696,4 +718,5 @@ def _run_chat_turn(prompt: str, runtime: ChatRuntime | None = None) -> ChatTurnR
turn_cost_ms=0,
checkpoint_emitted=checkpoint_emitted,
pipeline_record=cognitive_pipeline_record_from_result(result),
field_evidence=field_evidence_from_result(result),
)

169
workbench/field_evidence.py Normal file
View file

@ -0,0 +1,169 @@
"""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

View file

@ -12,6 +12,7 @@ from typing import Any
from workbench.schemas import (
ChatTurnResult,
CognitivePipelineRecord,
FieldEvidence,
TraceIntegrity,
to_data,
utc_now,
@ -56,6 +57,7 @@ class TurnJournalEntry:
checkpoint_emitted: bool
leeway_evidence: dict[str, Any] | None = None
pipeline_record: CognitivePipelineRecord | dict[str, Any] | None = None
field_evidence: FieldEvidence | dict[str, Any] | None = None
trace_integrity: TraceIntegrity | None = None
journal_digest: str = ""
@ -96,6 +98,7 @@ class TurnJournalEntry:
checkpoint_emitted=result.checkpoint_emitted,
leeway_evidence=to_data(result.leeway_evidence),
pipeline_record=to_data(result.pipeline_record),
field_evidence=to_data(result.field_evidence),
trace_integrity=_trace_integrity_for_hash(result.trace_hash),
)

View file

@ -52,6 +52,7 @@ CRITICAL_FIELDS = frozenset(
"proposal_candidates",
"leeway_evidence",
"pipeline_record",
"field_evidence",
"checkpoint_emitted",
"trace_integrity",
}

View file

@ -145,6 +145,31 @@ class CognitivePipelineRecord:
edges: list[CognitivePipelineEdge] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class FieldEvidence:
"""C3 field-substrate evidence: exact scalar invariants for a turn's field.
Honest, read-only geometry: the engine owns the CL(4,1) field; this record
surfaces only the EXACT scalars it computes (``versor_condition``, the
``cga_inner`` transition value) plus a content-addressed ``field_digest``
NEVER the raw multivector (the geometry can't fake coherence, so we show the
numbers, not a decorative blob). ``field_valid`` is the live
``versor_condition < 1e-6`` assertion; it is consistency-checked against the
ceiling at construction (see ``workbench.field_evidence.validate``).
"""
schema_version: Literal["field_evidence_v1"]
status: PipelineEvidenceStatus
missing_reason: str | None
trace_hash: str | None
versor_condition: float | None
versor_condition_ceiling: float
field_valid: bool | None
field_digest: str | None
parent_field_digest: str | None
transition_inner_product: float | None
@dataclass(frozen=True, slots=True)
class ChatTurnResult:
prompt: str
@ -167,6 +192,7 @@ class ChatTurnResult:
checkpoint_emitted: bool
leeway_evidence: LeewayEvidence | None = None
pipeline_record: CognitivePipelineRecord | None = None
field_evidence: FieldEvidence | None = None
turn_id: int | None = None
@ -203,6 +229,7 @@ class TurnJournalEntrySchema:
journal_digest: str
leeway_evidence: LeewayEvidence | None = None
pipeline_record: CognitivePipelineRecord | None = None
field_evidence: FieldEvidence | None = None
@dataclass(frozen=True, slots=True)