From 53ff9359a85c79404c23dd30d865f1f78df26597 Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 13 Jun 2026 16:26:46 -0700 Subject: [PATCH] =?UTF-8?q?feat(workbench):=20Wave=20M=20C3=20field=20subs?= =?UTF-8?q?trate=20=E2=80=94=20persist-first=20field=20evidence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/workbench/data-shapes-v1.md | 31 ++++ docs/workbench/wave-M-worthiness.md | 33 ++-- tests/test_workbench_field_evidence.py | 153 ++++++++++++++++ workbench-ui/schema-snapshot.json | 16 +- workbench-ui/src/api/client.ts | 7 + workbench-ui/src/api/queries.ts | 12 ++ workbench-ui/src/app/trace/TraceRoute.tsx | 58 +++++- .../FieldInvariantCard.test.tsx | 74 ++++++++ .../FieldInvariantCard/FieldInvariantCard.tsx | 111 ++++++++++++ workbench-ui/src/types/api.ts | 21 +++ workbench/api.py | 23 +++ workbench/field_evidence.py | 169 ++++++++++++++++++ workbench/journal.py | 3 + workbench/replay.py | 1 + workbench/schemas.py | 27 +++ 15 files changed, 726 insertions(+), 13 deletions(-) create mode 100644 tests/test_workbench_field_evidence.py create mode 100644 workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.test.tsx create mode 100644 workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.tsx create mode 100644 workbench/field_evidence.py diff --git a/docs/workbench/data-shapes-v1.md b/docs/workbench/data-shapes-v1.md index 19029f35..1fa8271f 100644 --- a/docs/workbench/data-shapes-v1.md +++ b/docs/workbench/data-shapes-v1.md @@ -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 diff --git a/docs/workbench/wave-M-worthiness.md b/docs/workbench/wave-M-worthiness.md index fa82e36a..b98a275a 100644 --- a/docs/workbench/wave-M-worthiness.md +++ b/docs/workbench/wave-M-worthiness.md @@ -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. diff --git a/tests/test_workbench_field_evidence.py b/tests/test_workbench_field_evidence.py new file mode 100644 index 00000000..1d017866 --- /dev/null +++ b/tests/test_workbench_field_evidence.py @@ -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 diff --git a/workbench-ui/schema-snapshot.json b/workbench-ui/schema-snapshot.json index 50786f24..6032af56 100644 --- a/workbench-ui/schema-snapshot.json +++ b/workbench-ui/schema-snapshot.json @@ -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", diff --git a/workbench-ui/src/api/client.ts b/workbench-ui/src/api/client.ts index bc020aad..0d6f1c3d 100644 --- a/workbench-ui/src/api/client.ts +++ b/workbench-ui/src/api/client.ts @@ -22,6 +22,7 @@ import type { CalibrationClass, ServingMetrics, CognitivePipelineRecord, + FieldEvidence, TurnJournalEntry, TurnJournalSummary, ContemplationRunDetail, @@ -186,6 +187,12 @@ export async function fetchTracePipeline(turnId: number): Promise { + return apiFetch( + `/trace/${encodeURIComponent(String(turnId))}/field`, + ); +} + export async function fetchAuditEvents( limit?: number, offset?: number, diff --git a/workbench-ui/src/api/queries.ts b/workbench-ui/src/api/queries.ts index 1bab1806..8a85137b 100644 --- a/workbench-ui/src/api/queries.ts +++ b/workbench-ui/src/api/queries.ts @@ -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({ + 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], diff --git a/workbench-ui/src/app/trace/TraceRoute.tsx b/workbench-ui/src/app/trace/TraceRoute.tsx index d6d69bde..b99ebd4a 100644 --- a/workbench-ui/src/app/trace/TraceRoute.tsx +++ b/workbench-ui/src/app/trace/TraceRoute.tsx @@ -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 ; + } + if (error) { + return ( + + ); + } + if (!record) { + return ; + } + return ; +} + 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" ? ( + + ) : null} {activeTab === "surfaces" ? : null} {activeTab === "grounding" ? : null} {activeTab === "verdicts" ? : 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} diff --git a/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.test.tsx b/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.test.tsx new file mode 100644 index 00000000..a50aff93 --- /dev/null +++ b/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.test.tsx @@ -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 { + 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(); + 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( + , + ); + expect(screen.getByTestId("field-transition")).toBeInTheDocument(); + }); + + it("never claims valid when the field breaches the ceiling", () => { + render( + , + ); + expect(screen.queryByText("field valid")).not.toBeInTheDocument(); + expect(screen.getByText("field breach")).toBeInTheDocument(); + }); + + it("is honest about absent evidence", () => { + render( + , + ); + expect(screen.getByTestId("field-missing")).toBeInTheDocument(); + expect(screen.getByText(/No field evidence/)).toBeInTheDocument(); + }); + + it("does not render any raw multivector array", () => { + const { container } = render(); + expect(container.textContent).not.toMatch(/\[0,\s*0,\s*0/); + expect(container.textContent).not.toContain("b64"); + }); +}); diff --git a/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.tsx b/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.tsx new file mode 100644 index 00000000..684cc754 --- /dev/null +++ b/workbench-ui/src/design/components/FieldInvariantCard/FieldInvariantCard.tsx @@ -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 ( +
+

+ missing_evidence +

+

+ No field evidence was persisted for this turn + {record.missing_reason ? ` (${record.missing_reason})` : ""}. +

+
+ ); + } + + 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 ( +
+
+

{valid ? "field valid" : "field breach"}

+

+ versor_condition = {formatCondition(condition)} + + {valid ? "<" : "≥"} + + + {record.versor_condition_ceiling.toExponential(0)} + +

+

+ {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."} +

+
+ +
+
field_digest
+
+ {digestPayload(record.field_digest) ? ( + + ) : ( + + )} +
+ +
parent_field_digest
+
+ {digestPayload(record.parent_field_digest) ? ( + + ) : ( + first turn — no prior field + )} +
+ +
cga_inner(before, after)
+
+ {record.transition_inner_product === null ? ( + + first turn — no transition + + ) : ( + {formatInner(record.transition_inner_product)} + )} +
+
+
+ ); +} diff --git a/workbench-ui/src/types/api.ts b/workbench-ui/src/types/api.ts index f977209e..bf372395 100644 --- a/workbench-ui/src/types/api.ts +++ b/workbench-ui/src/types/api.ts @@ -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; diff --git a/workbench/api.py b/workbench/api.py index f6ad6928..3bb3caa7 100644 --- a/workbench/api.py +++ b/workbench/api.py @@ -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), ) diff --git a/workbench/field_evidence.py b/workbench/field_evidence.py new file mode 100644 index 00000000..89d7b4af --- /dev/null +++ b/workbench/field_evidence.py @@ -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 diff --git a/workbench/journal.py b/workbench/journal.py index 3dd26790..09ccea52 100644 --- a/workbench/journal.py +++ b/workbench/journal.py @@ -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), ) diff --git a/workbench/replay.py b/workbench/replay.py index 027af0d4..7b86c5bd 100644 --- a/workbench/replay.py +++ b/workbench/replay.py @@ -52,6 +52,7 @@ CRITICAL_FIELDS = frozenset( "proposal_candidates", "leeway_evidence", "pipeline_record", + "field_evidence", "checkpoint_emitted", "trace_integrity", } diff --git a/workbench/schemas.py b/workbench/schemas.py index 5fa42a46..a6abfbe7 100644 --- a/workbench/schemas.py +++ b/workbench/schemas.py @@ -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)