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:
parent
4991f61133
commit
53ff9359a8
15 changed files with 726 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
153
tests/test_workbench_field_evidence.py
Normal file
153
tests/test_workbench_field_evidence.py
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
169
workbench/field_evidence.py
Normal 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
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ CRITICAL_FIELDS = frozenset(
|
|||
"proposal_candidates",
|
||||
"leeway_evidence",
|
||||
"pipeline_record",
|
||||
"field_evidence",
|
||||
"checkpoint_emitted",
|
||||
"trace_integrity",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue