diff --git a/workbench-ui/src/app/trace/PracticeEvidencePanel.tsx b/workbench-ui/src/app/trace/PracticeEvidencePanel.tsx new file mode 100644 index 00000000..215d43a2 --- /dev/null +++ b/workbench-ui/src/app/trace/PracticeEvidencePanel.tsx @@ -0,0 +1,169 @@ +import { useMemo } from "react"; +import type { PracticeEvidence } from "../../types/practiceEvidence"; +import { MetadataTable } from "../../design/components/MetadataTable/MetadataTable"; +import { StableJsonViewer } from "../../design/components/StableJsonViewer"; +import { EmptyState } from "../../design/components/states/EmptyState"; +import { ErrorState } from "../../design/components/states/ErrorState"; +import { LoadingState } from "../../design/components/states/LoadingState"; +import { tracePracticeReproducer } from "./practiceEvidenceEndpoint"; +import { + type PracticeEvidenceDetailSection, + practiceEvidencePanelModel, +} from "./practiceEvidencePanelModel"; + +export interface PracticeEvidencePanelProps { + evidence?: PracticeEvidence | null; + isLoading: boolean; + error: unknown; + turnId: number; + errorMessage: (error: unknown) => string; +} + +function rowValue(value: string) { + return value; +} + +function DetailSection({ section }: { section: PracticeEvidenceDetailSection }) { + return ( +
+

+ {section.title} +

+ {section.items.length === 0 ? ( +

{section.emptyMessage}

+ ) : ( +
+ {section.items.map((item) => ( +
+

+ {item.title} +

+ ({ + key: row.key, + value: rowValue(row.value), + mono: true, + }))} + /> +
+ ))} +
+ )} +
+ ); +} + +export function PracticeEvidencePanel({ + evidence, + isLoading, + error, + turnId, + errorMessage, +}: PracticeEvidencePanelProps) { + const reproducer = tracePracticeReproducer(turnId); + const rawJson = useMemo( + () => (evidence ? JSON.stringify(evidence, null, 2) : ""), + [evidence], + ); + + if (isLoading) { + return ; + } + if (error) { + return ( + + ); + } + if (!evidence) { + return ( + + ); + } + + const model = practiceEvidencePanelModel(evidence); + return ( +
+ {model.emptyMessage ? ( +
+

{model.status}

+

{model.emptyMessage}

+
+ ) : null} + +
+ ({ + key: row.key, + value: rowValue(row.value), + mono: true, + }))} + /> + ({ + key: row.key, + value: rowValue(row.value), + mono: true, + }))} + /> +
+ + {model.chainRows.length > 0 ? ( + ({ + key: row.key, + value: rowValue(row.value), + mono: true, + }))} + /> + ) : null} + +
+ {model.detailSections.map((section) => ( + + ))} +
+ +
+

+ Source spans +

+ {model.sourceSpanRows.length > 0 ? ( + ({ + key: row.key, + value: rowValue(row.value), + mono: true, + }))} + /> + ) : ( +

+ No source spans recorded. +

+ )} +
+ +
+
+ reproducer +
+ + {model.reproducer} + +
+ + {model.showRaw ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/workbench-ui/src/app/trace/practiceEvidenceEndpoint.test.ts b/workbench-ui/src/app/trace/practiceEvidenceEndpoint.test.ts new file mode 100644 index 00000000..4b8d3105 --- /dev/null +++ b/workbench-ui/src/app/trace/practiceEvidenceEndpoint.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from "vitest"; +import type { PracticeEvidence } from "../../types/practiceEvidence"; +import { + fetchTracePracticeWith, + tracePracticePath, + tracePracticeReproducer, +} from "./practiceEvidenceEndpoint"; + +const evidence: PracticeEvidence = { + schema_version: "practice_evidence_v1", + turn_id: 7, + status: "missing_evidence", + missing_reason: "sealed practice evidence was not persisted for this turn", + record_kind: null, + practice_disposition: null, + chain: [], + sealed_trace: null, + trace_refusal: null, + diagnostic_only: true, + serving_allowed: false, + mutation_allowed: false, + replay_execution_allowed: false, + replay_executed_by_workbench: false, +}; + +describe("practice evidence endpoint helpers", () => { + it("builds encoded trace practice paths", () => { + expect(tracePracticePath(7)).toBe("/trace/7/practice"); + expect(tracePracticeReproducer(7)).toBe("curl /trace/7/practice"); + }); + + it("fetches practice evidence through an injected fetcher", async () => { + const fetcher = vi.fn((_path: string): Promise => Promise.resolve(evidence as T)); + + await expect( + fetchTracePracticeWith(7, fetcher as unknown as (path: string) => Promise), + ).resolves.toBe(evidence); + expect(fetcher).toHaveBeenCalledWith("/trace/7/practice"); + }); +}); diff --git a/workbench-ui/src/app/trace/practiceEvidenceEndpoint.ts b/workbench-ui/src/app/trace/practiceEvidenceEndpoint.ts new file mode 100644 index 00000000..a6e627b5 --- /dev/null +++ b/workbench-ui/src/app/trace/practiceEvidenceEndpoint.ts @@ -0,0 +1,20 @@ +import type { PracticeEvidence } from "../../types/practiceEvidence"; + +export function tracePracticePath(turnId: number): string { + return `/trace/${encodeURIComponent(String(turnId))}/practice`; +} + +export async function fetchTracePracticeWith( + turnId: number, + fetcher: (path: string) => Promise, +): Promise { + return fetcher(tracePracticePath(turnId)); +} + +export const TRACE_PRACTICE_LOADING_LABEL = "Loading sealed practice evidence..."; +export const TRACE_PRACTICE_EMPTY_LABEL = "No sealed practice evidence recorded for this turn."; +export const TRACE_PRACTICE_ERROR_MUTATION_STATUS = "No trace mutation occurred."; + +export function tracePracticeReproducer(turnId: number): string { + return `curl /trace/${encodeURIComponent(String(turnId))}/practice`; +} diff --git a/workbench-ui/src/app/trace/practiceEvidencePanelModel.test.ts b/workbench-ui/src/app/trace/practiceEvidencePanelModel.test.ts new file mode 100644 index 00000000..9876a49c --- /dev/null +++ b/workbench-ui/src/app/trace/practiceEvidencePanelModel.test.ts @@ -0,0 +1,187 @@ +import { describe, expect, it } from "vitest"; +import type { PracticeEvidence } from "../../types/practiceEvidence"; +import { practiceEvidencePanelModel } from "./practiceEvidencePanelModel"; + +const missingEvidence: PracticeEvidence = { + schema_version: "practice_evidence_v1", + turn_id: 7, + status: "missing_evidence", + missing_reason: "sealed practice evidence was not persisted for this turn", + record_kind: null, + practice_disposition: null, + chain: [], + sealed_trace: null, + trace_refusal: null, + diagnostic_only: true, + serving_allowed: false, + mutation_allowed: false, + replay_execution_allowed: false, + replay_executed_by_workbench: false, +}; + +function sealedTraceEvidence(): PracticeEvidence { + return { + ...missingEvidence, + status: "recorded", + missing_reason: null, + record_kind: "sealed_trace", + practice_disposition: "sealed", + chain: [ + { + kind: "problem_frame", + status: "recorded", + refs: ["pf_123"], + summary: "Problem frame digest bound into the sealed practice trace.", + }, + { + kind: "geometric_search_run", + status: "recorded", + refs: ["gsr_1"], + summary: "Geometric search run identity; Workbench does not execute search.", + }, + { + kind: "replay_results", + status: "missing_evidence", + refs: [], + summary: "Replay adapter result identities; Workbench does not run replay here.", + }, + ], + sealed_trace: { + trace_id: "spt_1", + trace_policy_version: "sealed-practice-v1", + input_digest: "input_sha", + problem_frame_digest: "pf_123", + original_contract_assessment_id: "ca_1", + residual_ids: ["res_1"], + search_gate_decision_id: "sg_1", + compute_budget_id: "budget_1", + geometric_search_run_id: "gsr_1", + candidate_attempt_ids: ["attempt_1"], + candidate_attempt_binding_ids: ["binding_1"], + replay_result_ids: [], + replay_refusal_ids: ["rr_1"], + upstream_identity_chain: ["pf_123", "ca_1", "res_1", "sg_1", "budget_1", "gsr_1"], + practice_disposition: "sealed", + trace_records: ["record_1"], + evidence_spans: [{ text: "Lena has 3 marbles.", start: 0, end: 20, sentence_index: 0 }], + created_by_policy: "sealed-practice-v1", + explanation: "sealed diagnostic practice trace", + }, + trace_refusal: null, + }; +} + +function refusalEvidence(): PracticeEvidence { + return { + ...missingEvidence, + status: "recorded", + missing_reason: null, + record_kind: "trace_refusal", + practice_disposition: "refused", + chain: [ + { + kind: "trace_refusal", + status: "recorded", + refs: ["ptr_1"], + summary: "Practice trace refused before a sealed practice trace could be projected.", + }, + ], + trace_refusal: { + trace_refusal_id: "ptr_1", + trace_policy_version: "sealed-practice-v1", + input_digest: null, + practice_disposition: "refused", + reason_codes: ["missing_residual"], + explanation: "no residual target", + }, + sealed_trace: null, + }; +} + +describe("practice evidence panel model", () => { + it("models missing evidence honestly", () => { + const model = practiceEvidencePanelModel(missingEvidence); + + expect(model.status).toBe("missing_evidence"); + expect(model.emptyMessage).toBe("sealed practice evidence was not persisted for this turn"); + expect(model.reproducer).toBe("curl /trace/7/practice"); + expect(model.authorityRows).toEqual([ + { key: "schema_version", value: "practice_evidence_v1" }, + { key: "status", value: "missing_evidence" }, + { key: "record_kind", value: "none" }, + { key: "practice_disposition", value: "none" }, + { key: "diagnostic_only", value: "true" }, + { key: "serving_allowed", value: "false" }, + { key: "mutation_allowed", value: "false" }, + { key: "replay_execution_allowed", value: "false" }, + { key: "replay_executed_by_workbench", value: "false" }, + { key: "missing_reason", value: "sealed practice evidence was not persisted for this turn" }, + ]); + expect(model.countRows).toEqual([ + { key: "chain_cards", value: "0" }, + { key: "sealed_trace", value: "0" }, + { key: "trace_refusal", value: "0" }, + { key: "source_spans", value: "0" }, + ]); + expect(model.chainRows).toEqual([]); + expect(model.sourceSpanRows).toEqual([]); + expect(model.showRaw).toBe(false); + }); + + it("models a recorded sealed practice trace without granting authority", () => { + const model = practiceEvidencePanelModel(sealedTraceEvidence()); + + expect(model.emptyMessage).toBe(null); + expect(model.showRaw).toBe(true); + expect(model.countRows).toContainEqual({ key: "chain_cards", value: "3" }); + expect(model.chainRows).toEqual([ + { key: "problem_frame", value: "recorded — pf_123" }, + { key: "geometric_search_run", value: "recorded — gsr_1" }, + { key: "replay_results", value: "missing_evidence — none" }, + ]); + expect(model.authorityRows).toContainEqual({ key: "diagnostic_only", value: "true" }); + expect(model.authorityRows).toContainEqual({ key: "serving_allowed", value: "false" }); + expect(model.authorityRows).toContainEqual({ key: "mutation_allowed", value: "false" }); + expect(model.authorityRows).toContainEqual({ key: "replay_execution_allowed", value: "false" }); + expect(model.authorityRows).toContainEqual({ + key: "replay_executed_by_workbench", + value: "false", + }); + + const chain = model.detailSections.find((section) => section.title === "Evidence chain"); + if (chain === undefined) throw new Error("missing Evidence chain section"); + const geometricSearchCard = chain.items.find((item) => item.title === "card 2: geometric_search_run"); + if (geometricSearchCard === undefined) throw new Error("missing geometric search card"); + expect(geometricSearchCard.rows).toContainEqual({ + key: "authority", + value: + "identity card only; Workbench does not execute search, replay, operators, sealing, or mutation", + }); + + const sealed = model.detailSections.find((section) => section.title === "Sealed trace"); + if (sealed === undefined) throw new Error("missing Sealed trace section"); + const sealedTrace = sealed.items.find((item) => item.title === "spt_1"); + if (sealedTrace === undefined) throw new Error("missing sealed trace item"); + expect(sealedTrace.rows).toContainEqual({ key: "geometric_search_run_id", value: "gsr_1" }); + expect(sealedTrace.rows).toContainEqual({ key: "replay_refusal_ids", value: "rr_1" }); + expect(model.sourceSpanRows).toEqual([ + { key: "sealed_trace.1", value: "0:20 Lena has 3 marbles. (sentence 0)" }, + ]); + }); + + it("models practice trace refusals", () => { + const model = practiceEvidencePanelModel(refusalEvidence()); + + expect(model.emptyMessage).toBe(null); + expect(model.countRows).toContainEqual({ key: "trace_refusal", value: "1" }); + expect(model.chainRows).toEqual([{ key: "trace_refusal", value: "recorded — ptr_1" }]); + + const refusal = model.detailSections.find((section) => section.title === "Trace refusal"); + if (refusal === undefined) throw new Error("missing Trace refusal section"); + const refusalItem = refusal.items.find((item) => item.title === "ptr_1"); + if (refusalItem === undefined) throw new Error("missing trace refusal item"); + expect(refusalItem.rows).toContainEqual({ key: "trace_refusal_id", value: "ptr_1" }); + expect(refusalItem.rows).toContainEqual({ key: "reason_codes", value: "missing_residual" }); + expect(refusalItem.rows).toContainEqual({ key: "explanation", value: "no residual target" }); + }); +}); diff --git a/workbench-ui/src/app/trace/practiceEvidencePanelModel.ts b/workbench-ui/src/app/trace/practiceEvidencePanelModel.ts new file mode 100644 index 00000000..10a60f1e --- /dev/null +++ b/workbench-ui/src/app/trace/practiceEvidencePanelModel.ts @@ -0,0 +1,184 @@ +import type { + PracticeEvidence, + PracticeEvidenceCard, + PracticeSourceSpanView, + SealedPracticeTraceView, + PracticeTraceRefusalView, +} from "../../types/practiceEvidence"; +import { tracePracticeReproducer } from "./practiceEvidenceEndpoint"; +import { practiceEvidenceEmptyMessage, practiceSourceSpanLabel } from "./practiceEvidenceView"; + +export interface PracticeEvidenceSummaryRow { + key: string; + value: string; +} + +export interface PracticeEvidenceDetailItem { + title: string; + rows: PracticeEvidenceSummaryRow[]; +} + +export interface PracticeEvidenceDetailSection { + title: string; + emptyMessage: string; + items: PracticeEvidenceDetailItem[]; +} + +export interface PracticeEvidencePanelModel { + status: PracticeEvidence["status"]; + emptyMessage: string | null; + reproducer: string; + authorityRows: PracticeEvidenceSummaryRow[]; + countRows: PracticeEvidenceSummaryRow[]; + chainRows: PracticeEvidenceSummaryRow[]; + detailSections: PracticeEvidenceDetailSection[]; + sourceSpanRows: PracticeEvidenceSummaryRow[]; + showRaw: boolean; +} + +function boolLabel(value: boolean): string { + return value ? "true" : "false"; +} + +function listLabel(values: readonly string[] | null | undefined): string { + return values && values.length > 0 ? values.join(", ") : "none"; +} + +function sourceSpanRows(evidence: PracticeEvidence): PracticeEvidenceSummaryRow[] { + const rows: PracticeEvidenceSummaryRow[] = []; + const append = (prefix: string, spans: readonly PracticeSourceSpanView[] | null | undefined) => { + if (!spans) return; + spans.forEach((span, index) => { + rows.push({ key: `${prefix}.${index + 1}`, value: practiceSourceSpanLabel(span) }); + }); + }; + if (evidence.sealed_trace != null) { + append("sealed_trace", evidence.sealed_trace.evidence_spans); + } + return rows; +} + +function cardRows(card: PracticeEvidenceCard): PracticeEvidenceSummaryRow[] { + return [ + { key: "kind", value: card.kind }, + { key: "status", value: card.status }, + { key: "refs", value: listLabel(card.refs) }, + { key: "summary", value: card.summary || "none" }, + { + key: "authority", + value: "identity card only; Workbench does not execute search, replay, operators, sealing, or mutation", + }, + ]; +} + +function sealedTraceRows(trace: SealedPracticeTraceView): PracticeEvidenceSummaryRow[] { + return [ + { key: "trace_id", value: trace.trace_id }, + { key: "trace_policy_version", value: trace.trace_policy_version }, + { key: "input_digest", value: trace.input_digest }, + { key: "problem_frame_digest", value: trace.problem_frame_digest }, + { key: "original_contract_assessment_id", value: trace.original_contract_assessment_id }, + { key: "residual_ids", value: listLabel(trace.residual_ids) }, + { key: "search_gate_decision_id", value: trace.search_gate_decision_id }, + { key: "compute_budget_id", value: trace.compute_budget_id }, + { key: "geometric_search_run_id", value: trace.geometric_search_run_id }, + { key: "candidate_attempt_ids", value: listLabel(trace.candidate_attempt_ids) }, + { key: "candidate_attempt_binding_ids", value: listLabel(trace.candidate_attempt_binding_ids) }, + { key: "replay_result_ids", value: listLabel(trace.replay_result_ids) }, + { key: "replay_refusal_ids", value: listLabel(trace.replay_refusal_ids) }, + { key: "upstream_identity_chain", value: listLabel(trace.upstream_identity_chain) }, + { key: "practice_disposition", value: trace.practice_disposition }, + { key: "trace_records", value: listLabel(trace.trace_records) }, + { key: "created_by_policy", value: trace.created_by_policy || "none" }, + { key: "explanation", value: trace.explanation || "none" }, + ]; +} + +function traceRefusalRows(refusal: PracticeTraceRefusalView): PracticeEvidenceSummaryRow[] { + return [ + { key: "trace_refusal_id", value: refusal.trace_refusal_id }, + { key: "trace_policy_version", value: refusal.trace_policy_version }, + { key: "input_digest", value: refusal.input_digest ?? "none" }, + { key: "practice_disposition", value: refusal.practice_disposition }, + { key: "reason_codes", value: listLabel(refusal.reason_codes) }, + { key: "explanation", value: refusal.explanation || "none" }, + ]; +} + +function detailSections(evidence: PracticeEvidence): PracticeEvidenceDetailSection[] { + const chain = evidence.chain ?? []; + return [ + { + title: "Evidence chain", + emptyMessage: "No sealed practice chain cards recorded.", + items: chain.map((card, index) => ({ + title: `card ${index + 1}: ${card.kind}`, + rows: cardRows(card), + })), + }, + { + title: "Sealed trace", + emptyMessage: "No sealed practice trace recorded.", + items: + evidence.sealed_trace == null + ? [] + : [ + { + title: evidence.sealed_trace.trace_id, + rows: sealedTraceRows(evidence.sealed_trace), + }, + ], + }, + { + title: "Trace refusal", + emptyMessage: "No practice trace refusal recorded.", + items: + evidence.trace_refusal == null + ? [] + : [ + { + title: evidence.trace_refusal.trace_refusal_id, + rows: traceRefusalRows(evidence.trace_refusal), + }, + ], + }, + ]; +} + +export function practiceEvidencePanelModel(evidence: PracticeEvidence): PracticeEvidencePanelModel { + const chain = evidence.chain ?? []; + const sealedTrace = evidence.sealed_trace; + const traceRefusal = evidence.trace_refusal; + const sourceSpanCount = sealedTrace?.evidence_spans?.length ?? 0; + + return { + status: evidence.status, + emptyMessage: practiceEvidenceEmptyMessage(evidence), + reproducer: tracePracticeReproducer(evidence.turn_id), + authorityRows: [ + { key: "schema_version", value: evidence.schema_version }, + { key: "status", value: evidence.status }, + { key: "record_kind", value: evidence.record_kind ?? "none" }, + { key: "practice_disposition", value: evidence.practice_disposition ?? "none" }, + { key: "diagnostic_only", value: boolLabel(evidence.diagnostic_only) }, + { key: "serving_allowed", value: boolLabel(evidence.serving_allowed) }, + { key: "mutation_allowed", value: boolLabel(evidence.mutation_allowed) }, + { key: "replay_execution_allowed", value: boolLabel(evidence.replay_execution_allowed) }, + { key: "replay_executed_by_workbench", value: boolLabel(evidence.replay_executed_by_workbench) }, + { key: "missing_reason", value: evidence.missing_reason ?? "none" }, + ], + countRows: [ + { key: "chain_cards", value: String(chain.length) }, + { key: "sealed_trace", value: sealedTrace == null ? "0" : "1" }, + { key: "trace_refusal", value: traceRefusal == null ? "0" : "1" }, + { key: "source_spans", value: String(sourceSpanCount) }, + ], + chainRows: chain.map((card) => ({ + key: card.kind, + value: `${card.status} — ${listLabel(card.refs)}`, + })), + detailSections: detailSections(evidence), + sourceSpanRows: sourceSpanRows(evidence), + showRaw: evidence.status === "recorded", + }; +} diff --git a/workbench-ui/src/app/trace/practiceEvidenceView.ts b/workbench-ui/src/app/trace/practiceEvidenceView.ts new file mode 100644 index 00000000..8191a222 --- /dev/null +++ b/workbench-ui/src/app/trace/practiceEvidenceView.ts @@ -0,0 +1,27 @@ +import type { PracticeEvidence, PracticeSourceSpanView } from "../../types/practiceEvidence"; + +export const PRACTICE_AUTHORITY_DISCLOSURES = [ + "Diagnostic Only", + "Serving Disallowed", + "Mutation Disallowed", + "Replay Execution Disallowed", + "Workbench Did Not Execute Replay", +] as const; + +export function practiceEvidenceEmptyMessage(evidence: PracticeEvidence): string | null { + if (evidence.status === "missing_evidence") { + return evidence.missing_reason ?? "No sealed practice evidence recorded for this turn."; + } + const chain = evidence.chain ?? []; + return chain.length > 0 || evidence.sealed_trace != null || evidence.trace_refusal != null + ? null + : "No sealed practice evidence recorded for this turn."; +} + +export function practiceSourceSpanLabel(span: PracticeSourceSpanView): string { + const sentence = + span.sentence_index === null || span.sentence_index === undefined + ? "sentence unknown" + : `sentence ${span.sentence_index}`; + return `${span.start}:${span.end} ${span.text} (${sentence})`; +} diff --git a/workbench-ui/src/types/practiceEvidence.ts b/workbench-ui/src/types/practiceEvidence.ts new file mode 100644 index 00000000..e53afc48 --- /dev/null +++ b/workbench-ui/src/types/practiceEvidence.ts @@ -0,0 +1,78 @@ +export type PracticeEvidenceStatus = "recorded" | "missing_evidence"; +export type PracticeRecordKind = "sealed_trace" | "trace_refusal" | null; + +export type PracticeCardKind = + | "problem_frame" + | "contract_assessment" + | "residuals" + | "search_gate" + | "compute_budget" + | "geometric_search_run" + | "candidate_attempts" + | "attempt_bindings" + | "replay_results" + | "replay_refusals" + | "sealed_trace" + | "trace_refusal"; + +export interface PracticeSourceSpanView { + text: string; + start: number; + end: number; + sentence_index: number | null; +} + +export interface PracticeEvidenceCard { + kind: PracticeCardKind; + status: PracticeEvidenceStatus; + refs: string[]; + summary: string; +} + +export interface SealedPracticeTraceView { + trace_id: string; + trace_policy_version: string; + input_digest: string; + problem_frame_digest: string; + original_contract_assessment_id: string; + residual_ids: string[]; + search_gate_decision_id: string; + compute_budget_id: string; + geometric_search_run_id: string; + candidate_attempt_ids: string[]; + candidate_attempt_binding_ids: string[]; + replay_result_ids: string[]; + replay_refusal_ids: string[]; + upstream_identity_chain: string[]; + practice_disposition: string; + trace_records: string[]; + evidence_spans: PracticeSourceSpanView[]; + created_by_policy: string; + explanation: string; +} + +export interface PracticeTraceRefusalView { + trace_refusal_id: string; + trace_policy_version: string; + input_digest: string | null; + practice_disposition: string; + reason_codes: string[]; + explanation: string; +} + +export interface PracticeEvidence { + schema_version: "practice_evidence_v1"; + turn_id: number; + status: PracticeEvidenceStatus; + missing_reason: string | null; + record_kind: PracticeRecordKind; + practice_disposition: string | null; + chain: PracticeEvidenceCard[]; + sealed_trace: SealedPracticeTraceView | null; + trace_refusal: PracticeTraceRefusalView | null; + diagnostic_only: boolean; + serving_allowed: boolean; + mutation_allowed: boolean; + replay_execution_allowed: boolean; + replay_executed_by_workbench: boolean; +} diff --git a/workbench/replay.py b/workbench/replay.py index 7b86c5bd..0bf03f88 100644 --- a/workbench/replay.py +++ b/workbench/replay.py @@ -55,6 +55,8 @@ CRITICAL_FIELDS = frozenset( "field_evidence", "checkpoint_emitted", "trace_integrity", + "construction_evidence", + "practice_evidence", } ) # Wall-clock by nature, or derived over wall-clock bytes (journal_digest