diff --git a/apps/control-station/src/core/laboratory/advancedIndex.ts b/apps/control-station/src/core/laboratory/advancedIndex.ts index 6fe32b1..c5eaadc 100644 --- a/apps/control-station/src/core/laboratory/advancedIndex.ts +++ b/apps/control-station/src/core/laboratory/advancedIndex.ts @@ -37,8 +37,10 @@ import { fetchE46IGroundingDinoFullReplay } from "./e46iGroundingDinoFullReplay" import { fetchE46JRawFisheyeRealtime } from "./e46jRawFisheyeRealtime"; import { fetchE47SemanticSlamResult } from "./e47SemanticSlam"; import { fetchM4ThreatReplayResult } from "./m4ReplayThreat"; +import { fetchM47ReferenceGraphLab } from "./m47ReferenceGraph"; export type AdvancedLaboratoryWorkId = + | "m47-reference-graph-shadow" | "m4-replay-threat" | "l3-pointpillars-visual-audit" | "l31-pointpillars-ravnoves" @@ -80,6 +82,7 @@ export interface AdvancedLaboratoryIndexItem { } const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [ + "m47-reference-graph-shadow", "m4-replay-threat", "l3-pointpillars-visual-audit", "l31-pointpillars-ravnoves", @@ -116,6 +119,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [ ]; const RESULT_PREFIX: Readonly> = { + "m47-reference-graph-shadow": "m47-reference-graph-lab", "m4-replay-threat": "m4-threat-replay", "l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit", "l31-pointpillars-ravnoves": "l31-pointpillars-ravnoves", @@ -159,6 +163,7 @@ export function isAdvancedLaboratoryWorkId( export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults { return { + m47Graph: null, m4Threat: null, l3: null, l31: null, @@ -283,7 +288,8 @@ export function advancedLaboratoryResultAvailable( workId: AdvancedLaboratoryWorkId, results: AdvancedLaboratoryResults, ): boolean { - return workId === "m4-replay-threat" ? results.m4Threat !== null + return workId === "m47-reference-graph-shadow" ? results.m47Graph !== null + : workId === "m4-replay-threat" ? results.m4Threat !== null : workId === "l3-pointpillars-visual-audit" ? results.l3 !== null : workId === "l31-pointpillars-ravnoves" ? results.l31 !== null : workId === "l32-pointpillars-camera-review" ? results.l32 !== null @@ -323,13 +329,24 @@ export async function fetchAdvancedLaboratoryResult( { fetcher = fetch, signal, + resultId, }: { fetcher?: LaboratoryFetch; signal?: AbortSignal; + resultId?: string; } = {}, ): Promise { const results = emptyAdvancedLaboratoryResults(); - if (workId === "m4-replay-threat") { + if (workId === "m47-reference-graph-shadow") { + if (!resultId) { + throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана."); + } + results.m47Graph = await fetchM47ReferenceGraphLab({ + resultId, + fetcher, + signal, + }); + } else if (workId === "m4-replay-threat") { results.m4Threat = await fetchM4ThreatReplayResult({ fetcher, signal }); } else if (workId === "l3-pointpillars-visual-audit") { results.l3 = await fetchL3PointPillarsVisualAudit({ fetcher, signal }); diff --git a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts index 8543715..9ea4969 100644 --- a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts +++ b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts @@ -33,8 +33,10 @@ import type { E46IGroundingDinoFullReplayResult } from "./e46iGroundingDinoFullR import type { E46JRawFisheyeRealtimeResult } from "./e46jRawFisheyeRealtime"; import type { E47SemanticSlamResult } from "./e47SemanticSlam"; import type { M4ThreatReplayResult } from "./m4ReplayThreat"; +import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph"; export interface AdvancedLaboratoryResults { + m47Graph: M47ReferenceGraphLabResult | null; m4Threat: M4ThreatReplayResult | null; l3: L3PointPillarsVisualAuditResult | null; l31: L31PointPillarsRavnovesResult | null; diff --git a/apps/control-station/src/core/laboratory/advancedResults.ts b/apps/control-station/src/core/laboratory/advancedResults.ts index dabc72c..17ed3e4 100644 --- a/apps/control-station/src/core/laboratory/advancedResults.ts +++ b/apps/control-station/src/core/laboratory/advancedResults.ts @@ -967,7 +967,7 @@ export async function fetchAdvancedLaboratoryResults({ const e39 = settledCatalogValue(settled[7]); const e40 = settledCatalogValue(settled[8]); return { - m4Threat: null, + m47Graph: null, m4Threat: null, l3: null, l31: null, l32: null, l33: null, diff --git a/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts b/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts new file mode 100644 index 0000000..4f43452 --- /dev/null +++ b/apps/control-station/src/core/laboratory/m47ReferenceGraph.ts @@ -0,0 +1,223 @@ +import { + fetchLaboratoryEvidenceReport, + type JsonValue, +} from "./evidenceReport"; +import { + fetchM4ThreatReplayResult, + type M4ThreatReplayResult, +} from "./m4ReplayThreat"; + +export interface M47ReferenceGraphLabResult { + resultId: string; + createdAtUtc: string; + graphResultId: string; + linkedVisualResultId: string; + graphId: "reference-perception-graph/v2"; + worker: { + id: "worker-006"; + node: string; + artifactSha256: string; + codeRevision: string; + elapsedSeconds: number; + }; + frames: { + expected: 4489; + admitted: 4489; + delivered: 4489; + }; + parityMismatchCounts: Readonly>; + queueHighWatermarks: Readonly>; + limitations: readonly string[]; + visual: M4ThreatReplayResult; +} + +type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise; + +class M47ReferenceGraphContractError extends Error {} + +function object(value: unknown, label: string): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new M47ReferenceGraphContractError(`${label}: ожидался объект.`); + } + return value as Record; +} + +function text(value: unknown, label: string): string { + if (typeof value !== "string" || !value.trim()) { + throw new M47ReferenceGraphContractError(`${label}: ожидалась строка.`); + } + return value; +} + +function exact( + value: unknown, + expected: T, + label: string, +): T { + if (value !== expected) { + throw new M47ReferenceGraphContractError(`${label}: нарушен контракт.`); + } + return expected; +} + +function finite(value: unknown, label: string): number { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + throw new M47ReferenceGraphContractError(`${label}: ожидалось неотрицательное число.`); + } + return value; +} + +function digest(value: unknown, label: string, length: 40 | 64): string { + const parsed = text(value, label); + if (!new RegExp(`^[a-f0-9]{${length}}$`).test(parsed)) { + throw new M47ReferenceGraphContractError(`${label}: нарушена digest-идентичность.`); + } + return parsed; +} + +function zeroParity(value: unknown): M47ReferenceGraphLabResult["parityMismatchCounts"] { + const document = object(value, "M4.7 parity"); + const keys = [ + "source_binding", + "current", + "rolling_retained", + "held", + "expired", + "camera_uncertainty", + "threat_assessments", + ] as const; + if ( + Object.keys(document).length !== keys.length + || keys.some((key) => document[key] !== 0) + ) { + throw new M47ReferenceGraphContractError("M4.7 parity: обнаружено расхождение."); + } + return Object.fromEntries(keys.map((key) => [key, 0])) as unknown as ( + M47ReferenceGraphLabResult["parityMismatchCounts"] + ); +} + +function queueWatermarks( + value: unknown, +): M47ReferenceGraphLabResult["queueHighWatermarks"] { + const document = object(value, "M4.7 queue watermarks"); + const keys = ["detector", "geometry", "temporal", "rolling", "threat"] as const; + if (Object.keys(document).length !== keys.length) { + throw new M47ReferenceGraphContractError("M4.7 queues: состав стадий изменился."); + } + return Object.fromEntries(keys.map((key) => { + const watermark = finite(document[key], `M4.7 queue ${key}`); + if (!Number.isInteger(watermark) || watermark > 2) { + throw new M47ReferenceGraphContractError(`M4.7 queue ${key}: нарушена граница.`); + } + return [key, watermark]; + })) as unknown as M47ReferenceGraphLabResult["queueHighWatermarks"]; +} + +function stringArray(value: JsonValue | undefined, label: string): readonly string[] { + if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) { + throw new M47ReferenceGraphContractError(`${label}: ожидался список строк.`); + } + return value as string[]; +} + +export async function fetchM47ReferenceGraphLab({ + resultId, + fetcher = fetch, + signal, +}: { + resultId: string; + fetcher?: LaboratoryFetch; + signal?: AbortSignal; +}): Promise { + if (!/^m47-reference-graph-lab-[a-f0-9]{64}$/.test(resultId)) { + throw new M47ReferenceGraphContractError("M4.7 result id: нарушена идентичность."); + } + const evidence = await fetchLaboratoryEvidenceReport({ + workId: "m47-reference-graph-shadow", + resultId, + fetcher, + signal, + }); + const report = evidence.rawReport; + exact( + report.schema_version, + "missioncore.reference-perception-graph-lab-report/v1", + "M4.7 report schema", + ); + exact(report.result_id, resultId, "M4.7 report result"); + const source = object(report.source, "M4.7 source"); + const method = object(report.method, "M4.7 method"); + const execution = object(report.execution, "M4.7 execution"); + const metrics = object(report.metrics, "M4.7 metrics"); + const frames = object(metrics.frames, "M4.7 frames"); + const acceptance = object(report.acceptance, "M4.7 acceptance"); + const authority = object(report.authority, "M4.7 authority"); + const visualEvidence = object(report.visual_evidence, "M4.7 visual evidence"); + exact(acceptance.accepted, true, "M4.7 acceptance"); + exact(authority.commands_enabled, false, "M4.7 commands"); + exact(authority.actuation_allowed, false, "M4.7 actuation"); + exact(authority.navigation_or_safety_accepted, false, "M4.7 safety authority"); + exact(visualEvidence.shared_recorded_clock, true, "M4.7 recorded clock"); + exact( + visualEvidence.video_camera_3d_plan_available, + true, + "M4.7 visual evidence", + ); + const linkedVisualResultId = text( + visualEvidence.linked_result_id, + "M4.7 linked visual result", + ); + if (!/^m4-threat-replay-[a-f0-9]{64}$/.test(linkedVisualResultId)) { + throw new M47ReferenceGraphContractError("M4.7 visual binding: нарушена идентичность."); + } + const visual = await fetchM4ThreatReplayResult({ + resultId: linkedVisualResultId, + fetcher, + signal, + }); + if (!visual || visual.resultId !== linkedVisualResultId) { + throw new M47ReferenceGraphContractError( + "M4.7 visual binding: сервер вернул другой replay result.", + ); + } + const graphResultId = text(source.graph_result_id, "M4.7 graph result"); + if (!/^m47-reference-graph-[a-f0-9]{64}$/.test(graphResultId)) { + throw new M47ReferenceGraphContractError("M4.7 graph result: нарушена идентичность."); + } + return { + resultId, + createdAtUtc: text(evidence.createdAtUtc, "M4.7 created at"), + graphResultId, + linkedVisualResultId, + graphId: exact(method.graph_id, "reference-perception-graph/v2", "M4.7 graph id"), + worker: { + id: exact(execution.worker_id, "worker-006", "M4.7 worker"), + node: text(execution.worker_node, "M4.7 worker node"), + artifactSha256: digest(execution.artifact_sha256, "M4.7 artifact", 64), + codeRevision: digest(execution.code_revision, "M4.7 revision", 40), + elapsedSeconds: finite(execution.elapsed_seconds, "M4.7 elapsed"), + }, + frames: { + expected: exact(frames.expected, 4489, "M4.7 expected frames"), + admitted: exact(frames.admitted, 4489, "M4.7 admitted frames"), + delivered: exact(frames.delivered, 4489, "M4.7 delivered frames"), + }, + parityMismatchCounts: zeroParity(metrics.parity_mismatch_counts), + queueHighWatermarks: queueWatermarks(metrics.queue_high_watermarks), + limitations: stringArray(report.limitations, "M4.7 limitations"), + visual, + }; +} diff --git a/apps/control-station/src/core/laboratory/m4ReplayThreat.ts b/apps/control-station/src/core/laboratory/m4ReplayThreat.ts index cd8a290..2a61148 100644 --- a/apps/control-station/src/core/laboratory/m4ReplayThreat.ts +++ b/apps/control-station/src/core/laboratory/m4ReplayThreat.ts @@ -315,22 +315,59 @@ function parseMetricVisual(value: unknown): M4ThreatMetricVisual { } export async function fetchM4ThreatReplayResult({ + resultId: requestedResultId, fetcher = fetch, signal, }: { + resultId?: string; fetcher?: LaboratoryFetch; signal?: AbortSignal; } = {}): Promise { - const response = await fetcher("/api/v1/laboratory/m4-threat/results?limit=1", { - headers: { Accept: "application/json" }, - signal, - }); + if ( + requestedResultId !== undefined + && !/^m4-threat-replay-[a-f0-9]{64}$/.test(requestedResultId) + ) { + throw new M4ThreatContractError("M4.6 requested result id: нарушена идентичность."); + } + let response = await fetcher( + requestedResultId === undefined + ? "/api/v1/laboratory/m4-threat/results?limit=1" + : `/api/v1/laboratory/m4-threat/results/${requestedResultId}`, + { headers: { Accept: "application/json" }, signal }, + ); + // Rolling deployments may briefly pair the new UI with the previous read-only + // API. Preserve exact-result binding by selecting the requested result from + // the catalog instead of silently falling back to whatever happens to be latest. + let catalogFallback = false; + if (requestedResultId !== undefined && response.status === 404) { + response = await fetcher("/api/v1/laboratory/m4-threat/results?limit=10", { + headers: { Accept: "application/json" }, + signal, + }); + catalogFallback = true; + } if (!response.ok) throw new M4ThreatContractError(`M4.6 LAB недоступен: HTTP ${response.status}.`); - const catalog = object(await response.json(), "M4.6 catalog"); - exact(catalog.schema_version, "missioncore.m4-threat-replay-catalog/v1", "M4.6 catalog schema"); - const items = array(catalog.items, "M4.6 results"); - if (!items.length) return null; - const item = object(items[0], "M4.6 result"); + const payload = await response.json(); + let item: Record; + if (requestedResultId === undefined || catalogFallback) { + const catalog = object(payload, "M4.6 catalog"); + exact(catalog.schema_version, "missioncore.m4-threat-replay-catalog/v1", "M4.6 catalog schema"); + const items = array(catalog.items, "M4.6 results"); + if (!items.length) return null; + const selected = requestedResultId === undefined + ? items[0] + : items.find((candidate) => ( + typeof candidate === "object" + && candidate !== null + && (candidate as Record).result_id === requestedResultId + )); + if (selected === undefined) { + throw new M4ThreatContractError("M4.6 requested result: точный результат отсутствует в каталоге."); + } + item = object(selected, "M4.6 result"); + } else { + item = object(payload, "M4.6 result"); + } exact(item.schema_version, "missioncore.m4-threat-replay-view/v1", "M4.6 view schema"); exact(item.accepted, true, "M4.6 acceptance"); exact(item.authority, "replay-simulated", "M4.6 authority"); @@ -349,8 +386,12 @@ export async function fetchM4ThreatReplayResult({ const configuration = object(item.configuration, "M4.6 configuration"); const configuredBodyFrame = object(configuration.body_frame, "M4.6 configured body frame"); const sourceResultIds = object(item.source_result_ids, "M4.6 sources"); + const parsedResultId = resultId(item.result_id); + if (requestedResultId !== undefined) { + exact(parsedResultId, requestedResultId, "M4.6 requested result"); + } return { - resultId: resultId(item.result_id), + resultId: parsedResultId, createdAtUtc: text(item.created_at_utc, "M4.6 created"), profileId: text(item.profile_id, "M4.6 profile"), rigProfileId: text(item.rig_profile_id, "M4.6 rig"), diff --git a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx index 422c490..decfe14 100644 --- a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx @@ -41,6 +41,7 @@ import { E46IGroundingDinoFullReplayResultView } from "./E46IGroundingDinoFullRe import { E46JRawFisheyeRealtimeResultView } from "./E46JRawFisheyeRealtimeResult"; import { E47SemanticSlamResultView } from "./E47SemanticSlamResult"; import { M4ReplayThreatResultView } from "./M4ReplayThreatResult"; +import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult"; export { isAdvancedLaboratoryWorkId }; export type { AdvancedLaboratoryWorkId }; @@ -83,6 +84,9 @@ export function AdvancedLaboratoryResult({ failedSessionId: string | null; replayError: string | null; }) { + if (workId === "m47-reference-graph-shadow" && results.m47Graph) { + return ; + } if (workId === "m4-replay-threat" && results.m4Threat) { return ; } diff --git a/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx b/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx new file mode 100644 index 0000000..4ca6ed8 --- /dev/null +++ b/apps/control-station/src/workspaces/laboratory/M47ReferenceGraphResult.tsx @@ -0,0 +1,132 @@ +import { + LaboratoryEvidence, + LaboratoryResultSummary, + LaboratorySummary, + LaboratoryWorkTemplate, +} from "../../components/laboratory/LaboratoryPresentation"; +import type { M47ReferenceGraphLabResult } from "../../core/laboratory/m47ReferenceGraph"; +import { formatNumber } from "../../presentation"; +import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual"; + +export function M47ReferenceGraphResultView({ + rigLabel, + result, +}: { + rigLabel: string; + result: M47ReferenceGraphLabResult; +}) { + const parityDimensions = Object.keys(result.parityMismatchCounts).length; + const maximumQueue = Math.max(...Object.values(result.queueHighWatermarks)); + return ( + + )} + evidence={( + + + + )} + result={( + + )} + /> + ); +} diff --git a/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts b/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts index 26b24bf..06fe8e7 100644 --- a/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts +++ b/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts @@ -63,6 +63,13 @@ interface KnownWorkDefinition { const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг"; const KNOWN_WORKS: Readonly, KnownWorkDefinition>> = { + "m47-reference-graph-shadow": { + profileId: "rig-dual-evidence-virtual-corridor-v1", + profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`, + experimentId: "m47-reference-perception-graph-shadow", + experimentName: "RAVNOVES00 canonical reference graph", + variantName: "M4.7 · Worker 006 · lossless graph parity", + }, "m4-replay-threat": { profileId: "rig-dual-evidence-virtual-corridor-v1", profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`, diff --git a/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts b/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts index b51c9b2..5412bdb 100644 --- a/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts +++ b/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts @@ -18,6 +18,7 @@ function mergeResults( next: AdvancedLaboratoryResults, ): AdvancedLaboratoryResults { return { + m47Graph: next.m47Graph ?? current.m47Graph, m4Threat: next.m4Threat ?? current.m4Threat, l3: next.l3 ?? current.l3, l31: next.l31 ?? current.l31, @@ -109,12 +110,15 @@ export function useAdvancedLaboratoryCatalog({ !isAdvancedLaboratoryWorkId(selectedWorkId) || advancedLaboratoryResultAvailable(selectedWorkId, results) ) return; + const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId; + if (selectedWorkId === "m47-reference-graph-shadow" && !indexedResultId) return; const controller = new AbortController(); setLoadingWorkId(selectedWorkId); setFailedWorkId(null); setResultError(null); void fetchAdvancedLaboratoryResult(selectedWorkId, { signal: controller.signal, + resultId: indexedResultId, }).then(async (next) => { if (controller.signal.aborted) return; setResults((current) => mergeResults(current, next)); @@ -128,7 +132,7 @@ export function useAdvancedLaboratoryCatalog({ if (!controller.signal.aborted) setLoadingWorkId(null); }); return () => controller.abort(); - }, [results, selectedWorkId]); + }, [index, results, selectedWorkId]); return { index, diff --git a/apps/control-station/test/m47ReferenceGraph.test.mjs b/apps/control-station/test/m47ReferenceGraph.test.mjs new file mode 100644 index 0000000..3868543 --- /dev/null +++ b/apps/control-station/test/m47ReferenceGraph.test.mjs @@ -0,0 +1,214 @@ +import assert from "node:assert/strict"; +import { after, before, test } from "node:test"; + +import { createServer } from "vite"; + +let server; +let fetchM47ReferenceGraphLab; + +const resultId = `m47-reference-graph-lab-${"a".repeat(64)}`; +const graphResultId = `m47-reference-graph-${"b".repeat(64)}`; +const visualResultId = `m4-threat-replay-${"c".repeat(64)}`; + +before(async () => { + server = await createServer({ + appType: "custom", + logLevel: "silent", + server: { middlewareMode: true }, + }); + ({ fetchM47ReferenceGraphLab } = await server.ssrLoadModule( + "/src/core/laboratory/m47ReferenceGraph.ts", + )); +}); + +after(async () => { + await server?.close(); +}); + +function m4Result() { + return { + schema_version: "missioncore.m4-threat-replay-view/v1", + result_id: visualResultId, + created_at_utc: "2026-08-05T15:36:01.553Z", + status: "accepted", + profile_id: "m4-ravnoves00-virtual-corridor/v3", + rig_profile_id: "virtual-base-footprint-1000x600/v3", + corridor_profile_id: "ravnoves00-forward-corridor-8m/v3", + source_result_ids: { + detector: `m4-detector-replay-${"d".repeat(64)}`, + geometry: `m4-geometry-replay-${"e".repeat(64)}`, + temporal: `m4-temporal-replay-${"f".repeat(64)}`, + }, + metrics: { + decisions: { threat: 7606, "not-threat": 10769, unknown: 128878 }, + evidence: { + "camera-only": 10158, + "current-metric": 28081, + "rolling-map-retained": 70989, + "stale-or-held": 38025, + }, + fixtures: { critical: 5, critical_false_not_threat: 0, passed: 10, total: 10 }, + runtime: { + frames_per_second: 175.0, + provider_latency_p50_ms: 2.5, + provider_latency_p95_ms: 7.2, + provider_latency_max_ms: 26.6, + }, + body_frame: { + available: 3928, + qualified: 3861, + rejected: 67, + camera_forward_alignment_deg: { p95: 8.439, maximum: 24.252 }, + }, + reason_counts: { "geometry-only-evidence": 87550 }, + }, + configuration: { + virtual_body_m: [1, 0.6], + nominal_sensor_height_m: 1.25, + forward_corridor_m: 8, + prediction_horizon_seconds: 5, + body_frame: { + origin: "local-surface-vertical-projection", + up: "vendor-slam-map-gravity-axis", + forward: "smoothed-slam-trajectory-validated-by-camera-axis", + }, + }, + limitations: ["replay only"], + accepted: true, + authority: "replay-simulated", + physical_collision_accepted: false, + actuation_allowed: false, + }; +} + +function evidenceReport() { + const mismatch = { + source_binding: 0, + current: 0, + rolling_retained: 0, + held: 0, + expired: 0, + camera_uncertainty: 0, + threat_assessments: 0, + }; + const raw = { + schema_version: "missioncore.reference-perception-graph-lab-report/v1", + result_id: resultId, + source: { graph_result_id: graphResultId }, + method: { graph_id: "reference-perception-graph/v2" }, + execution: { + worker_id: "worker-006", + worker_node: "DESKTOP-OPJ8J04", + artifact_sha256: "1".repeat(64), + code_revision: "2".repeat(40), + elapsed_seconds: 198.126, + }, + metrics: { + frames: { expected: 4489, admitted: 4489, delivered: 4489 }, + parity_mismatch_counts: mismatch, + queue_high_watermarks: { + detector: 2, + geometry: 2, + temporal: 2, + rolling: 2, + threat: 2, + }, + }, + acceptance: { accepted: true }, + authority: { + commands_enabled: false, + actuation_allowed: false, + navigation_or_safety_accepted: false, + }, + visual_evidence: { + linked_result_id: visualResultId, + shared_recorded_clock: true, + video_camera_3d_plan_available: true, + }, + limitations: ["recorded replay only"], + }; + return { + schema_version: "missioncore.laboratory-evidence-report/v1", + work_id: "m47-reference-graph-shadow", + result_id: resultId, + created_at_utc: "2026-08-23T18:00:16.061Z", + access: "read-only", + proof: { + document_schema_version: "missioncore.reference-perception-graph-lab/v1", + document_sha256: "3".repeat(64), + identity_sha256: "a".repeat(64), + report_schema_version: raw.schema_version, + report_sha256: "4".repeat(64), + artifact_count: 6, + verified_artifact_count: 6, + }, + completeness: { identity: "recorded" }, + identity: { authority: raw.authority }, + source: raw.source, + configuration: null, + method: raw.method, + execution: raw.execution, + resources: null, + metrics: raw.metrics, + gates: raw.acceptance, + decision: null, + limitations: raw.limitations, + authority: raw.authority, + artifacts: [], + visual_evidence: {}, + raw_report: raw, + }; +} + +test("M4.7 binds exact Worker graph proof to the synchronized M4.6 visual", async () => { + const requests = []; + const result = await fetchM47ReferenceGraphLab({ + resultId, + fetcher: async (input) => { + const path = String(input); + requests.push(path); + return new Response(JSON.stringify( + path.includes("/evidence-reports/") ? evidenceReport() : m4Result(), + ), { status: 200 }); + }, + }); + + assert.equal(result.graphResultId, graphResultId); + assert.equal(result.visual.resultId, visualResultId); + assert.equal(result.worker.id, "worker-006"); + assert.equal(result.frames.delivered, 4489); + assert.deepEqual(Object.values(result.parityMismatchCounts), Array(7).fill(0)); + assert.equal(Math.max(...Object.values(result.queueHighWatermarks)), 2); + assert.deepEqual(requests, [ + `/api/v1/laboratory/evidence-reports/m47-reference-graph-shadow/${resultId}`, + `/api/v1/laboratory/m4-threat/results/${visualResultId}`, + ]); +}); + +test("M4.7 keeps exact visual binding during a rolling API deployment", async () => { + const requests = []; + const result = await fetchM47ReferenceGraphLab({ + resultId, + fetcher: async (input) => { + const path = String(input); + requests.push(path); + if (path.includes("/evidence-reports/")) { + return new Response(JSON.stringify(evidenceReport()), { status: 200 }); + } + if (path.endsWith(`/${visualResultId}`)) { + return new Response(null, { status: 404 }); + } + return new Response(JSON.stringify({ + schema_version: "missioncore.m4-threat-replay-catalog/v1", + items: [m4Result()], + }), { status: 200 }); + }, + }); + + assert.equal(result.visual.resultId, visualResultId); + assert.deepEqual(requests, [ + `/api/v1/laboratory/evidence-reports/m47-reference-graph-shadow/${resultId}`, + `/api/v1/laboratory/m4-threat/results/${visualResultId}`, + "/api/v1/laboratory/m4-threat/results?limit=10", + ]); +}); diff --git a/config/laboratories/m47-reference-graph-shadow.json b/config/laboratories/m47-reference-graph-shadow.json new file mode 100644 index 0000000..8da1bec --- /dev/null +++ b/config/laboratories/m47-reference-graph-shadow.json @@ -0,0 +1,10 @@ +{ + "schema_version": "missioncore.laboratory-evidence-definition/v1", + "work_id": "m47-reference-graph-shadow", + "evidence": { + "runtime_relative_root": "m47/reference-graph-labs", + "result_id_prefix": "m47-reference-graph-lab", + "document_name": "manifest.json", + "schema_version": "missioncore.reference-perception-graph-lab/v1" + } +} diff --git a/config/laboratory-execution.json b/config/laboratory-execution.json index 3b2b02b..c34288d 100644 --- a/config/laboratory-execution.json +++ b/config/laboratory-execution.json @@ -89,6 +89,7 @@ } ], "legacy_work_ids": [ + "m47-reference-graph-shadow", "e31-source-binding", "e32-track-geometry", "e34-temporal-layer", diff --git a/scripts/publish_m47_reference_graph_lab.py b/scripts/publish_m47_reference_graph_lab.py new file mode 100644 index 0000000..c988936 --- /dev/null +++ b/scripts/publish_m47_reference_graph_lab.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Publish the accepted Worker 006 M4.7 graph as a visual LAB result.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.laboratory.m47_reference_graph import publish_m47_reference_graph_lab + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--graph-result-root", type=Path, required=True) + parser.add_argument("--visual-result-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = publish_m47_reference_graph_lab( + graph_result_root=args.graph_result_root, + visual_result_root=args.visual_result_root, + output_root=args.output_root, + ) + print( + json.dumps( + { + "accepted": True, + "result_id": result.result_id, + "result_root": str(result.result_root), + }, + sort_keys=True, + separators=(",", ":"), + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/k1link/laboratory/m47_reference_graph.py b/src/k1link/laboratory/m47_reference_graph.py new file mode 100644 index 0000000..68de7b2 --- /dev/null +++ b/src/k1link/laboratory/m47_reference_graph.py @@ -0,0 +1,355 @@ +"""Publish an immutable visual LAB binding for the accepted M4.7 graph shadow.""" + +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Final + +from k1link.perception.reference_graph_result import read_reference_graph_result +from k1link.perception.threat_replay import read_threat_replay_result + +M47_REFERENCE_GRAPH_LAB_SCHEMA: Final = "missioncore.reference-perception-graph-lab/v1" +M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA: Final = ( + "missioncore.reference-perception-graph-lab-report/v1" +) +M47_REFERENCE_GRAPH_LAB_PREFIX: Final = "m47-reference-graph-lab-" + + +class M47ReferenceGraphLabError(RuntimeError): + """The M4.7 graph and visual replay cannot be bound without exact proof.""" + + +@dataclass(frozen=True, slots=True) +class M47ReferenceGraphLab: + result_id: str + result_root: Path + manifest: dict[str, object] + report: dict[str, object] + + +def publish_m47_reference_graph_lab( + *, + graph_result_root: Path, + visual_result_root: Path, + output_root: Path, +) -> M47ReferenceGraphLab: + graph = read_reference_graph_result(graph_result_root) + visual = read_threat_replay_result(visual_result_root) + if not graph.accepted or not visual.accepted: + raise M47ReferenceGraphLabError("both graph and visual replay must be accepted") + graph_report = graph.report + graph_manifest = graph.manifest + graph_runtime = _object(graph_manifest.get("runtime"), "graph runtime") + parity = _object(graph_report.get("accepted_parity"), "graph parity") + mismatches = _object(parity.get("mismatch_counts"), "graph parity mismatches") + visual_identity = _object(visual.manifest.get("identity"), "visual identity") + if ( + parity.get("accepted") is not True + or parity.get("expected_frames") != 4489 + or parity.get("compared_frames") != 4489 + or set(mismatches) + != { + "source_binding", + "current", + "rolling_retained", + "held", + "expired", + "camera_uncertainty", + "threat_assessments", + } + or any(value != 0 for value in mismatches.values()) + or parity.get("threat_frames_sha256") != visual_identity.get("frames_sha256") + or parity.get("temporal_frames_sha256") != visual_identity.get("temporal_frames_sha256") + ): + raise M47ReferenceGraphLabError("graph-to-visual parity proof changed") + terminal = _object(graph_report.get("terminal_outcomes"), "terminal outcomes") + queues = _object(graph_report.get("queue_high_watermarks"), "queue high watermarks") + execution = _object(graph_report.get("execution"), "graph execution") + authority = { + "physical_live": False, + "commands_enabled": False, + "actuation_allowed": False, + "navigation_or_safety_accepted": False, + "ground_truth": False, + "mode": "replay-simulated", + } + visual_evidence = { + "linked_result_id": visual.result_id, + "binding": "exact-threat-and-temporal-ledger-parity", + "timeline_frames": 4489, + "shared_recorded_clock": True, + "video_camera_3d_plan_available": True, + "regression_sequences": [138, 274, 1880, 2584], + "independent_ground_truth": False, + } + identity: dict[str, object] = { + "schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA, + "binding_id": "m47-reference-graph-visual-binding/v1", + "source": { + "source_id": "RAVNOVES00", + "source_session_id": visual_identity.get("source_session_id"), + "graph_result_id": graph.result_id, + "visual_result_id": visual.result_id, + "temporal_frames_sha256": parity.get("temporal_frames_sha256"), + "threat_frames_sha256": parity.get("threat_frames_sha256"), + }, + "method": { + "graph_id": graph_manifest.get("graph_id"), + "run_mode": graph_manifest.get("run_mode"), + "source_profile_id": graph_manifest.get("source_profile_id"), + "canonical_payload_sha256": graph_manifest.get("canonical_payload_sha256"), + "parity_schema_version": parity.get("schema_version"), + }, + "execution": graph_runtime, + "acceptance": { + "accepted": True, + "expected_frames": 4489, + "admitted_frames": graph_report.get("admitted_frames"), + "delivered_frames": terminal.get("delivered"), + "parity_mismatch_counts": mismatches, + }, + "visual_evidence": visual_evidence, + "authority": authority, + } + identity_sha256 = _canonical_sha256(identity) + result_id = f"{M47_REFERENCE_GRAPH_LAB_PREFIX}{identity_sha256}" + elapsed_seconds = execution.get("elapsed_seconds") + report: dict[str, object] = { + "schema_version": M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA, + "result_id": result_id, + "source": identity["source"], + "method": identity["method"], + "execution": { + **graph_runtime, + "elapsed_seconds": elapsed_seconds, + }, + "metrics": { + "frames": { + "expected": 4489, + "admitted": graph_report.get("admitted_frames"), + "delivered": terminal.get("delivered"), + "failed": terminal.get("failed", 0), + "stale": terminal.get("stale", 0), + "superseded": terminal.get("superseded", 0), + "rejected": terminal.get("rejected", 0), + "unavailable": terminal.get("unavailable", 0), + }, + "queue_high_watermarks": queues, + "parity_mismatch_counts": mismatches, + "canonical_payload_sha256": graph_manifest.get("canonical_payload_sha256"), + }, + "acceptance": { + "accepted": True, + "gates": graph_report.get("gates"), + "parity": parity, + }, + "decision": { + "state": "accepted-reference-graph-replay", + "next_gate": "independent-object-centric-detection-quality", + "summary": ( + "Canonical source→detector→geometry→temporal/motion→rolling→threat " + "graph preserves the accepted M4.5R/M4.6 payload for every frame." + ), + }, + "limitations": [ + "This result proves recorded lossless replay parity, not physical live operation.", + "The linked M4.6 visual replay is engineering evidence, not independent object truth.", + "No navigation, safety, command or actuation authority is granted.", + "Independent object-centric detection quality remains the next acceptance gate.", + ], + "authority": authority, + "visual_evidence": visual_evidence, + } + output = output_root.expanduser().resolve() + output.mkdir(mode=0o700, parents=True, exist_ok=True) + if output.is_symlink() or not output.is_dir(): + raise M47ReferenceGraphLabError("LAB output root must be a real directory") + staging = output / f".m47-reference-graph-lab.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + sources = { + "graph-manifest.json": graph.result_root / "manifest.json", + "graph-report.json": graph.result_root / "report.json", + "graph-runtime.json": graph.result_root / "runtime.json", + "visual-manifest.json": visual.result_root / "manifest.json", + "visual-report.json": visual.result_root / "report.json", + } + for name, source in sources.items(): + shutil.copyfile(source, staging / name) + _write_json(staging / "report.json", report) + roles = { + "report.json": "m47-lab-report", + "graph-manifest.json": "m47-graph-manifest", + "graph-report.json": "m47-graph-report", + "graph-runtime.json": "m47-runtime", + "visual-manifest.json": "linked-visual-manifest", + "visual-report.json": "linked-visual-report", + } + schemas = { + "graph-manifest.json": graph_manifest.get("schema_version"), + "graph-report.json": graph_report.get("schema_version"), + "graph-runtime.json": graph_runtime.get("schema_version"), + "visual-manifest.json": visual.manifest.get("schema_version"), + "visual-report.json": visual.report.get("schema_version"), + "report.json": M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA, + } + artifacts = [_artifact(staging / name, role, schemas[name]) for name, role in roles.items()] + manifest: dict[str, object] = { + "schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": graph_runtime.get("started_at_utc"), + "accepted": True, + "ground_truth": False, + "authority": authority, + "artifacts": artifacts, + } + _write_json(staging / "manifest.json", manifest) + target = output / result_id + _publish(staging, target) + except Exception: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_m47_reference_graph_lab(target) + + +def read_m47_reference_graph_lab(result_root: Path) -> M47ReferenceGraphLab: + candidate = result_root.expanduser().absolute() + if candidate.is_symlink(): + raise M47ReferenceGraphLabError("M4.7 LAB result root is invalid") + resolved = candidate.resolve(strict=True) + if ( + resolved.is_symlink() + or not resolved.is_dir() + or not resolved.name.startswith(M47_REFERENCE_GRAPH_LAB_PREFIX) + ): + raise M47ReferenceGraphLabError("M4.7 LAB result root is invalid") + manifest = _read_json(resolved / "manifest.json") + if ( + manifest.get("schema_version") != M47_REFERENCE_GRAPH_LAB_SCHEMA + or manifest.get("result_id") != resolved.name + or manifest.get("accepted") is not True + or manifest.get("ground_truth") is not False + ): + raise M47ReferenceGraphLabError("M4.7 LAB manifest changed") + identity = _object(manifest.get("identity"), "LAB identity") + identity_sha256 = _canonical_sha256(identity) + if ( + manifest.get("identity_sha256") != identity_sha256 + or resolved.name != f"{M47_REFERENCE_GRAPH_LAB_PREFIX}{identity_sha256}" + ): + raise M47ReferenceGraphLabError("M4.7 LAB identity changed") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 6: + raise M47ReferenceGraphLabError("M4.7 LAB artifact inventory changed") + for value in artifacts: + descriptor = _object(value, "LAB artifact") + path_value = descriptor.get("path") + if not isinstance(path_value, str) or "/" in path_value or "\\" in path_value: + raise M47ReferenceGraphLabError("M4.7 LAB artifact path changed") + path = resolved / path_value + if ( + path.is_symlink() + or not path.is_file() + or path.stat().st_size != descriptor.get("byte_length") + or _file_sha256(path) != descriptor.get("sha256") + ): + raise M47ReferenceGraphLabError("M4.7 LAB artifact proof changed") + report = _read_json(resolved / "report.json") + if ( + report.get("schema_version") != M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or _object(report.get("acceptance"), "LAB acceptance").get("accepted") is not True + or report.get("authority") != identity.get("authority") + ): + raise M47ReferenceGraphLabError("M4.7 LAB report changed") + return M47ReferenceGraphLab( + result_id=resolved.name, + result_root=resolved, + manifest=manifest, + report=report, + ) + + +def _artifact(path: Path, role: str, schema: object) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _file_sha256(path), + "schema_version": schema, + "media_type": "application/json", + } + + +def _publish(staging: Path, target: Path) -> None: + if target.exists(): + existing = {path.name: _file_sha256(path) for path in target.iterdir() if path.is_file()} + proposed = {path.name: _file_sha256(path) for path in staging.iterdir() if path.is_file()} + if existing != proposed: + raise M47ReferenceGraphLabError("immutable M4.7 LAB identity collision") + shutil.rmtree(staging) + return + os.replace(staging, target) + + +def _object(value: object, label: str) -> dict[str, object]: + if not isinstance(value, dict) or any(not isinstance(key, str) for key in value): + raise M47ReferenceGraphLabError(f"{label} must be an object") + return value + + +def _write_json(path: Path, document: dict[str, object]) -> None: + with path.open("wb") as stream: + stream.write(_canonical_json(document) + b"\n") + stream.flush() + os.fsync(stream.fileno()) + + +def _read_json(path: Path) -> dict[str, object]: + if path.is_symlink() or not path.is_file() or path.stat().st_size > 1024 * 1024: + raise M47ReferenceGraphLabError("M4.7 LAB JSON artifact is invalid") + try: + return _object(json.loads(path.read_text("utf-8")), "M4.7 LAB JSON artifact") + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise M47ReferenceGraphLabError("M4.7 LAB JSON artifact is unreadable") from error + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + + +def _canonical_sha256(value: object) -> str: + return hashlib.sha256(_canonical_json(value)).hexdigest() + + +def _file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +__all__ = [ + "M47_REFERENCE_GRAPH_LAB_PREFIX", + "M47_REFERENCE_GRAPH_LAB_REPORT_SCHEMA", + "M47_REFERENCE_GRAPH_LAB_SCHEMA", + "M47ReferenceGraphLab", + "M47ReferenceGraphLabError", + "publish_m47_reference_graph_lab", + "read_m47_reference_graph_lab", +] diff --git a/src/k1link/perception/reference_graph_result.py b/src/k1link/perception/reference_graph_result.py index 8e793c9..729111f 100644 --- a/src/k1link/perception/reference_graph_result.py +++ b/src/k1link/perception/reference_graph_result.py @@ -153,6 +153,103 @@ def seal_reference_graph_result( ) +def read_reference_graph_result(result_root: Path) -> SealedReferenceGraphResult: + candidate = result_root.expanduser().absolute() + if candidate.is_symlink(): + raise ReferenceGraphResultError("reference graph result root is invalid") + resolved = candidate.resolve(strict=True) + if ( + resolved.is_symlink() + or not resolved.is_dir() + or not resolved.name.startswith(REFERENCE_GRAPH_RESULT_PREFIX) + ): + raise ReferenceGraphResultError("reference graph result root is invalid") + manifest = _read_json(resolved / "manifest.json") + expected_manifest_keys = { + "schema_version", + "result_id", + "identity_sha256", + "graph_id", + "source_profile_id", + "run_mode", + "canonical_payload_sha256", + "runtime", + "files", + "accepted", + } + if set(manifest) != expected_manifest_keys: + raise ReferenceGraphResultError("reference graph manifest fields changed") + if manifest.get("schema_version") != REFERENCE_GRAPH_MANIFEST_SCHEMA: + raise ReferenceGraphResultError("reference graph manifest schema changed") + if not isinstance(manifest.get("accepted"), bool): + raise ReferenceGraphResultError("reference graph acceptance type changed") + identity = { + key: manifest[key] + for key in ( + "graph_id", + "source_profile_id", + "run_mode", + "canonical_payload_sha256", + "runtime", + "files", + ) + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("result_id") != resolved.name + or manifest.get("identity_sha256") != identity_sha256 + or resolved.name != f"{REFERENCE_GRAPH_RESULT_PREFIX}{identity_sha256}" + ): + raise ReferenceGraphResultError("reference graph identity changed") + runtime = ReferenceGraphRuntimeIdentity.from_dict(manifest.get("runtime")) + files = manifest.get("files") + if not isinstance(files, dict) or set(files) != { + "frames.jsonl", + "outcomes.jsonl", + "report.json", + "runtime.json", + }: + raise ReferenceGraphResultError("reference graph artifact inventory changed") + for name, descriptor_value in files.items(): + if not isinstance(descriptor_value, dict) or set(descriptor_value) != { + "sha256", + "bytes", + }: + raise ReferenceGraphResultError("reference graph artifact proof changed") + path = _safe_result_file(resolved, name) + if descriptor_value.get("bytes") != path.stat().st_size or descriptor_value.get( + "sha256" + ) != _sha256_file(path): + raise ReferenceGraphResultError("reference graph artifact proof does not match") + runtime_document = _read_json(resolved / "runtime.json") + if runtime_document != runtime.to_dict(): + raise ReferenceGraphResultError("reference graph runtime artifact changed") + report = _read_json(resolved / "report.json") + if ( + report.get("schema_version") != REFERENCE_GRAPH_REPORT_SCHEMA + or report.get("graph_id") != manifest.get("graph_id") + or report.get("canonical_payload_sha256") != manifest.get("canonical_payload_sha256") + or not isinstance(report.get("execution"), dict) + or report["execution"].get("runtime_identity") != runtime_document + or report.get("accepted") is not manifest.get("accepted") + ): + raise ReferenceGraphResultError("reference graph report changed") + gates = report.get("gates") + if ( + not isinstance(gates, dict) + or not gates + or manifest.get("accepted") is not all(value is True for value in gates.values()) + ): + raise ReferenceGraphResultError("reference graph acceptance proof changed") + return SealedReferenceGraphResult( + result_id=resolved.name, + result_root=resolved, + accepted=bool(manifest["accepted"]), + report=report, + manifest=manifest, + ) + + def _write_json_lines(path: Path, rows: tuple[dict[str, object], ...]) -> None: with path.open("wb") as handle: for row in rows: @@ -168,6 +265,28 @@ def _write_json(path: Path, document: dict[str, object]) -> None: os.fsync(handle.fileno()) +def _read_json(path: Path) -> dict[str, object]: + if path.is_symlink() or not path.is_file() or path.stat().st_size > 1024 * 1024: + raise ReferenceGraphResultError("reference graph JSON artifact is invalid") + try: + document = json.loads(path.read_text("utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error: + raise ReferenceGraphResultError("reference graph JSON artifact is unreadable") from error + if not isinstance(document, dict) or any(not isinstance(key, str) for key in document): + raise ReferenceGraphResultError("reference graph JSON artifact must be an object") + return document + + +def _safe_result_file(root: Path, name: str) -> Path: + path = root / name + if path.is_symlink(): + raise ReferenceGraphResultError("reference graph artifact must not be a symlink") + resolved = path.resolve(strict=True) + if resolved.parent != root or not resolved.is_file(): + raise ReferenceGraphResultError("reference graph artifact escaped its result") + return resolved + + def _publish_immutable(staging: Path, target: Path) -> None: if target.exists(): if target.is_symlink() or not target.is_dir(): @@ -199,5 +318,6 @@ __all__ = [ "REFERENCE_GRAPH_RESULT_PREFIX", "ReferenceGraphResultError", "SealedReferenceGraphResult", + "read_reference_graph_result", "seal_reference_graph_result", ] diff --git a/src/k1link/web/m4_threat_replay_api.py b/src/k1link/web/m4_threat_replay_api.py index ef2af8a..92dcaf8 100644 --- a/src/k1link/web/m4_threat_replay_api.py +++ b/src/k1link/web/m4_threat_replay_api.py @@ -98,6 +98,10 @@ def build_m4_threat_replay_router( "access": "read-only-replay-simulated", } + @router.get("/results/{result_id}") + def get_result(result_id: str) -> dict[str, object]: + return _project_result(result(result_id)) + @router.get("/results/{result_id}/visuals") def list_visuals(result_id: str) -> dict[str, object]: frozen = result(result_id) diff --git a/tests/test_laboratory_evidence_registry.py b/tests/test_laboratory_evidence_registry.py index 0f93a6e..395acb5 100644 --- a/tests/test_laboratory_evidence_registry.py +++ b/tests/test_laboratory_evidence_registry.py @@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None: repository_root / "config" / "laboratories" ) - assert len(registry.definitions) == 33 + assert len(registry.definitions) == 34 assert {item.work_id for item in registry.definitions} >= { "e31-source-binding", "e46j-raw-fisheye-realtime", @@ -138,4 +138,5 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None: "l33-camera-first-detector-review", "l34f-adjudicated-reference", "m4-replay-threat", + "m47-reference-graph-shadow", } diff --git a/tests/test_m47_reference_graph_lab.py b/tests/test_m47_reference_graph_lab.py new file mode 100644 index 0000000..cfd5b62 --- /dev/null +++ b/tests/test_m47_reference_graph_lab.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import json +from pathlib import Path +from types import SimpleNamespace + +from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry +from k1link.laboratory.evidence_report import ( + LaboratoryEvidenceReportService, + verify_laboratory_evidence_result, +) +from k1link.laboratory.m47_reference_graph import ( + publish_m47_reference_graph_lab, + read_m47_reference_graph_lab, +) + + +def _write_json(path: Path, value: object) -> None: + path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n") + + +def test_m47_lab_binds_worker_graph_to_exact_visual_replay( + tmp_path: Path, + monkeypatch, +) -> None: + graph_root = tmp_path / "graph" + visual_root = tmp_path / "visual" + graph_root.mkdir() + visual_root.mkdir() + runtime = { + "schema_version": "missioncore.reference-graph-runtime-identity/v3", + "worker_id": "worker-006", + "worker_node": "DESKTOP-OPJ8J04", + "worker_container_id": "a" * 64, + "worker_image_id": "sha256:" + "b" * 64, + "isolated_triton_container_id": "c" * 64, + "isolated_triton_image_id": "sha256:" + "b" * 64, + "historical_worker_container_id": "d" * 64, + "historical_worker_running": False, + "historical_triton_container_id": "e" * 64, + "historical_triton_running": False, + "artifact_sha256": "f" * 64, + "patch_id": "mission-core-m47-test", + "code_revision": "1" * 40, + "graph_id": "reference-perception-graph/v2", + "started_at_utc": "2026-08-23T18:00:16.061Z", + "source_mount_read_only": True, + "isolated_model_service": True, + "public_worker_port_added": False, + "commands_enabled": False, + "actuation_allowed": False, + } + mismatch_counts = { + "source_binding": 0, + "current": 0, + "rolling_retained": 0, + "held": 0, + "expired": 0, + "camera_uncertainty": 0, + "threat_assessments": 0, + } + graph_manifest = { + "schema_version": "missioncore.reference-perception-graph-manifest/v1", + "graph_id": "reference-perception-graph/v2", + "source_profile_id": "m4-ravnoves00-recorded-realtime/v1", + "run_mode": "lossless-replay", + "canonical_payload_sha256": "2" * 64, + "runtime": runtime, + } + graph_report = { + "schema_version": "missioncore.reference-perception-graph-report/v1", + "admitted_frames": 4489, + "terminal_outcomes": {"delivered": 4489}, + "queue_high_watermarks": { + "detector": 2, + "geometry": 2, + "temporal": 2, + "rolling": 2, + "threat": 2, + }, + "accepted_parity": { + "schema_version": "missioncore.reference-perception-graph-parity/v1", + "accepted": True, + "expected_frames": 4489, + "compared_frames": 4489, + "temporal_frames_sha256": "3" * 64, + "threat_frames_sha256": "4" * 64, + "mismatch_counts": mismatch_counts, + }, + "execution": {"elapsed_seconds": 198.0, "runtime_identity": runtime}, + "gates": {"accepted": True}, + } + visual_manifest = { + "schema_version": "missioncore.perception-threat-replay-result/v2", + "identity": { + "source_session_id": "20260720T065719Z_viewer_live", + "frames_sha256": "4" * 64, + "temporal_frames_sha256": "3" * 64, + }, + } + visual_report = {"schema_version": "missioncore.perception-threat-replay-report/v2"} + for root, documents in ( + ( + graph_root, + { + "manifest.json": graph_manifest, + "report.json": graph_report, + "runtime.json": runtime, + }, + ), + ( + visual_root, + {"manifest.json": visual_manifest, "report.json": visual_report}, + ), + ): + for name, document in documents.items(): + _write_json(root / name, document) + graph = SimpleNamespace( + accepted=True, + result_id="m47-reference-graph-" + "5" * 64, + result_root=graph_root, + manifest=graph_manifest, + report=graph_report, + ) + visual = SimpleNamespace( + accepted=True, + result_id="m4-threat-replay-" + "6" * 64, + result_root=visual_root, + manifest=visual_manifest, + report=visual_report, + ) + monkeypatch.setattr( + "k1link.laboratory.m47_reference_graph.read_reference_graph_result", + lambda _: graph, + ) + monkeypatch.setattr( + "k1link.laboratory.m47_reference_graph.read_threat_replay_result", + lambda _: visual, + ) + + runtime_root = tmp_path / "runtime" + result = publish_m47_reference_graph_lab( + graph_result_root=graph_root, + visual_result_root=visual_root, + output_root=runtime_root / "m47" / "reference-graph-labs", + ) + + assert read_m47_reference_graph_lab(result.result_root) == result + assert result.report["visual_evidence"]["linked_result_id"] == visual.result_id + assert result.report["execution"]["worker_id"] == "worker-006" + assert result.report["metrics"]["parity_mismatch_counts"] == mismatch_counts + repository_root = Path(__file__).resolve().parents[1] + registry = LaboratoryEvidenceRegistry.from_directory( + repository_root / "config" / "laboratories" + ) + definition = next( + item for item in registry.definitions if item.work_id == "m47-reference-graph-shadow" + ) + proof = verify_laboratory_evidence_result(definition, result.result_root) + assert proof["artifact_count"] == 6 + projected = LaboratoryEvidenceReportService(registry, lambda: runtime_root).read( + definition.work_id, + result.result_id, + ) + assert projected["raw_report"]["schema_version"] == ( + "missioncore.reference-perception-graph-lab-report/v1" + ) diff --git a/tests/test_m4_threat_replay_result.py b/tests/test_m4_threat_replay_result.py index 204bec4..8689fb5 100644 --- a/tests/test_m4_threat_replay_result.py +++ b/tests/test_m4_threat_replay_result.py @@ -100,11 +100,13 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non def test_m4_6_lab_api_projects_report_and_exact_visual_frame() -> None: list_results = _endpoint("/api/v1/laboratory/m4-threat/results") + get_result = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}") list_visuals = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/visuals") get_visual = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}") catalog = list_results(limit=1) assert catalog["items"][0]["result_id"] == RESULT_ID + assert get_result(RESULT_ID) == catalog["items"][0] assert catalog["items"][0]["authority"] == "replay-simulated" visuals = list_visuals(RESULT_ID) assert len(visuals["items"]) == 32 diff --git a/tests/test_reference_graph_result.py b/tests/test_reference_graph_result.py index 36bcb66..ce0bb26 100644 --- a/tests/test_reference_graph_result.py +++ b/tests/test_reference_graph_result.py @@ -16,7 +16,10 @@ from k1link.perception.graph_contracts import ( from k1link.perception.providers import ReferencePerceptionGraphConfigV2 from k1link.perception.reference_graph_identity import ReferenceGraphRuntimeIdentity from k1link.perception.reference_graph_parity import ReferenceGraphParityReport -from k1link.perception.reference_graph_result import seal_reference_graph_result +from k1link.perception.reference_graph_result import ( + read_reference_graph_result, + seal_reference_graph_result, +) REPOSITORY_ROOT = Path(__file__).resolve().parents[1] @@ -174,6 +177,7 @@ def test_reference_graph_result_is_content_addressed_and_reproducible(tmp_path: assert runtime == _runtime().to_dict() assert first.manifest["runtime"] == runtime assert first.manifest["files"]["runtime.json"]["sha256"] + assert read_reference_graph_result(first.result_root) == first def test_reference_graph_result_fails_closed_on_supersession(tmp_path: Path) -> None: