From 63ea2bed672cd17251fc484560a58d0de2e1075c Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Mon, 7 Sep 2026 10:31:33 +0300 Subject: [PATCH] feat(k1): stabilize LAB bridge and isolate onboard device integration --- .../src/components/RerunViewport.tsx | 12 +- .../laboratory/CanonicalResultRerunReplay.tsx | 5 +- .../src/core/device-plugins/contracts.ts | 4 + .../core/environment/environmentSettings.ts | 8 +- .../src/core/observation/viewerProfile.ts | 21 +- apps/control-station/src/productModel.ts | 18 +- .../src/workspaces/fleet/VehicleSensors.tsx | 13 +- .../test/applicationArchitecture.test.mjs | 2 +- .../test/devicePluginContracts.test.mjs | 18 +- .../devicePluginFrontendBoundary.test.mjs | 22 +- .../test/environmentSettings.test.mjs | 4 +- .../test/k1SupervisorPresentation.test.mjs | 79 +- .../test/networkProvisioning.test.mjs | 133 + .../test/observatoryWorkspace.test.mjs | 4 +- .../test/polygonRunArchive.test.mjs | 2 +- .../test/productShellContract.test.mjs | 4 +- .../test/sensorEnrollment.test.mjs | 102 + .../test/viewerProfile.test.mjs | 13 + apps/control-station/tsconfig.app.json | 34 +- apps/control-station/vite.config.ts | 1 + apps/node-agent/cmd/node-agent/main.go | 6 + .../internal/node/device_enrollment.go | 323 + .../internal/node/device_enrollment_test.go | 111 + apps/node-agent/internal/node/pairing.go | 29 +- .../internal/node/pairing_transport.go | 16 + apps/node-agent/internal/node/sensors.go | 61 +- apps/node-agent/internal/node/server.go | 32 +- .../packaging/50-mission-core-k1.rules | 6 + apps/node-agent/packaging/build.py | 5 +- apps/node-agent/packaging/build_deb.py | 27 +- .../packaging/fetch_driver_bundle.py | 33 +- .../packaging/install-k1-credential | 36 + apps/node-agent/packaging/k1-bundle.json | 265 + apps/node-agent/packaging/k1_bootstrap.py | 26 + apps/node-agent/packaging/k1_prepare.py | 94 + .../packaging/mission-core-k1.service | 38 + apps/node-agent/packaging/postinst | 6 + apps/node-agent/packaging/preinst | 11 + apps/node-agent/packaging/prerm | 8 + .../node-agent/packaging/resolve_k1_bundle.py | 60 + apps/node-agent/ui/package-lock.json | 20 +- apps/node-agent/ui/package.json | 2 + apps/node-agent/ui/rerun-runtime.html | 16 + apps/node-agent/ui/src/NodeSensors.tsx | 6 +- apps/node-agent/ui/src/rerunRuntime.ts | 1 + apps/node-agent/ui/tsconfig.json | 3 + apps/node-agent/ui/vite.config.js | 10 +- docs/04_K1_WIFI_PROVISIONING_PROFILE.md | 19 + ...026-09-06-k1-bridge-architecture-review.md | 269 + .../2026-09-06-k1-bridge-code-snapshot.json | 108 + ...026-09-06-k1-bridge-connection-incident.md | 140 + .../2026-09-06-k1-bridge-ops-index.json | 6720 +++++++++++++++++ ...026-09-06-k1-live-reference-camera-root.md | 129 + .../2026-09-06-k1-station-reply-semantics.md | 95 + ...026-09-06-node-k1-bridge-implementation.md | 164 + ...k1-lab-acceptance-and-node-continuation.md | 62 + ...6-09-07-k1-plugin-boundary-r1-sources.json | 145 + .../2026-09-07-k1-plugin-boundary-r1.md | 147 + docs/audits/2026-09-07-k1-wifi-refusal-ui.md | 57 + packages/plugin-sdk/README.md | 5 + packages/sensor-ui/src/SensorWorkspace.tsx | 21 +- packages/sensor-ui/src/contracts.ts | 11 +- packages/sensor-ui/src/enrollment.ts | 20 + packages/sensor-ui/src/extensions.ts | 29 + packages/sensor-ui/src/pluginSdk.ts | 4 + packages/sensor-ui/src/rerunHost.ts | 12 + packages/sensor-ui/src/sensors.css | 4 + plugins/xgrids-k1/README.md | 17 + .../frontend/src/XgridsK1Connection.tsx | 11 + plugins/xgrids-k1/frontend/src/api.ts | 24 +- .../src/components/K1OperatorError.tsx | 11 +- .../src/components/K1ProvisioningPipeline.tsx | 21 +- .../xgrids-k1/frontend/src/configuration.ts | 3 + .../frontend/src/connectionAttempt.ts | 17 + .../src/networkFailurePresentation.ts | 7 + .../frontend/src/networkProvisioning.ts | 241 + plugins/xgrids-k1/frontend/src/plugin.ts | 2 + .../xgrids-k1/frontend/src/presentation.ts | 6 +- .../src/sensors/DeviceEnrollmentWindow.tsx | 62 + .../frontend/src/sensors/K1Detail.tsx | 25 + .../frontend/src/sensors/K1LiveSettings.tsx | 28 + .../frontend/src/sensors/K1LiveView.tsx | 63 + .../frontend/src/sensors/enrollment.ts | 124 + .../xgrids-k1/frontend/src/sensors/plugin.ts | 7 + .../xgrids-k1/frontend/src/sensors/runtime.ts | 7 + .../frontend/src/useXgridsK1Runtime.ts | 337 +- pyproject.toml | 1 + .../device_plugins/xgrids_k1/ble/scanner.py | 109 +- src/k1link/device_plugins/xgrids_k1/camera.py | 15 +- .../xgrids_k1/connection_attempt.py | 495 ++ src/k1link/device_plugins/xgrids_k1/facade.py | 517 +- .../device_plugins/xgrids_k1/linux_host.py | 214 + .../device_plugins/xgrids_k1/node_bridge.py | 319 + .../device_plugins/xgrids_k1/node_sensor.py | 255 + .../device_plugins/xgrids_k1/wifi_failure.py | 33 + src/k1link/fleet/device_enrollment.py | 173 + src/k1link/fleet/registry.py | 6 +- src/k1link/viewer/node_media.py | 170 + src/k1link/viewer/node_rerun.py | 144 + src/k1link/viewer/rerun_bridge.py | 47 +- src/k1link/web/environment_api.py | 26 +- src/k1link/web/fleet_api.py | 49 + src/k1link/web/runtime_diagnostics.py | 35 + tests/fleet/test_device_enrollment.py | 111 + tests/test_ble_scanner.py | 127 +- tests/test_environment_api.py | 30 + tests/test_k1_connection_read_model.py | 170 + tests/test_k1_wifi_failure.py | 72 + tests/test_node_k1_bridge.py | 204 + tests/test_node_media.py | 102 + tests/test_viewer_diagnostics_api.py | 49 + tests/test_xgrids_acquisition_lifecycle.py | 66 +- tests/test_xgrids_ble_runtime_arbiter.py | 2 +- tests/test_xgrids_camera_gateway.py | 93 + uv.lock | 130 +- 115 files changed, 13700 insertions(+), 988 deletions(-) create mode 100644 apps/control-station/test/networkProvisioning.test.mjs create mode 100644 apps/control-station/test/sensorEnrollment.test.mjs create mode 100644 apps/node-agent/internal/node/device_enrollment.go create mode 100644 apps/node-agent/internal/node/device_enrollment_test.go create mode 100644 apps/node-agent/packaging/50-mission-core-k1.rules create mode 100644 apps/node-agent/packaging/install-k1-credential create mode 100644 apps/node-agent/packaging/k1-bundle.json create mode 100644 apps/node-agent/packaging/k1_bootstrap.py create mode 100644 apps/node-agent/packaging/k1_prepare.py create mode 100644 apps/node-agent/packaging/mission-core-k1.service create mode 100644 apps/node-agent/packaging/resolve_k1_bundle.py create mode 100644 apps/node-agent/ui/rerun-runtime.html create mode 100644 apps/node-agent/ui/src/rerunRuntime.ts create mode 100644 docs/audits/2026-09-06-k1-bridge-architecture-review.md create mode 100644 docs/audits/2026-09-06-k1-bridge-code-snapshot.json create mode 100644 docs/audits/2026-09-06-k1-bridge-connection-incident.md create mode 100644 docs/audits/2026-09-06-k1-bridge-ops-index.json create mode 100644 docs/audits/2026-09-06-k1-live-reference-camera-root.md create mode 100644 docs/audits/2026-09-06-k1-station-reply-semantics.md create mode 100644 docs/audits/2026-09-06-node-k1-bridge-implementation.md create mode 100644 docs/audits/2026-09-07-k1-lab-acceptance-and-node-continuation.md create mode 100644 docs/audits/2026-09-07-k1-plugin-boundary-r1-sources.json create mode 100644 docs/audits/2026-09-07-k1-plugin-boundary-r1.md create mode 100644 docs/audits/2026-09-07-k1-wifi-refusal-ui.md create mode 100644 packages/sensor-ui/src/enrollment.ts create mode 100644 packages/sensor-ui/src/extensions.ts create mode 100644 packages/sensor-ui/src/pluginSdk.ts create mode 100644 packages/sensor-ui/src/rerunHost.ts create mode 100644 plugins/xgrids-k1/frontend/src/connectionAttempt.ts create mode 100644 plugins/xgrids-k1/frontend/src/networkFailurePresentation.ts create mode 100644 plugins/xgrids-k1/frontend/src/networkProvisioning.ts create mode 100644 plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/K1Detail.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/K1LiveSettings.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/K1LiveView.tsx create mode 100644 plugins/xgrids-k1/frontend/src/sensors/enrollment.ts create mode 100644 plugins/xgrids-k1/frontend/src/sensors/plugin.ts create mode 100644 plugins/xgrids-k1/frontend/src/sensors/runtime.ts create mode 100644 src/k1link/device_plugins/xgrids_k1/connection_attempt.py create mode 100644 src/k1link/device_plugins/xgrids_k1/linux_host.py create mode 100644 src/k1link/device_plugins/xgrids_k1/node_bridge.py create mode 100644 src/k1link/device_plugins/xgrids_k1/node_sensor.py create mode 100644 src/k1link/device_plugins/xgrids_k1/wifi_failure.py create mode 100644 src/k1link/fleet/device_enrollment.py create mode 100644 src/k1link/viewer/node_media.py create mode 100644 src/k1link/viewer/node_rerun.py create mode 100644 tests/fleet/test_device_enrollment.py create mode 100644 tests/test_k1_connection_read_model.py create mode 100644 tests/test_k1_wifi_failure.py create mode 100644 tests/test_node_k1_bridge.py create mode 100644 tests/test_node_media.py diff --git a/apps/control-station/src/components/RerunViewport.tsx b/apps/control-station/src/components/RerunViewport.tsx index 4857bd8..5038db7 100644 --- a/apps/control-station/src/components/RerunViewport.tsx +++ b/apps/control-station/src/components/RerunViewport.tsx @@ -762,7 +762,15 @@ export async function probeRecordedPerceptionViewerSource( return { sourceUrl: endpoint.href, byteLength: declaredLength }; } -export function RerunViewport({ +export function RerunViewport(props: RerunViewportProps) { + // Even when source URLs coincide, another profile cannot inherit a mounted + // viewer's playback, blueprint channels, recovery state or scene draft. + const identity = props.profile.kind === "laboratory-result" + ? `${props.profile.kind}:${props.profile.resultId}` : props.profile.kind; + return ; +} + +function RerunViewportInstance({ profile, onStatusChange, onSelectionChange, @@ -772,7 +780,7 @@ export function RerunViewport({ onPerceptionLoadChange, onPointColorLoadChange, }: RerunViewportProps) { - const recordedProfile = profile.kind === "recorded-session" ? profile : null; + const recordedProfile = profile.kind === "recorded-session" || profile.kind === "laboratory-result" ? profile : null; const liveProfile = profile.kind === "live-acquisition" ? profile : null; const sourceUrl = profile.sourceUrl; const recordedArtifact = recordedProfile?.artifact ?? null; diff --git a/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx b/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx index 9ee4126..63d102b 100644 --- a/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx +++ b/apps/control-station/src/components/laboratory/CanonicalResultRerunReplay.tsx @@ -34,7 +34,7 @@ import { } from "../laboratory/CanonicalRecordedLabReplay"; import type { CanonicalLabReplayDescriptor } from "../../core/laboratory/canonicalLabReplay"; import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive"; -import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile"; +import { laboratoryResultRerunProfile } from "../../core/observation/viewerProfile"; import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions"; import type { AIViewerLayer } from "../../core/observatory/aiComposition"; import { @@ -305,7 +305,8 @@ export function CanonicalResultRerunReplay({ accumulationSeconds: showLocalSlam ? sceneDraft.accumulationSeconds : 0, }), [sceneDraft, showLocalSlam, showSourcePoints, spatialMode]); const semanticVisible = showDDRNet || showEoMT; - const profile = launch ? recordedSessionRerunProfile({ + const profile = launch ? laboratoryResultRerunProfile({ + resultId, sourceUrl: launch.replay.sourceUrl, artifact: { sourceUrl: launch.replay.sourceUrl, diff --git a/apps/control-station/src/core/device-plugins/contracts.ts b/apps/control-station/src/core/device-plugins/contracts.ts index 3f98df1..473710f 100644 --- a/apps/control-station/src/core/device-plugins/contracts.ts +++ b/apps/control-station/src/core/device-plugins/contracts.ts @@ -103,6 +103,10 @@ export interface DevicePluginConnectionProps { } export interface DeviceUiPlugin { + sensorUi?: { + contributions: readonly import('../../../../../packages/sensor-ui/src/extensions').SensorUiContribution[]; + Enrollment?: ComponentType; + }; manifest: DevicePluginManifest; RuntimeProvider: ComponentType<{ activeModel: DeviceModelDefinition | null; diff --git a/apps/control-station/src/core/environment/environmentSettings.ts b/apps/control-station/src/core/environment/environmentSettings.ts index 2c2cd3d..773065f 100644 --- a/apps/control-station/src/core/environment/environmentSettings.ts +++ b/apps/control-station/src/core/environment/environmentSettings.ts @@ -113,13 +113,13 @@ const defaultQuickActions: Record< EnvironmentSurfaceId, readonly [string | null, string | null] > = { - home: ["spatial-scene", "local-device"], - fleet: ["contour-health", "local-device"], - observation: ["spatial-scene", "cameras"], + home: ["spatial-scene", "vehicles"], + fleet: ["vehicles", "contour-health"], + observation: ["cameras", "world-map"], missions: ["mission-planner", null], data: ["recordings", "datasets"], system: ["modules", "integrations"], - polygon: ["lab-archive", null], + polygon: ["lab-archive", "local-device"], }; export function defaultEnvironmentSettings(): EnvironmentSettings { diff --git a/apps/control-station/src/core/observation/viewerProfile.ts b/apps/control-station/src/core/observation/viewerProfile.ts index 1abd1e7..91d5210 100644 --- a/apps/control-station/src/core/observation/viewerProfile.ts +++ b/apps/control-station/src/core/observation/viewerProfile.ts @@ -85,6 +85,13 @@ export interface RecordedSessionRerunProfile { lockPerceptionCameraInteraction: boolean; } +/** A LAB owns its result presentation; it does not inherit session-view settings. */ +export interface LaboratoryResultRerunProfile + extends Omit { + kind: "laboratory-result"; + resultId: string; +} + /** * Deprecated comparison-only contract for LAB artifacts that have not yet * been republished as a native Rerun sidecar. It must never be selected by a @@ -101,7 +108,8 @@ export interface LaboratoryRecordedEvidenceViewerProfile { export type RerunViewerProfile = | LiveAcquisitionRerunProfile - | RecordedSessionRerunProfile; + | RecordedSessionRerunProfile + | LaboratoryResultRerunProfile; export type ObservationViewerProfile = | RerunViewerProfile @@ -119,11 +127,18 @@ export const LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE = Object.freeze({ export function liveAcquisitionRerunProfile( input: Omit, ): LiveAcquisitionRerunProfile { - return { kind: "live-acquisition", clock: "stream_time", ...input }; + return { ...input, kind: "live-acquisition", clock: "stream_time" }; } export function recordedSessionRerunProfile( input: Omit, ): RecordedSessionRerunProfile { - return { kind: "recorded-session", clock: "session_time", ...input }; + return { ...input, kind: "recorded-session", clock: "session_time" }; +} + +export function laboratoryResultRerunProfile( + input: Omit, +): LaboratoryResultRerunProfile { + if (!input.resultId.trim()) throw new Error("LAB result identity is required"); + return { ...input, kind: "laboratory-result", clock: "session_time" }; } diff --git a/apps/control-station/src/productModel.ts b/apps/control-station/src/productModel.ts index 5e6872c..5f011cb 100644 --- a/apps/control-station/src/productModel.ts +++ b/apps/control-station/src/productModel.ts @@ -94,7 +94,7 @@ export const roots: RootDefinition[] = [ label: "Парк", title: "Аппараты и устройства", eyebrow: "ПАРК / УСТРОЙСТВА", - description: "Реестр аппаратов, локальное подключение, сенсоры и конфигурации борта.", + description: "Реестр аппаратов, бортовые компьютеры, сенсоры и конфигурации борта.", statement: "Одинаково подключать одиночный стенд, наземную платформу и будущий рой.", accent: "АППАРАТЫ И ПОЛЕЗНАЯ НАГРУЗКА", }, @@ -203,11 +203,11 @@ export const workspaces: WorkspaceDefinition[] = [ }, { id: "local-device", - root: "fleet", - label: "Подключение", - title: "Подключение", - eyebrow: "ПАРК / ТЕКУЩИЙ АДАПТЕР", - description: "Выбор модели, сценарий установленного плагина и запуск доступного потока.", + root: "polygon", + label: "Тестовые устройства", + title: "Тестовые устройства", + eyebrow: "LAB / ТЕСТОВЫЕ УСТРОЙСТВА", + description: "Подключение устройств к компьютеру оператора для испытаний и записи.", icon: "network", kind: "device", groups: [], @@ -258,11 +258,11 @@ export const workspaces: WorkspaceDefinition[] = [ }, { id: "spatial-scene", - root: "observation", + root: "polygon", label: "Пространственная сцена", title: "Пространственная сцена", - eyebrow: "НАБЛЮДЕНИЕ / ЭФИР", - description: "Облако точек, траектория, преобразования и пространственные слои в единой 3D-сцене.", + eyebrow: "LAB / ПРОСТРАНСТВЕННАЯ СЦЕНА", + description: "Облако точек, камеры и траектория тестового устройства в реальном времени.", icon: "globe", kind: "spatial", groups: [ diff --git a/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx b/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx index 6bcbdd7..ba1c849 100644 --- a/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx +++ b/apps/control-station/src/workspaces/fleet/VehicleSensors.tsx @@ -1,13 +1,24 @@ import {useMemo} from 'react'; +import {useDevicePluginHost} from '../../core/device-plugins/DevicePluginHost'; +import {createIsolatedRerunHost} from '../../components/rerun/isolatedRerunHost'; import {SensorWorkspace} from '../../../../../packages/sensor-ui/src/SensorWorkspace'; import type {SensorInventory,SensorTransport} from '../../../../../packages/sensor-ui/src/contracts'; import {fleetRequest} from '../../core/fleet/useFleet'; export function VehicleSensors({vehicleID,enabled,onDetailChange}:{vehicleID:string;enabled:boolean;onDetailChange:(open:boolean)=>void}){ + const {registry}=useDevicePluginHost(); + const sensorContributions=useMemo(()=>registry.plugins.flatMap(plugin=>plugin.sensorUi?.contributions??[]),[registry]); + const enrollmentViews=registry.plugins.flatMap(plugin=>plugin.sensorUi?.Enrollment?[plugin.sensorUi.Enrollment]:[]); + const SensorEnrollmentView=enrollmentViews.length===1?enrollmentViews[0]:undefined; const transport=useMemo(()=>({ + enrollment:{ + state:()=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment`), + submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations`,'POST',value), + operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/enrollment/operations/${encodeURIComponent(id)}`), + }, inventory:async()=>{const fleet=await fleetRequest<{items:{id:string;connectivity:string;sensor_state:SensorInventory}[]}>();const value=fleet.items.find(v=>v.id===vehicleID);if(!value)throw new Error('Аппарат не найден.');return {...value.sensor_state,fresh:value.connectivity==='online'};}, subscribe:(receive,unavailable)=>{const events=new EventSource('/api/v1/fleet/events');events.onmessage=e=>{try{const value=JSON.parse(e.data).items.find((v:{id:string})=>v.id===vehicleID);if(value)receive({...value.sensor_state,fresh:value.connectivity==='online'});else unavailable();}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();}, submit:value=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations`,'POST',value), operation:id=>fleetRequest(`/${encodeURIComponent(vehicleID)}/devices/operations/${encodeURIComponent(id)}`), }),[vehicleID]); - return ; + return ; } diff --git a/apps/control-station/test/applicationArchitecture.test.mjs b/apps/control-station/test/applicationArchitecture.test.mjs index 359ef60..d17addd 100644 --- a/apps/control-station/test/applicationArchitecture.test.mjs +++ b/apps/control-station/test/applicationArchitecture.test.mjs @@ -143,7 +143,7 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor /advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport/, ); assert.equal(sharedReplay.match(/\(hasTgs \? 1 : 0\)/); assert.match(sharedReplay, /hasTgs \? 0\.000001 : 0/); for (const adapter of ["CanonicalVegetationRerunReplay", "PortableResultReplay"]) { diff --git a/apps/control-station/test/devicePluginContracts.test.mjs b/apps/control-station/test/devicePluginContracts.test.mjs index 136c824..5dde64b 100644 --- a/apps/control-station/test/devicePluginContracts.test.mjs +++ b/apps/control-station/test/devicePluginContracts.test.mjs @@ -1676,7 +1676,7 @@ test("physical-command guidance projects backend policy without exposing reason ); }); -test("restart-recovery guidance describes automatic recovery without protocol ceremony", () => { +test("restart-recovery guidance offers an explicit read-only recovery", () => { const deniedFresh = { connection_policy: { schema_version: "missioncore.xgrids-k1-connection-policy/v1", @@ -1712,7 +1712,7 @@ test("restart-recovery guidance describes automatic recovery without protocol ce ); assert.deepEqual(durableGuidance, { reason: "В свежем Bluetooth-поиске не найден K1, связанный с незавершённой записью.", - nextAction: "Дождитесь автоматического восстановления сохранённого подключения K1.", + nextAction: "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.", }); assert.doesNotMatch( `${durableGuidance.reason} ${durableGuidance.nextAction}`, @@ -1725,7 +1725,7 @@ test("restart-recovery guidance describes automatic recovery without protocol ce deniedFresh, "observe-fresh-device-network", ).nextAction, - "Дождитесь автоматического восстановления связи с тем же K1.", + "Нажмите «Переподключиться», чтобы проверить связь с тем же K1.", ); }); @@ -3461,7 +3461,7 @@ test("each terminal explicit provisioning click starts a fresh operation identit assert.match( connectRecovery, - /failedOperation\?\.status === "succeeded"[\s\S]*?!exactAppliedNetworkIntentCompleted\([\s\S]*?&& !hasExactConnectionReady/, + /observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/, ); assert.match( connectRecovery, @@ -4009,7 +4009,7 @@ test("failed network-profile writes close the UI session with a fresh explicit n assert.equal( message, - "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(message, /Bleak|GATT|ATT/i); @@ -4024,8 +4024,8 @@ test("failed network-profile writes close the UI session with a fresh explicit n }); assert.match(attMessage, /ATT 4 INVALID_PDU/); assert.match(attMessage, /Автоматический повтор команды K1 не отправлялся/); - assert.match(attMessage, /Сессия подключения в интерфейсе сброшена/); - assert.match(attMessage, /новый поиск Bluetooth, выберите K1/); + assert.match(attMessage, /Проверьте состояние K1 через «Переподключиться»/); + assert.match(attMessage, /новое подключение через поиск Bluetooth/); const preWriteAttMessage = networkProvisionFailureMessage({ action: "network.provision", @@ -4059,7 +4059,7 @@ test("post-dispatch ambiguity resets the UI session without an automatic retry", assert.equal( message, - "После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(message, /ручн|read-only|защитный барьер/i); }); @@ -4247,7 +4247,7 @@ test("host Wi-Fi failures close the attempt and require a fresh explicit connect assert.equal( operationTimeout, - "Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Сессия подключения в интерфейсе сброшена. Выполните новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз.", + "Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.", ); assert.doesNotMatch(operationTimeout, /получите пароль|пароль отсутствует/i); assert.match(missingNetwork, /K1 принял команду Quick Connect/); diff --git a/apps/control-station/test/devicePluginFrontendBoundary.test.mjs b/apps/control-station/test/devicePluginFrontendBoundary.test.mjs index 6bdb1ee..08d2add 100644 --- a/apps/control-station/test/devicePluginFrontendBoundary.test.mjs +++ b/apps/control-station/test/devicePluginFrontendBoundary.test.mjs @@ -218,8 +218,8 @@ test("K1 connection surface enforces one scan, local selection, and one Apply", ); assert.match(provisioning, /scanSecondsRemaining/); assert.match(provisioning, /setInterval\(updateCountdown, 250\)/); - assert.match(provisioning, /Поиск Bluetooth · \{scanSecondsRemaining \?\? 6\} с/); - assert.match(provisioning, /scanWithResult\(\{[^}]*durationSeconds:\s*6/); + assert.match(provisioning, /Поиск Bluetooth · до \{scanSecondsRemaining \?\? BLE_DISCOVERY_TIMEOUT_SECONDS\} с/); + assert.match(provisioning, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.doesNotMatch(provisioning, /K1 уже доступен|Сетевой адрес K1 доступен/); assert.doesNotMatch( provisioning, @@ -284,7 +284,7 @@ test("K1 click-owned actions are fenced without hidden frontend continuations", provisioning.indexOf("const verifyAppliedNetwork"), ); assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1); - assert.match(search, /durationSeconds:\s*6/); + assert.match(search, /durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.equal((apply.match(/await connect\(/g) ?? []).length, 1); assert.doesNotMatch( apply, @@ -680,7 +680,7 @@ test("automatic K1 live start opens the selected delivered camera despite an old assert.match(layout, /source\.capabilities\.overlay/); }); -test("K1 connect errors reset the UI session without exposing reconciliation ceremony", () => { +test("K1 connect errors preserve the result and expose explicit recovery", () => { const runtime = readFileSync( join(pluginFrontendRoot, "useXgridsK1Runtime.ts"), "utf8", @@ -691,15 +691,17 @@ test("K1 connect errors reset the UI session without exposing reconciliation cer assert.notEqual(connectEnd, -1); const connectFlow = runtime.slice(connectStart, connectEnd); - assert.match(connectFlow, /operationByIdempotencyKey\(/); - assert.match(connectFlow, /failedOperation\?\.status === "succeeded"/); - assert.match(connectFlow, /resetConnectSessionMessage\(/); + assert.match(connectFlow, /observeProvisioningRequest\(/); + assert.match(connectFlow, /operation\?\.status !== "succeeded"/); + assert.match(connectFlow, /networkProvisionFailureMessage\(/); assert.doesNotMatch( connectFlow, /требует ручной проверки|измените параметры только после проверки устройства|Сохранён тот же ключ/, ); - assert.match(runtime, /Сессия подключения в интерфейсе сброшена/); - assert.match(runtime, /новый поиск Bluetooth, выберите K1 и запустите подключение ещё раз/); + const networkFlow = readFileSync(join(pluginFrontendRoot, "networkProvisioning.ts"), "utf8"); + assert.match(networkFlow, /Проверьте состояние K1 через «Переподключиться»/); + assert.match(networkFlow, /новое подключение через поиск Bluetooth/); + assert.equal((networkFlow.match(/await context\.send\(/g) ?? []).length, 1); const verifyStart = runtime.indexOf("const verifyConnection = useCallback("); const verifyEnd = runtime.indexOf("const probeConfiguredEndpoint = useCallback(", verifyStart); const verifyFlow = runtime.slice(verifyStart, verifyEnd); @@ -729,7 +731,7 @@ test("K1 provisioning keeps the operator draft separate from the backend lease", assert.match(selection, /requestExplicitProvisioning\(device\.device_id/); assert.equal((apply.match(/await connect\(/g) ?? []).length, 1); assert.doesNotMatch(apply, /scanWithResult\(|verifyConnection\(|candidateRefresh/); - assert.match(provisioning, /scanWithResult\(\{ durationSeconds: 6 \}\)/); + assert.match(provisioning, /scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/); assert.match( provisioning, /onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/, diff --git a/apps/control-station/test/environmentSettings.test.mjs b/apps/control-station/test/environmentSettings.test.mjs index 144c3e5..8b52b5b 100644 --- a/apps/control-station/test/environmentSettings.test.mjs +++ b/apps/control-station/test/environmentSettings.test.mjs @@ -65,8 +65,8 @@ test("default environment exposes every current product page", () => { "system", "polygon", ]); - assert.equal(defaults.pages.fleet.primaryWorkspaceId, "contour-health"); - assert.equal(defaults.pages.observation.primaryWorkspaceId, "spatial-scene"); + assert.equal(defaults.pages.fleet.primaryWorkspaceId, "vehicles"); + assert.equal(defaults.pages.observation.primaryWorkspaceId, "cameras"); }); test("server document decodes and re-encodes the complete page contract", () => { diff --git a/apps/control-station/test/k1SupervisorPresentation.test.mjs b/apps/control-station/test/k1SupervisorPresentation.test.mjs index 4c17290..eca0bba 100644 --- a/apps/control-station/test/k1SupervisorPresentation.test.mjs +++ b/apps/control-station/test/k1SupervisorPresentation.test.mjs @@ -250,7 +250,7 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I "const chooseAnother", ); - assert.match(search, /scanWithResult\(\{[^}]*durationSeconds:\s*6/); + assert.match(search, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/); assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1); assert.doesNotMatch(search, /\b(?:connect|verifyConnection|submitConnect)\s*\(/); @@ -295,7 +295,7 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I } assert.equal((source.match(/buttonLabel:\s*"Применить"/g) ?? []).length, 3); assert.match(source, /устарел|stale/i); - assert.match(source, /неизвест|outcome-unknown|safe_to_retry/i); + assert.match(source, /network_outcome_unknown|safe_to_retry/i); }); test("exact network-applied REST proof completes frontend Apply before control is ready", () => { @@ -377,7 +377,7 @@ test("exact network-applied REST proof completes frontend Apply before control i ); assert.match( connectFlow, - /failedOperation\?\.status === "succeeded"[\s\S]*?!exactAppliedNetworkIntentCompleted\([\s\S]*?&& !hasExactConnectionReady/, + /observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/, ); assert.doesNotMatch( connectFlow, @@ -1359,7 +1359,7 @@ function actionByLabel(node, label) { return actionByLabel(node.props.children, label); } -function renderProvisioningWithAttempt(props, presentation) { +function renderProvisioningWithAttempt(props, presentation, afterSearch = false) { function AttemptHarness() { const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; @@ -1367,6 +1367,12 @@ function renderProvisioningWithAttempt(props, presentation) { assert.equal(typeof dispatcher?.useState, "function"); const originalUseState = dispatcher.useState; dispatcher.useState = (initialState) => { + if (afterSearch && initialState === emptySearchPresentation) { + const state = props.controller.state; + return [{ sequence: 1, snapshotRuntimeId: state.snapshot_runtime_id, + connectionMode: props.desiredMode, desiredModeRevision: state.desired_connection_mode_revision, + active: false, completedDiscoveryGeneration: state.ble_discovery_generation }, () => undefined]; + } if (initialState === emptyProvisioningAttemptPresentation) { return [presentation, () => undefined]; } @@ -2487,10 +2493,11 @@ test("toolbar reset in one runtime rejects late Scan A and admits exactly one Sc } }); -test("local connection navigation and selector copy use process nouns", () => { +test("operator-local connection belongs to LAB and retains all test modes", () => { const localConnection = workspaces.find((workspace) => workspace.id === "local-device"); - assert.equal(localConnection?.label, "Подключение"); - assert.equal(localConnection?.title, "Подключение"); + assert.equal(localConnection?.root, "polygon"); + assert.equal(localConnection?.label, "Тестовые устройства"); + assert.equal(localConnection?.title, "Тестовые устройства"); assert.equal(model().displayName, "XGRIDS LixelKity K1"); assert.deepEqual( connectionModeOptions.map(({ label }) => label), @@ -7185,7 +7192,7 @@ test("a current-authority Bluetooth scan owns Step 01 and no network step", () = }); assert.match(markup, /01<\/span>/); - assert.match(markup, /Поиск Bluetooth · 6 с/); + assert.match(markup, /Поиск Bluetooth · до 20 с/); assert.equal( (markup.match(/class="nodedc-activity-indicator"/g) ?? []).length, 1, @@ -7478,7 +7485,7 @@ test("applied-network recovery spends the old intent and admits only explicit se search, /const recoveryScanAttempt = appliedRecoveryEscape[\s\S]*?canScanAppliedRecovery[\s\S]*?unresolvedAppliedAttempt/, ); - assert.match(search, /scanResult = await scanWithResult\(\{ durationSeconds: 6 \}\)/); + assert.match(search, /scanResult = await scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/); assert.match( search, /if \(scanResult\.succeeded && recoveryAttemptKey\) \{\s*setEscapedAppliedAttemptKey\(recoveryAttemptKey\)/, @@ -7708,3 +7715,57 @@ test("contour health never promotes selection or replay metrics to live authorit }); assert.equal(replay.aiActive, false); }); + + +test("the recovery panel explains the reviewed K1 network-not-found reply", () => { + const state = terminalConnectionRecoveryState(); + state.connection_attempt.public_error_code = "k1-wifi-network-not-found"; + const markup = renderToStaticMarkup(createElement(K1OperatorError, { + attempt: state.connection_attempt, onRefresh() {}, onClear() {}, + })); + assert.match(markup, /Не удалось подключить K1 к Wi‑Fi<\/strong>/); + assert.match(markup, /указанная сеть не найдена/); + assert.match(markup, /Проверьте название сети и пароль/); + assert.doesNotMatch(markup, /ошибкой Bluetooth|INVALID_PDU/); +}); + +test("a reviewed Wi-Fi rejection offers explicit network setup for the same K1", () => { + const state = terminalConnectionRecoveryState(); + state.connection_attempt.public_error_code = "k1-wifi-network-not-found"; + const controller = { + ...provisioningController(state), error: "private transport error", + errorCorrelation: { + action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0, + connectionAttemptId: state.connection_attempt.attempt_id, + }, + getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state), + }; + const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" }, + provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id, + attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true); + assert.equal(buttonMarkupWithText(markup, "Указать сеть Wi‑Fi заново").length, 1); + assert.doesNotMatch(markup, /Подключить новый K1|private transport error/); +}); + +test("a failed Connect after completed Bluetooth search exposes recovery instead of a locked form", () => { + const state = terminalConnectionRecoveryState(); + state.ble_discovery_generation = 6; + state.connection_attempt.public_error_code = "BleakGATTProtocolError"; + state.connection_attempt.stage = "gatt-write-failed"; + const controller = { + ...provisioningController(state), + error: "private transport exception must stay hidden", + errorCorrelation: { + action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0, + connectionAttemptId: state.connection_attempt.attempt_id, + }, + getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state), + }; + const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" }, + provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id, + attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true); + assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 1); + assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1); + assert.match(markup, /K1 ответил ошибкой Bluetooth/); + assert.doesNotMatch(markup, /Подключение не выполнено|дождитесь, пока система|private transport exception|Пароль передан/); +}); diff --git a/apps/control-station/test/networkProvisioning.test.mjs b/apps/control-station/test/networkProvisioning.test.mjs new file mode 100644 index 0000000..ed48329 --- /dev/null +++ b/apps/control-station/test/networkProvisioning.test.mjs @@ -0,0 +1,133 @@ +import assert from "node:assert/strict"; +import { before, after, test } from "node:test"; +import { createServer } from "vite"; + +let server; +let observeProvisioningRequest; +let networkProvisionFailureMessage; +let ApiError; +before(async () => { + server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } }); + ({ observeProvisioningRequest, networkProvisionFailureMessage } = await server.ssrLoadModule("@xgrids-k1/frontend/networkProvisioning.ts")); + ({ ApiError } = await server.ssrLoadModule("@xgrids-k1/frontend/api.ts")); +}); +after(async () => { await server?.close(); }); + +test("reviewed station errors explain Wi-Fi recovery without claiming a Bluetooth failure", () => { + for (const [code, pattern] of [ + ["k1-wifi-network-not-found", /указанная сеть не найдена/], + ["k1-wifi-credentials-required", /устройство запросило учётные данные сети/], + ]) { + const message = networkProvisionFailureMessage({ status: "failed", error: { code } }); + assert.match(message, pattern); + assert.match(message, /Проверьте название сети и пароль/); + assert.doesNotMatch(message, /INVALID_PDU|ошибкой Bluetooth|Переподключиться/); + } + assert.match(networkProvisionFailureMessage({ status: "failed", error: { + code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU", + } }), /INVALID_PDU/); + assert.match(networkProvisionFailureMessage({ status: "failed", error: { + code: "network-not-found", + } }), /macOS/); +}); + +const request = { device_id: "synthetic-k1", connection_mode: "bridge", idempotency_key: "explicit-one" }; +function snapshot(status, revision = 2) { + return { + snapshot_runtime_id: "runtime-one", + snapshot_runtime_started_at_utc: "2026-09-06T00:00:00Z", + snapshot_revision: revision, + operations: status ? [{ + operation_id: "op-one", action: "network.provision", idempotency_key: request.idempotency_key, + status, error: status === "failed" ? { + code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU", + device_write_attempted: true, device_write_confirmed: false, + safe_to_retry: false, side_effect_status: "unknown", + } : null, + }] : [], + }; +} +function context(overrides = {}) { + let current = snapshot(null, 1); + const calls = []; + return { + calls, + initialState: current, + send: async (input) => { calls.push(["send", input]); throw new ApiError("transport", 502); }, + readState: async () => { calls.push(["read"]); return snapshot("failed"); }, + acceptState: (incoming) => { if (incoming.snapshot_revision >= current.snapshot_revision) current = incoming; }, + currentState: () => current, + assertCurrent() {}, + ...overrides, + }; +} + +test("HTTP 502 retains the exact failed snapshot and ATT facts after one submission", async () => { + const ctx = context(); + const result = await observeProvisioningRequest(request, ctx); + assert.deepEqual(ctx.calls.map(([kind]) => kind), ["send", "read"]); + assert.equal(ctx.calls[0][1], request); + assert.equal(result.operation.operation_id, "op-one"); + assert.equal(result.operation.error.ble_att_error_name, "INVALID_PDU"); + assert.equal(result.state.operations[0], result.operation); + assert.equal(result.transportError.status, 502); +}); + +test("an existing failed or successful intent is never resubmitted", async () => { + for (const status of ["failed", "succeeded"]) { + const ctx = context({ initialState: snapshot(status) }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, status); + assert.equal(ctx.calls.length, 0); + } +}); + +test("lost HTTP response can observe journal success without another command", async () => { + const ctx = context({ readState: async () => snapshot("succeeded") }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, "succeeded"); + assert.equal(ctx.calls.filter(([kind]) => kind === "send").length, 1); +}); + +test("an admitted pending intent is followed only by bounded local state reads", async () => { + let now = 0; + const ctx = context({ + initialState: snapshot("running"), + settlementOptions: { now: () => now, wait: async (ms) => { now += ms; } }, + }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation.status, "failed"); + assert.deepEqual(ctx.calls.map(([kind]) => kind), ["read"]); +}); + +test("newer WebSocket failure outranks a stale running REST result", async () => { + const newer = snapshot("failed", 7); + const ctx = context({ + send: async () => snapshot("running", 5), + currentState: () => newer, + }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.state, newer); + assert.equal(result.operation.status, "failed"); + assert.equal(ctx.calls.length, 0); +}); + +test("superseding an intent after dispatch stops continuation without replay", async () => { + let active = true; + const ctx = context({ + send: async () => { active = false; throw new ApiError("lost", 502); }, + assertCurrent: () => { if (!active) throw new Error("superseded"); }, + }); + await assert.rejects(observeProvisioningRequest(request, ctx), /superseded/); + assert.equal(ctx.calls.length, 0); +}); + +test("an unrelated journal row cannot confirm the current command", async () => { + const unrelated = snapshot("succeeded"); + unrelated.operations[0].idempotency_key = "another-intent"; + const ctx = context({ readState: async () => unrelated }); + const result = await observeProvisioningRequest(request, ctx); + assert.equal(result.operation, null); + assert.equal(result.transportError.status, 502); + assert.equal(ctx.calls.length, 1); +}); diff --git a/apps/control-station/test/observatoryWorkspace.test.mjs b/apps/control-station/test/observatoryWorkspace.test.mjs index ba50dc8..dfc720a 100644 --- a/apps/control-station/test/observatoryWorkspace.test.mjs +++ b/apps/control-station/test/observatoryWorkspace.test.mjs @@ -27,7 +27,7 @@ async function read(relativePath) { test("Observatory is the third independent Polygon workspace", () => { assert.deepEqual( productModel.workspacesForRoot("polygon").map(({ id }) => id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"], ); assert.deepEqual( productModel.workspaceById("observatory"), @@ -138,7 +138,7 @@ test("Observatory mounts the one shared canonical replay only after explicit adm /RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|resolveCanonicalLabReplay|deleteObservationSession|setInterval|setTimeout|useObservationSessions|useAdvancedLaboratoryCatalog|advanced-index|prefetch|preload/, ); assert.equal(sharedReplay.match(/ id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"], ); assert.equal( workspacesForRoot("system").some(({ id }) => id === "polygon-run"), diff --git a/apps/control-station/test/productShellContract.test.mjs b/apps/control-station/test/productShellContract.test.mjs index d3d8266..e49e4e7 100644 --- a/apps/control-station/test/productShellContract.test.mjs +++ b/apps/control-station/test/productShellContract.test.mjs @@ -29,8 +29,10 @@ test("top navigation has no Center and Park owns contour health first", () => { assert.equal(productModel.workspaceById("contour-health")?.root, "fleet"); assert.deepEqual( productModel.workspacesForRoot("polygon").map(({ id }) => id), - ["lab-archive", "simulations", "observatory"], + ["lab-archive", "simulations", "observatory", "local-device", "spatial-scene"], ); + assert.equal(productModel.workspaceById("spatial-scene")?.root, "polygon"); + assert.equal(productModel.workspacesForRoot("observation").some(({ id }) => id === "spatial-scene"), false); assert.equal(productModel.workspaceById("lab-archive")?.kind, "lab-archive"); assert.deepEqual( { diff --git a/apps/control-station/test/sensorEnrollment.test.mjs b/apps/control-station/test/sensorEnrollment.test.mjs new file mode 100644 index 0000000..7c92a4e --- /dev/null +++ b/apps/control-station/test/sensorEnrollment.test.mjs @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict'; +import {before,after,test} from 'node:test'; +import {readFileSync,readdirSync} from 'node:fs'; +import {createServer} from 'vite'; +let server,api,resolveContribution; +before(async()=>{ + server=await createServer({appType:'custom',logLevel:'silent',server:{middlewareMode:true}}); + api=await server.ssrLoadModule('@xgrids-k1/frontend/sensors/enrollment.ts'); + ({sensorContribution:resolveContribution}=await server.ssrLoadModule('../../packages/sensor-ui/src/extensions.ts')); +}); +after(async()=>{await server?.close();}); +const initial={node_id:'node-one',available:true,fresh:true,runtime_id:'runtime-one',snapshot_revision:1,runtime_started_at:'2026-09-06T00:00:00Z',selected_device_id:'synthetic-ble'}; +function fixture(){ + let request,reads=0,posts=0,clock=Date.now(); + const accepted=[]; + const value=(status='running',revision=2)=>({...initial,snapshot_revision:revision,connected:status==='succeeded',connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',attempt_id:request.operation_id,connection_mode:'bridge',status,phase:'network_applied',control_state:status==='succeeded'?'ready':'unknown',safe_next_action:status==='succeeded'?'start-acquisition':'wait-for-current-attempt'}}); + const transport={ + submit:async input=>{posts++;request=input;return {operation_id:request.operation_id,state:'complete',result:value()};}, + operation:async id=>({operation_id:id,state:'complete',result:value()}), + state:async()=>{reads++;return value('succeeded',3);}, + }; + return {transport,value,accepted,parameters:{device_id:'synthetic-ble',ssid:'synthetic-network',password:crypto.randomUUID()}, + observer:{onState:s=>accepted.push(s),now:()=>clock,pause:async()=>{clock+=100000;}}, + counts:()=>({reads,posts}),request:()=>request}; +} + +test('network ACK waits for the exact owned control bootstrap with one POST',async()=>{ + const f=fixture();const result=await api.enroll(f.transport,initial,'connect',f.parameters,f.observer); + assert.equal(result.connected,true);assert.deepEqual(f.counts(),{reads:1,posts:1}); + assert.equal(f.accepted[0].connection_attempt.status,'running');assert.equal(f.accepted.at(-1).connection_attempt.status,'succeeded'); + assert.equal('password' in f.parameters,false); +}); +test('lost POST response reads the same operation ID and never submits again',async()=>{ + const f=fixture();const submit=f.transport.submit; + f.transport.submit=async input=>{await submit(input);throw new Error('response lost');}; + let ids=[];const operation=f.transport.operation; + f.transport.operation=async id=>{ids.push(id);return operation(id);}; + const result=await api.enroll(f.transport,initial,'connect',f.parameters,f.observer); + assert.equal(result.connected,true);assert.equal(f.counts().posts,1);assert.deepEqual(ids,[f.request().operation_id]); + assert.equal('password' in f.parameters,false); +}); +test('late snapshot cannot restore readiness or overwrite newer attempt evidence',()=>{ + const current={...initial,snapshot_revision:8,connected:false}; + assert.equal(api.mergeEnrollmentState(current,{...initial,snapshot_revision:7,connected:true}),current); + const newer={...current,runtime_id:'runtime-two',runtime_started_at:'2026-09-06T00:01:00Z'}; + assert.equal(api.mergeEnrollmentState(newer,{...initial,snapshot_revision:900,connected:true}),newer); + assert.equal(api.mergeEnrollmentState(current,newer).runtime_id,'runtime-two'); + assert.equal(api.mergeEnrollmentState(current,{available:false}).fresh,false); + assert.equal(api.mergeEnrollmentState(current,{...initial,node_id:'another-board'}),current); +}); +test('a response for another operation is never accepted',async()=>{ + const f=fixture();const submit=f.transport.submit; + f.transport.submit=async input=>({...await submit(input),operation_id:'another-operation'}); + await assert.rejects(api.enroll(f.transport,initial,'connect',f.parameters,f.observer),/другое действие/); + assert.equal(f.counts().reads,0);assert.equal(f.counts().posts,1); +}); +test('foreign historical success cannot settle this attempt',async()=>{ + const f=fixture();const submit=f.transport.submit; + f.transport.submit=async input=>({...await submit(input),result:{...initial,connected:true,connection_attempt:{...f.value('succeeded').connection_attempt,attempt_id:'foreign'}}}); + f.transport.state=async()=>({...initial,connected:true,connection_attempt:{...f.value('succeeded').connection_attempt,attempt_id:'foreign'}}); + await assert.rejects(api.enroll(f.transport,initial,'connect',f.parameters,f.observer),/пока не подтверждён/); + assert.equal(f.counts().posts,1); +}); +test('runtime restart interrupts observation without another command',async()=>{ + const f=fixture();f.transport.state=async()=>({...initial,runtime_id:'runtime-two',runtime_started_at:'2026-09-06T00:01:00Z'}); + await assert.rejects(api.enroll(f.transport,initial,'connect',f.parameters,f.observer),/Сеанс K1 изменился/); + assert.equal(f.counts().posts,1);assert.equal(f.accepted.at(-1).runtime_id,'runtime-two'); +}); +test('closing the observer stops reads without cancelling/replaying a device command',async()=>{ + const f=fixture();const controller=new AbortController(); + f.observer.signal=controller.signal;f.observer.pause=async()=>controller.abort(); + await assert.rejects(api.enroll(f.transport,initial,'connect',f.parameters,f.observer),/Наблюдение закрыто/); + assert.deepEqual(f.counts(),{reads:0,posts:1}); +}); +test('station refusal is the same message in onboard and LAB presentations',()=>{ + const state={...initial,connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',attempt_id:'test',status:'failed',phase:'network_outcome_unknown',public_error_code:'k1-wifi-network-not-found'}}; + assert.match(api.enrollmentNotice(state),/Проверьте название сети и пароль/); + assert.doesNotMatch(api.enrollmentNotice(state),/Bluetooth.*ошиб/); +}); +test('sensor host can resolve zero or unrelated integrations and rejects ambiguity',()=>{ + const alpha={kind:'alpha',Detail:()=>null},beta={kind:'beta',Detail:()=>null}; + assert.equal(resolveContribution([],{kind:'alpha'}),undefined); + assert.equal(resolveContribution([alpha,beta],{kind:'beta'}),beta); + assert.equal(resolveContribution([alpha,{...alpha}],{kind:'alpha'}),undefined); + for(const name of readdirSync(new URL('../../../packages/sensor-ui/src/',import.meta.url))){ + if(!/\.tsx?$/.test(name))continue; + assert.doesNotMatch(readFileSync(new URL('../../../packages/sensor-ui/src/'+name,import.meta.url),'utf8'),/xgrids|lixel|\bk1\b|K1Detail/,'vendor dependency in '+name); + } +}); +test('onboard actions use current server policy and never infer authority from readiness',()=>{ + const state={...initial,connected:true,allowed_actions:['verify-control-read-only']}; + assert.equal(api.enrollmentAllowed(state,'connect'),false); + assert.equal(api.enrollmentAllowed(state,'scan'),false); + assert.equal(api.enrollmentAllowed(state,'verify'),true); + assert.equal(api.enrollmentAllowed({...state,fresh:false},'verify'),false); +}); +test('delayed operation result cannot clear an observed board outage',()=>{ + const current={...initial,fresh:false,available:false}; + const result=api.mergeEnrollmentState(current,{...initial,fresh:undefined,snapshot_revision:2}); + assert.equal(result.fresh,false); + assert.equal(api.mergeEnrollmentState(result,{...initial,fresh:true,snapshot_revision:3}).fresh,true); +}); diff --git a/apps/control-station/test/viewerProfile.test.mjs b/apps/control-station/test/viewerProfile.test.mjs index bb3fef8..17a4ea1 100644 --- a/apps/control-station/test/viewerProfile.test.mjs +++ b/apps/control-station/test/viewerProfile.test.mjs @@ -7,6 +7,7 @@ let server; let LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE; let liveAcquisitionRerunProfile; let recordedSessionRerunProfile; +let laboratoryResultRerunProfile; before(async () => { server = await createServer({ @@ -18,6 +19,7 @@ before(async () => { LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE, liveAcquisitionRerunProfile, recordedSessionRerunProfile, + laboratoryResultRerunProfile, } = await server.ssrLoadModule("/src/core/observation/viewerProfile.ts")); }); @@ -86,3 +88,14 @@ test("the old LAB transport is fenced as explicit legacy comparison only", () => }); assert.equal(Object.isFrozen(LABORATORY_RECORDED_EVIDENCE_VIEWER_PROFILE), true); }); + +test("LAB has its own immutable result identity and discriminator", () => { + const input = {resultId:"E38-result-1",sourceUrl:"/shared.rrd",kind:"recorded-session",clock:"stream_time"}; + const lab = laboratoryResultRerunProfile(input); + assert.equal(lab.kind,"laboratory-result"); + assert.equal(lab.clock,"session_time"); + assert.equal(lab.resultId,"E38-result-1"); + assert.equal(recordedSessionRerunProfile({...input}).kind,"recorded-session"); + assert.equal(liveAcquisitionRerunProfile({...input}).kind,"live-acquisition"); + assert.throws(()=>laboratoryResultRerunProfile({...input,resultId:" "})); +}); diff --git a/apps/control-station/tsconfig.app.json b/apps/control-station/tsconfig.app.json index 99cfa36..87ef2c5 100644 --- a/apps/control-station/tsconfig.app.json +++ b/apps/control-station/tsconfig.app.json @@ -3,7 +3,11 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, @@ -14,11 +18,24 @@ "moduleResolution": "Bundler", "baseUrl": ".", "paths": { - "@mission-core/plugin-sdk": ["src/core/device-plugins/frontendSdk.ts"], - "@xgrids-k1/frontend/*": ["../../plugins/xgrids-k1/frontend/src/*"], - "react": ["node_modules/@types/react/index.d.ts"], - "react/jsx-runtime": ["node_modules/@types/react/jsx-runtime.d.ts"], - "@nodedc/ui-react": ["node_modules/@nodedc/ui-react/dist/index.d.ts"] + "@mission-core/plugin-sdk": [ + "src/core/device-plugins/frontendSdk.ts" + ], + "@xgrids-k1/frontend/*": [ + "../../plugins/xgrids-k1/frontend/src/*" + ], + "react": [ + "node_modules/@types/react/index.d.ts" + ], + "react/jsx-runtime": [ + "node_modules/@types/react/jsx-runtime.d.ts" + ], + "@nodedc/ui-react": [ + "node_modules/@nodedc/ui-react/dist/index.d.ts" + ], + "@mission-core/sensor-sdk": [ + "../../packages/sensor-ui/src/pluginSdk.ts" + ] }, "resolveJsonModule": true, "isolatedModules": true, @@ -28,5 +45,8 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src", "../../plugins/xgrids-k1/frontend/src"] + "include": [ + "src", + "../../plugins/xgrids-k1/frontend/src" + ] } diff --git a/apps/control-station/vite.config.ts b/apps/control-station/vite.config.ts index 329ccbc..47325f6 100644 --- a/apps/control-station/vite.config.ts +++ b/apps/control-station/vite.config.ts @@ -74,6 +74,7 @@ export default defineConfig(({ mode }) => { plugins: [react(), wasm(), cesiumRuntimeAssets()], resolve: { alias: { + "@mission-core/sensor-sdk": fileURLToPath(new URL("../../packages/sensor-ui/src/pluginSdk.ts", import.meta.url)), "@mission-core/plugin-sdk": fileURLToPath( new URL("./src/core/device-plugins/frontendSdk.ts", import.meta.url), ), diff --git a/apps/node-agent/cmd/node-agent/main.go b/apps/node-agent/cmd/node-agent/main.go index 68fa9f2..d9ecfd1 100644 --- a/apps/node-agent/cmd/node-agent/main.go +++ b/apps/node-agent/cmd/node-agent/main.go @@ -80,6 +80,12 @@ func run() error { return err } pairing.Sensors = app.Sensors + app.DeviceEnrollment, err = node.OpenDeviceEnrollment(*dir, nodeID) + if err != nil { + return err + } + pairing.DeviceEnrollment = app.DeviceEnrollment + app.Sensors.NetworkDevices = app.DeviceEnrollment app.Access = &node.AccessStore{Path: filepath.Join(*dir, "ssh-keys.json"), Users: func() []string { return node.LocalAdmins("/") }} if err := os.MkdirAll(filepath.Dir(*socket), 0700); err != nil { return err diff --git a/apps/node-agent/internal/node/device_enrollment.go b/apps/node-agent/internal/node/device_enrollment.go new file mode 100644 index 0000000..d95ca08 --- /dev/null +++ b/apps/node-agent/internal/node/device_enrollment.go @@ -0,0 +1,323 @@ +package node + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "time" +) + +type EnrollmentCommand struct { + ID string `json:"operation_id"` + NodeID string `json:"node_id"` + RuntimeID string `json:"runtime_id"` + Action string `json:"action"` + Deadline string `json:"deadline_at"` + Parameters map[string]any `json:"parameters"` +} + +// This is the complete journal schema. No command parameters or credentials. +type EnrollmentOperation struct { + ID string `json:"operation_id"` + NodeID string `json:"node_id"` + RuntimeID string `json:"runtime_id"` + Action string `json:"action"` + State string `json:"state"` + Error string `json:"error,omitempty"` + Result map[string]any `json:"result,omitempty"` + Remote bool `json:"remote,omitempty"` + Updated int64 `json:"updated_at"` +} + +type DeviceEnrollment struct { + mu sync.Mutex + root string + nodeID string + client *http.Client + operations map[string]*EnrollmentOperation +} + +func OpenDeviceEnrollment(root, nodeID string) (*DeviceEnrollment, error) { + dir := filepath.Join(root, "device-enrollment") + if e := os.MkdirAll(dir, 0700); e != nil { + return nil, e + } + d := &DeviceEnrollment{root: dir, nodeID: nodeID, operations: map[string]*EnrollmentOperation{}} + d.client = &http.Client{Timeout: 185 * time.Second, Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", "/run/mission-core-k1/driver.sock") + }}} + files, _ := filepath.Glob(filepath.Join(dir, "op_*.json")) + for _, path := range files { + data, e := os.ReadFile(path) + if e != nil { + return nil, e + } + var op EnrollmentOperation + if json.Unmarshal(data, &op) != nil || !operationID.MatchString(op.ID) { + return nil, errors.New("invalid enrollment journal") + } + if op.State == "running" { + op.State = "unknown" + op.Error = "БК перезапущен. Обновите состояние K1 перед новой командой." + } + d.operations[op.ID] = &op + } + return d, nil +} + +func (d *DeviceEnrollment) call(ctx context.Context, path string, command any) (map[string]any, error) { + var body io.Reader + method := "GET" + if command != nil { + data, e := json.Marshal(command) + if e != nil { + return nil, e + } + body = bytes.NewReader(data) + method = "POST" + } + request, e := http.NewRequestWithContext(ctx, method, "http://k1"+path, body) + if e != nil { + return nil, e + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("X-Node-Id", d.nodeID) + response, e := d.client.Do(request) + if e != nil { + return nil, errors.New("Служба K1 на БК недоступна.") + } + defer response.Body.Close() + var value map[string]any + if json.NewDecoder(io.LimitReader(response.Body, 65536)).Decode(&value) != nil || response.StatusCode != 200 { + return nil, errors.New("Действие K1 не подтверждено. Обновите состояние устройства.") + } + return value, nil +} + +func (d *DeviceEnrollment) Status() map[string]any { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + value, e := d.call(ctx, "/status", nil) + if e != nil { + value = map[string]any{"available": false} + } + value["node_id"] = d.nodeID + value["name"], _ = os.Hostname() + value["fresh"] = true + return value +} + +func (c EnrollmentCommand) valid(nodeID string) bool { + deadline, e := time.Parse(time.RFC3339Nano, c.Deadline) + if e != nil || !deadline.After(time.Now()) || time.Until(deadline) > 180*time.Second || c.NodeID != nodeID || !operationID.MatchString(c.ID) || c.RuntimeID == "" || len(c.RuntimeID) > 128 { + return false + } + keys := map[string]bool{} + switch c.Action { + case "scan", "networks": + case "connect", "verify": + keys = map[string]bool{"device_id": true, "discovery_generation": true, "mode_revision": true} + id, ok := c.Parameters["device_id"].(string) + if !ok || len(id) < 1 || len(id) > 128 { + return false + } + for _, key := range []string{"discovery_generation", "mode_revision"} { + v, ok := c.Parameters[key].(float64) + if !ok || v < 0 || v != float64(int64(v)) { + return false + } + } + if c.Action == "connect" { + keys["ssid"] = true + keys["password"] = true + for key, limit := range map[string]int{"ssid": 32, "password": 64} { + v, ok := c.Parameters[key].(string) + if !ok || len(v) < 1 || len(v) > limit { + return false + } + } + } + default: + return false + } + if len(keys) != len(c.Parameters) { + return false + } + for key := range c.Parameters { + if !keys[key] { + return false + } + } + return true +} + +func copyEnrollment(value *EnrollmentOperation) *EnrollmentOperation { + if value == nil { + return nil + } + data, _ := json.Marshal(value) + var out EnrollmentOperation + _ = json.Unmarshal(data, &out) + return &out +} + +func (d *DeviceEnrollment) Submit(c EnrollmentCommand, remote bool) (*EnrollmentOperation, error) { + if !c.valid(d.nodeID) { + return nil, errors.New("Проверьте БК, выбранное устройство и параметры сети.") + } + d.mu.Lock() + if old := d.operations[c.ID]; old != nil { + out := copyEnrollment(old) + d.mu.Unlock() + return out, nil + } + if len(d.operations) >= 2000 { + for id, value := range d.operations { + if value.State != "running" && time.Now().Unix()-value.Updated > 86400 { + if os.Remove(filepath.Join(d.root, id+".json")) == nil { + delete(d.operations, id) + } + } + } + if len(d.operations) >= 2000 { + d.mu.Unlock() + return nil, errors.New("Журнал подключений заполнен. Повторите позже.") + } + } + for _, v := range d.operations { + if v.State == "running" { + d.mu.Unlock() + return nil, errors.New("Дождитесь текущего действия K1.") + } + } + d.mu.Unlock() + if d.Status()["runtime_id"] != c.RuntimeID { + return nil, errors.New("Сеанс K1 изменился. Обновите устройства.") + } + d.mu.Lock() + // Status can yield; repeat admission under the lock before the durable write. + if old := d.operations[c.ID]; old != nil { + out := copyEnrollment(old) + d.mu.Unlock() + return out, nil + } + for _, v := range d.operations { + if v.State == "running" { + d.mu.Unlock() + return nil, errors.New("Дождитесь текущего действия K1.") + } + } + op := &EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: remote, Updated: time.Now().Unix()} + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + d.mu.Unlock() + return nil, e + } + d.operations[c.ID] = op + out := copyEnrollment(op) + d.mu.Unlock() + go d.execute(c) + return out, nil +} + +func (d *DeviceEnrollment) execute(c EnrollmentCommand) { + deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + result, e := d.call(ctx, "/operation", c) + // Dropping references is best-effort lifetime reduction, not a claim that + // Go can zero every immutable JSON/string copy. No secret is journalled. + delete(c.Parameters, "password") + d.mu.Lock() + defer d.mu.Unlock() + op := d.operations[c.ID] + op.Updated = time.Now().Unix() + op.State = "complete" + op.Result = result + if e != nil { + op.State = "unknown" + op.Error = "Действие K1 не подтверждено. Обновите состояние; повторная команда автоматически не отправляется." + } + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + op.State = "unknown" + op.Error = "Результат не сохранён. Обновите состояние K1." + } +} + +func (d *DeviceEnrollment) Get(id string) *EnrollmentOperation { + d.mu.Lock() + defer d.mu.Unlock() + return copyEnrollment(d.operations[id]) +} +func (d *DeviceEnrollment) RemoteResults() []*EnrollmentOperation { + d.mu.Lock() + defer d.mu.Unlock() + out := []*EnrollmentOperation{} + for _, op := range d.operations { + if op.Remote { + out = append(out, copyEnrollment(op)) + if len(out) == 8 { + break + } + } + } + return out +} +func (d *DeviceEnrollment) Acknowledge(ids []string) { + d.mu.Lock() + defer d.mu.Unlock() + for _, id := range ids { + if op := d.operations[id]; op != nil && op.State != "running" { + op.Remote = false + _ = savePrivateJSON(filepath.Join(d.root, id+".json"), op) + } + } +} + +func (d *DeviceEnrollment) Routes(mux *http.ServeMux, server *Server) { + mux.HandleFunc("GET /api/devices/enrollment", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + v := d.Status() + _, name := server.Store.Public() + v["name"] = name + reply(w, 200, v) + }) + mux.HandleFunc("POST /api/devices/enrollment/operations", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + r.Body = http.MaxBytesReader(w, r.Body, 16384) + var c EnrollmentCommand + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + if r.Header.Get("Content-Type") != "application/json" || decoder.Decode(&c) != nil { + reply(w, 400, map[string]string{"error": "Некорректный запрос подключения"}) + return + } + op, e := d.Submit(c, false) + if e != nil { + reply(w, 409, map[string]string{"error": e.Error()}) + return + } + reply(w, 202, op) + }) + mux.HandleFunc("GET /api/devices/enrollment/operations/{id}", func(w http.ResponseWriter, r *http.Request) { + if !server.authorized(w, r) { + return + } + op := d.Get(r.PathValue("id")) + if op == nil { + reply(w, 200, map[string]string{"state": "unknown", "error": "Результат недоступен. Обновите состояние K1."}) + return + } + reply(w, 200, op) + }) +} diff --git a/apps/node-agent/internal/node/device_enrollment_test.go b/apps/node-agent/internal/node/device_enrollment_test.go new file mode 100644 index 0000000..e9bd339 --- /dev/null +++ b/apps/node-agent/internal/node/device_enrollment_test.go @@ -0,0 +1,111 @@ +package node + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "time" +) + +type enrollmentHTTP func(*http.Request) (*http.Response, error) + +func (f enrollmentHTTP) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func enrollmentRequest() EnrollmentCommand { + return EnrollmentCommand{ID: "op_" + strings.Repeat("a", 32), NodeID: "node-test", RuntimeID: "runtime-test", Action: "connect", Deadline: time.Now().Add(time.Minute).UTC().Format(time.RFC3339Nano), Parameters: map[string]any{"device_id": "test-ble", "mode_revision": float64(0), "discovery_generation": float64(1), "ssid": "test-network", "password": token()}} +} + +func TestEnrollmentJournalsNoCredentialAndDispatchesOnce(t *testing.T) { + root := t.TempDir() + d, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + var posts atomic.Int32 + d.client = &http.Client{Transport: enrollmentHTTP(func(r *http.Request) (*http.Response, error) { + body := `{"available":true,"runtime_id":"runtime-test"}` + if r.Method == "POST" { + posts.Add(1) + body = `{"available":true,"connected":true}` + } + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil + })} + c := enrollmentRequest() + secret := c.Parameters["password"].(string) + if _, e = d.Submit(c, true); e != nil { + t.Fatal(e) + } + deadline := time.Now().Add(time.Second) + for d.Get(c.ID).State == "running" && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if d.Get(c.ID).State != "complete" { + t.Fatal("operation did not complete") + } + // A repeated delivery owns no new physical intent, even with a replaced secret. + c.Parameters["password"] = token() + if _, e = d.Submit(c, true); e != nil { + t.Fatal(e) + } + if posts.Load() != 1 { + t.Fatal("duplicate dispatch") + } + journal, e := os.ReadFile(filepath.Join(root, "device-enrollment", c.ID+".json")) + if e != nil { + t.Fatal(e) + } + results, _ := json.Marshal(d.RemoteResults()) + if bytes.Contains(journal, []byte(secret)) || bytes.Contains(results, []byte(secret)) || bytes.Contains(journal, []byte("parameters")) { + t.Fatal("credential/parameters entered journal or results") + } +} + +func TestEnrollmentRestartDoesNotReplayRunningIntent(t *testing.T) { + root := t.TempDir() + d, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + c := enrollmentRequest() + op := EnrollmentOperation{ID: c.ID, NodeID: c.NodeID, RuntimeID: c.RuntimeID, Action: c.Action, State: "running", Remote: true} + if e := savePrivateJSON(filepath.Join(d.root, c.ID+".json"), op); e != nil { + t.Fatal(e) + } + restarted, e := OpenDeviceEnrollment(root, "node-test") + if e != nil { + t.Fatal(e) + } + if restarted.Get(c.ID).State != "unknown" { + t.Fatal("restart did not fence unknown result") + } + restarted.client = &http.Client{Transport: enrollmentHTTP(func(*http.Request) (*http.Response, error) { t.Fatal("restart contacted hardware"); return nil, nil })} + result, e := restarted.Submit(c, true) + if e != nil || result.State != "unknown" { + t.Fatal("repeated intent did not retain unknown outcome") + } +} + +func TestEnrollmentRejectsAnotherNodeExpiredAndHostMutation(t *testing.T) { + c := enrollmentRequest() + if !c.valid("node-test") { + t.Fatal("valid intent rejected") + } + if c.valid("another-node") { + t.Fatal("wrong node admitted") + } + c.Parameters["allow_host_wifi_switch"] = true + if c.valid("node-test") { + t.Fatal("host association admitted") + } + delete(c.Parameters, "allow_host_wifi_switch") + c.Deadline = time.Now().Add(-time.Second).Format(time.RFC3339Nano) + if c.valid("node-test") { + t.Fatal("expired operation admitted") + } +} diff --git a/apps/node-agent/internal/node/pairing.go b/apps/node-agent/internal/node/pairing.go index d188b74..e490a7f 100644 --- a/apps/node-agent/internal/node/pairing.go +++ b/apps/node-agent/internal/node/pairing.go @@ -39,20 +39,21 @@ type PairState struct { Revocations []CoreBinding `json:"revocations,omitempty"` } type Pairing struct { - Sensors *Sensors - mu sync.Mutex - path string - store *Store - state PairState - now func() time.Time - inventory func() Inventory - version string - lastSeen int64 - connection string - listenError string - failureWindow int64 - failures int - clients map[string]*http.Client + Sensors *Sensors + DeviceEnrollment *DeviceEnrollment + mu sync.Mutex + path string + store *Store + state PairState + now func() time.Time + inventory func() Inventory + version string + lastSeen int64 + connection string + listenError string + failureWindow int64 + failures int + clients map[string]*http.Client } func OpenPairing(store *Store, dir, version string, inventory func() Inventory) (*Pairing, error) { diff --git a/apps/node-agent/internal/node/pairing_transport.go b/apps/node-agent/internal/node/pairing_transport.go index e9348ab..f1a09bf 100644 --- a/apps/node-agent/internal/node/pairing_transport.go +++ b/apps/node-agent/internal/node/pairing_transport.go @@ -282,12 +282,28 @@ func (p *Pairing) channel(ctx context.Context) { payload["sensor_state"] = inv payload["sensor_results"] = p.Sensors.RemoteResults() } + if p.DeviceEnrollment != nil { + payload["device_enrollment"] = p.DeviceEnrollment.Status() + payload["enrollment_results"] = p.DeviceEnrollment.RemoteResults() + } result, status, e := p.send(ctx, *binding, "/v1/node/heartbeat", payload) p.mu.Lock() if p.state.Phase == "paired" && p.state.Binding != nil && p.state.Binding.BindingID == binding.BindingID { if e == nil && status == 200 { p.connection = "online" p.lastSeen = p.now().Unix() + if p.DeviceEnrollment != nil { + var ack []string + if json.Unmarshal(result["enrollment_acknowledgements"], &ack) == nil { + p.DeviceEnrollment.Acknowledge(ack) + } + var commands []EnrollmentCommand + if json.Unmarshal(result["enrollment_commands"], &commands) == nil { + for _, c := range commands { + _, _ = p.DeviceEnrollment.Submit(c, true) + } + } + } if p.Sensors != nil { var ack []string if json.Unmarshal(result["sensor_acknowledgements"], &ack) == nil { diff --git a/apps/node-agent/internal/node/sensors.go b/apps/node-agent/internal/node/sensors.go index 1cf1100..0e99d1c 100644 --- a/apps/node-agent/internal/node/sensors.go +++ b/apps/node-agent/internal/node/sensors.go @@ -45,19 +45,20 @@ type SensorOperation struct { Updated int64 `json:"updated_at"` } type Sensors struct { - events sensorEvents - mu sync.Mutex - prepareMu sync.Mutex - root string - nodeID string - instance string - client *http.Client - operations map[string]*SensorOperation - names map[string]string - initialized map[string]bool + events sensorEvents + mu sync.Mutex + prepareMu sync.Mutex + root string + nodeID string + instance string + client *http.Client + NetworkDevices *DeviceEnrollment + operations map[string]*SensorOperation + names map[string]string + initialized map[string]bool } -var sensorID = regexp.MustCompile(`^rsd455_[0-9a-f]{32}$`) +var sensorID = regexp.MustCompile(`^(rsd455|k1)_[0-9a-f]{32}$`) var operationID = regexp.MustCompile(`^op_[0-9a-f]{32}$`) func OpenSensors(root, nodeID string) (*Sensors, error) { @@ -168,6 +169,32 @@ func (s *Sensors) driver(path string, body any) (map[string]any, error) { func (s *Sensors) Inventory() map[string]any { items := []any{} seen := map[string]bool{} + if s.NetworkDevices != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + result, e := s.NetworkDevices.call(ctx, "/inventory", nil) + cancel() + if e == nil { + if found, ok := result["items"].([]any); ok { + for _, raw := range found { + item, ok := raw.(map[string]any) + if !ok { + continue + } + id, _ := item["id"].(string) + if !strings.HasPrefix(id, "k1_") || !sensorID.MatchString(id) { + continue + } + s.mu.Lock() + if name := s.names[id]; name != "" { + item["name"] = name + } + s.mu.Unlock() + seen[id] = true + items = append(items, item) + } + } + } + } if result, e := s.driver("/inventory", nil); e == nil { if found, ok := result["items"].([]any); ok { for _, v := range found { @@ -259,6 +286,9 @@ func sensorViewAction(action string) bool { } func (s *Sensors) Submit(c SensorCommand, remote bool) (*SensorOperation, error) { + if strings.HasPrefix(c.Session.DeviceID, "k1_") && (c.Action == "prepare" || c.Action == "replay") { + return nil, errors.New("Эта операция не поддерживается K1.") + } if c.APIVersion != SensorSchema || c.Kind != "OperationRequest" || !operationID.MatchString(c.ID) || c.Idempotency != c.ID || !sensorID.MatchString(c.Session.DeviceID) || len(c.Session.SessionID) > 192 { return nil, errors.New("Некорректная команда устройства.") } @@ -344,7 +374,14 @@ func (s *Sensors) execute(c SensorCommand) { } } else { var v map[string]any - v, err = s.driver("/operation", c) + if strings.HasPrefix(c.Session.DeviceID, "k1_") && s.NetworkDevices != nil { + deadline, _ := time.Parse(time.RFC3339Nano, c.Deadline) + ctx, cancel := context.WithDeadline(context.Background(), deadline) + v, err = s.NetworkDevices.call(ctx, "/sensor-operation", c) + cancel() + } else { + v, err = s.driver("/operation", c) + } uncertain = err != nil || v["state"] == "unknown" if err == nil { if v["state"] == "complete" { diff --git a/apps/node-agent/internal/node/server.go b/apps/node-agent/internal/node/server.go index c56e04f..819ddca 100644 --- a/apps/node-agent/internal/node/server.go +++ b/apps/node-agent/internal/node/server.go @@ -13,20 +13,21 @@ import ( ) type Server struct { - Store *Store - Pairing *Pairing - Sensors *Sensors - Assets fs.FS - Origin string - Version string - Inventory func() Inventory - Access *AccessStore - Tailscale func() TailscaleStatus - Environment func() EnvironmentStatus - mu sync.Mutex - logins map[string]time.Time - sessions map[string]time.Time - Now func() time.Time + Store *Store + Pairing *Pairing + Sensors *Sensors + DeviceEnrollment *DeviceEnrollment + Assets fs.FS + Origin string + Version string + Inventory func() Inventory + Access *AccessStore + Tailscale func() TailscaleStatus + Environment func() EnvironmentStatus + mu sync.Mutex + logins map[string]time.Time + sessions map[string]time.Time + Now func() time.Time } func token() string { @@ -82,6 +83,9 @@ func (s *Server) Handler() http.Handler { if s.Sensors != nil { s.Sensors.Routes(mux, s) } + if s.DeviceEnrollment != nil { + s.DeviceEnrollment.Routes(mux, s) + } if s.Pairing != nil { s.Pairing.localRoutes(mux, s) } diff --git a/apps/node-agent/packaging/50-mission-core-k1.rules b/apps/node-agent/packaging/50-mission-core-k1.rules new file mode 100644 index 0000000..a16bc55 --- /dev/null +++ b/apps/node-agent/packaging/50-mission-core-k1.rules @@ -0,0 +1,6 @@ +// Only WLAN discovery. No host association/profile modification authority. +polkit.addRule(function(action, subject) { + if (subject.user === "mission-core-k1" && action.id === "org.freedesktop.NetworkManager.wifi.scan") { + return polkit.Result.YES; + } +}); diff --git a/apps/node-agent/packaging/build.py b/apps/node-agent/packaging/build.py index 71984fc..f1b2ffd 100644 --- a/apps/node-agent/packaging/build.py +++ b/apps/node-agent/packaging/build.py @@ -11,7 +11,7 @@ import sys from build_deb import build, VERSION, BRAND_SHA256 ROOT = Path(__file__).resolve().parents[1] -DG_COMMIT = "999864e5b0a81555823cfa1ea6e8cf8a417c37f1" +DG_COMMIT = "1bdfc6c24072d38cc1068086ea271c444f2524ad" def guideline_sources(): @@ -32,6 +32,9 @@ def provenance(): "base_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(), "design_guideline_commit": DG_COMMIT, "design_guideline_files": guideline_sources(), + "shared_sensor_ui_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "packages/sensor-ui/src").rglob("*")) if p.is_file()}, + "k1_frontend_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "plugins/xgrids-k1/frontend/src").rglob("*")) if p.is_file()}, + "k1_runtime_files": {str(p.relative_to(ROOT.parents[1])): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted((ROOT.parents[1] / "src/k1link").rglob("*")) if p.is_file() and p.suffix in (".py", ".json")}, "toolchain": json.loads((ROOT / "toolchain.json").read_text()), "files": files} diff --git a/apps/node-agent/packaging/build_deb.py b/apps/node-agent/packaging/build_deb.py index 1d0138f..4ecb8df 100644 --- a/apps/node-agent/packaging/build_deb.py +++ b/apps/node-agent/packaging/build_deb.py @@ -13,7 +13,7 @@ import tarfile ROOT = Path(__file__).resolve().parents[1] -VERSION = "0.6.11" +VERSION = "0.7.1" BRAND_SHA256 = "8bfee8ca9f98e0db48d98aae3af4b32493b8593e18b064a0239d513d824182af" @@ -65,7 +65,7 @@ Architecture: amd64 Maintainer: NODE.DC local build Section: admin Priority: optional -Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme +Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, bluez, network-manager, iproute2, ffmpeg Description: Mission Core onboard computer configuration Local graphical setup, host inventory, SSH access and persistent node identity. """.encode() @@ -107,6 +107,29 @@ Description: Mission Core onboard computer configuration files.append(("usr/share/mission-core-node/realsense/" + item["name"], data, 0o644)) for path in (ROOT / "sensors").glob("*.py"): files.append(("usr/lib/mission-core-node/sensors/" + path.name, path.read_bytes(), 0o644)) + for name in ("k1_prepare.py", "k1_bootstrap.py"): + files.append(("usr/lib/mission-core-node/" + name, (p / name).read_bytes(), 0o644)) + files.append(("usr/lib/mission-core-node/install-k1-credential", (p / "install-k1-credential").read_bytes(), 0o755)) + files.append(("usr/lib/systemd/system/mission-core-k1.service", (p / "mission-core-k1.service").read_bytes(), 0o644)) + files.append(("usr/share/polkit-1/rules.d/50-mission-core-k1.rules", (p / "50-mission-core-k1.rules").read_bytes(), 0o644)) + k1_bundle = json.loads((p / "k1-bundle.json").read_text()) + files.append(("usr/share/mission-core-node/k1/bundle.json", (p / "k1-bundle.json").read_bytes(), 0o644)) + for item in k1_bundle["wheels"]: + data = (ROOT / "build/k1-wheels" / item["name"]).read_bytes() + if hashlib.sha256(data).hexdigest() != item["sha256"]: + raise ValueError("K1 bundle hash mismatch") + files.append(("usr/share/mission-core-node/k1/" + item["name"], data, 0o644)) + repository = ROOT.parents[1] + # Reuse the admitted plugin runtime and the transport-neutral renderer. + # No separate Core web service is started on the board. + for path in (repository / "src/k1link").rglob("*"): + if path.is_file() and path.suffix in (".py", ".json"): + files.append(("usr/lib/mission-core-node/k1/src/k1link/" + str(path.relative_to(repository / "src/k1link")), path.read_bytes(), 0o644)) + for relative in ("plugins/xgrids-k1/profile_loader.py", "plugins/xgrids-k1/plugin.manifest.json", + "config/observatory-equipment-models.json", + "config/observatory-recorded-capture-profiles.json", + "plugins/xgrids-k1/profiles/fw-3.0.2/local-network.v2.json"): + files.append(("usr/lib/mission-core-node/k1/" + relative, (repository / relative).read_bytes(), 0o644)) sdk = ROOT.parents[1] / "packages/plugin-sdk/python/missioncore_plugin_sdk" for path in sdk.rglob("*.py"): files.append(("usr/lib/mission-core-node/sdk/missioncore_plugin_sdk/" + str(path.relative_to(sdk)), path.read_bytes(), 0o644)) diff --git a/apps/node-agent/packaging/fetch_driver_bundle.py b/apps/node-agent/packaging/fetch_driver_bundle.py index a55fa23..be66b17 100644 --- a/apps/node-agent/packaging/fetch_driver_bundle.py +++ b/apps/node-agent/packaging/fetch_driver_bundle.py @@ -1,13 +1,18 @@ """Engineering build input, never run on an operator board. Exact PyPI hashes only.""" import hashlib +import argparse import json +import time from pathlib import Path -from urllib.request import urlopen +from urllib.request import Request, urlopen root = Path(__file__).resolve().parents[1] -manifest = json.loads((root / "packaging/realsense-bundle.json").read_text()) -output = root / "build/realsense-wheels" +parser = argparse.ArgumentParser() +parser.add_argument("--model", choices=("realsense", "k1"), default="realsense") +model = parser.parse_args().model +manifest = json.loads((root / f"packaging/{model}-bundle.json").read_text()) +output = root / f"build/{model}-wheels" output.mkdir(parents=True, exist_ok=True) for item in manifest["wheels"]: target = output / item["name"] @@ -23,8 +28,24 @@ for item in manifest["wheels"]: ) if not source["url"].startswith("https://files.pythonhosted.org/"): raise ValueError("Unexpected package origin") - with urlopen(source["url"], timeout=120) as response: - data = response.read(item["bytes"] + 1) + partial = target.with_suffix(target.suffix + ".partial") + for attempt in range(4): + offset = partial.stat().st_size if partial.exists() else 0 + request = Request(source["url"], headers={"Range": f"bytes={offset}-"} if offset else {}) + try: + with urlopen(request, timeout=60) as response: + if offset and response.status != 206: + raise RuntimeError("Package server did not honor resume range") + with partial.open("ab" if offset else "wb") as stream: + while chunk := response.read(1024 * 1024): + stream.write(chunk) + break + except (OSError, TimeoutError): + if attempt == 3: + raise + time.sleep(2) + data = partial.read_bytes() if len(data) != item["bytes"] or hashlib.sha256(data).hexdigest() != item["sha256"]: raise ValueError("Driver checksum mismatch") - target.write_bytes(data) + partial.replace(target) + print(json.dumps({"model": model, "wheel": item["name"], "bytes": len(data)}), flush=True) diff --git a/apps/node-agent/packaging/install-k1-credential b/apps/node-agent/packaging/install-k1-credential new file mode 100644 index 0000000..137501b --- /dev/null +++ b/apps/node-agent/packaging/install-k1-credential @@ -0,0 +1,36 @@ +#!/usr/bin/python3 -I +"""Administrator-only import of the exact application key from protected stdin.""" +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +if os.geteuid() != 0 or len(sys.argv) != 1: + raise SystemExit("Root stdin import required") +secret = bytearray(sys.stdin.buffer.read(1025).strip()) +try: + if len(secret) != 36 or any(v < 33 or v > 126 for v in secret): + raise SystemExit("Credential does not match the reviewed K1 profile") + root = Path("/etc/credstore.encrypted") + root.mkdir(mode=0o700, exist_ok=True) + if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022: + raise SystemExit("Unsafe credential store") + path = root / "k1-application" + if path.is_symlink() or path.exists(): + raise SystemExit("K1 credential already installed; explicit rotation required") + with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory: + staged = Path(directory) / "encrypted" + completed = subprocess.run( + ["/usr/bin/systemd-creds", "encrypt", "--name=k1-application", "--with-key=host", "-", str(staged)], + input=secret, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, + ) + if completed.returncode: + raise SystemExit("K1 credential import failed") + staged.chmod(0o600) + with staged.open("rb") as stream: + os.fsync(stream.fileno()) + # Atomic publication without overwriting a concurrently installed key. + os.link(staged, path) +finally: + secret[:] = b"\0" * len(secret) diff --git a/apps/node-agent/packaging/k1-bundle.json b/apps/node-agent/packaging/k1-bundle.json new file mode 100644 index 0000000..c2b5a83 --- /dev/null +++ b/apps/node-agent/packaging/k1-bundle.json @@ -0,0 +1,265 @@ +{ + "schema": "missioncore.node.driver-bundle/v1", + "model_id": "xgrids.k1", + "revision": "c7ed0bba39f757afdac176a8", + "python": "3.12", + "platform": "linux-amd64", + "lock_sha256": "551c8ccdc44bc3724328dd1e316c81d20e63d2e4dcd97148377d6cacef479246", + "wheels": [ + { + "name": "aioice-0.10.2-py3-none-any.whl", + "sha256": "14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", + "bytes": 24875 + }, + { + "name": "aiortc-1.14.0-py3-none-any.whl", + "sha256": "4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", + "bytes": 93183 + }, + { + "name": "annotated_doc-0.0.4-py3-none-any.whl", + "sha256": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", + "bytes": 5303 + }, + { + "name": "annotated_types-0.7.0-py3-none-any.whl", + "sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "bytes": 13643 + }, + { + "name": "anyio-4.14.2-py3-none-any.whl", + "sha256": "9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", + "bytes": 125813 + }, + { + "name": "attrs-26.1.0-py3-none-any.whl", + "sha256": "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", + "bytes": 67548 + }, + { + "name": "av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "sha256": "7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", + "bytes": 41174337 + }, + { + "name": "bleak-3.0.2-py3-none-any.whl", + "sha256": "39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", + "bytes": 146490 + }, + { + "name": "certifi-2026.7.22-py3-none-any.whl", + "sha256": "62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", + "bytes": 136983 + }, + { + "name": "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "sha256": "c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", + "bytes": 221822 + }, + { + "name": "click-8.4.2-py3-none-any.whl", + "sha256": "e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", + "bytes": 119243 + }, + { + "name": "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", + "sha256": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", + "bytes": 4459756 + }, + { + "name": "dbus_fast-5.0.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "4ffcf16034f71a801bd2108aeffb6337d104c9459e8b1a218d16a917c8a2d2e9", + "bytes": 852687 + }, + { + "name": "dnspython-2.8.0-py3-none-any.whl", + "sha256": "01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", + "bytes": 331094 + }, + { + "name": "fastapi-0.139.0-py3-none-any.whl", + "sha256": "cf15e1e9e667ddb0ad63811e60bd11390d1aac838ca4a7a23f421807b2308189", + "bytes": 130339 + }, + { + "name": "foxglove_sdk-0.25.3-cp310-abi3-manylinux_2_28_x86_64.whl", + "sha256": "bcc894b88188d8169973cfbb1370300f671760adea9d6e9447e5a03b2289527d", + "bytes": 19220466 + }, + { + "name": "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", + "bytes": 33364 + }, + { + "name": "h11-0.16.0-py3-none-any.whl", + "sha256": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", + "bytes": 37515 + }, + { + "name": "httpcore-1.0.9-py3-none-any.whl", + "sha256": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", + "bytes": 78784 + }, + { + "name": "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", + "bytes": 523851 + }, + { + "name": "httpx-0.28.1-py3-none-any.whl", + "sha256": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", + "bytes": 73517 + }, + { + "name": "idna-3.18-py3-none-any.whl", + "sha256": "7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", + "bytes": 65455 + }, + { + "name": "ifaddr-0.2.0-py3-none-any.whl", + "sha256": "085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", + "bytes": 12314 + }, + { + "name": "lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", + "bytes": 1368249 + }, + { + "name": "markdown_it_py-4.2.0-py3-none-any.whl", + "sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", + "bytes": 91687 + }, + { + "name": "mdurl-0.1.2-py3-none-any.whl", + "sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "bytes": 9979 + }, + { + "name": "numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", + "bytes": 16672469 + }, + { + "name": "paho_mqtt-2.1.0-py3-none-any.whl", + "sha256": "6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", + "bytes": 67219 + }, + { + "name": "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", + "bytes": 6940830 + }, + { + "name": "psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", + "bytes": 155560 + }, + { + "name": "pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", + "sha256": "5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", + "bytes": 50088993 + }, + { + "name": "pycparser-3.0-py3-none-any.whl", + "sha256": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", + "bytes": 48172 + }, + { + "name": "pydantic-2.13.4-py3-none-any.whl", + "sha256": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", + "bytes": 472262 + }, + { + "name": "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "sha256": "926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", + "bytes": 2094516 + }, + { + "name": "pyee-14.0.0-py3-none-any.whl", + "sha256": "3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d", + "bytes": 15553 + }, + { + "name": "pygments-2.20.0-py3-none-any.whl", + "sha256": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", + "bytes": 1231151 + }, + { + "name": "pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", + "bytes": 2434534 + }, + { + "name": "pyopenssl-26.2.0-py3-none-any.whl", + "sha256": "4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", + "bytes": 55823 + }, + { + "name": "python_dotenv-1.2.2-py3-none-any.whl", + "sha256": "1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "bytes": 22101 + }, + { + "name": "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", + "bytes": 807870 + }, + { + "name": "rerun_sdk-0.36.3-cp310-abi3-manylinux_2_28_x86_64.whl", + "sha256": "287059b7154bf3881f5b32035f5772d0556d55a0a894650fb74a2605fb39afbe", + "bytes": 163018185 + }, + { + "name": "rich-14.3.4-py3-none-any.whl", + "sha256": "07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", + "bytes": 310480 + }, + { + "name": "shellingham-1.5.4-py2.py3-none-any.whl", + "sha256": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", + "bytes": 9755 + }, + { + "name": "starlette-1.3.1-py3-none-any.whl", + "sha256": "c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", + "bytes": 73632 + }, + { + "name": "typer-0.26.8-py3-none-any.whl", + "sha256": "3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", + "bytes": 122564 + }, + { + "name": "typing_extensions-4.16.0-py3-none-any.whl", + "sha256": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "bytes": 45571 + }, + { + "name": "typing_inspection-0.4.2-py3-none-any.whl", + "sha256": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", + "bytes": 14611 + }, + { + "name": "uvicorn-0.51.0-py3-none-any.whl", + "sha256": "5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", + "bytes": 73219 + }, + { + "name": "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "sha256": "7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", + "bytes": 4426307 + }, + { + "name": "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "sha256": "e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", + "bytes": 456398 + }, + { + "name": "websockets-16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", + "sha256": "35f41979c8623df9bd30d949d82010a8fda5c56ff12cd8508a5b7272b6d4b53a", + "bytes": 187345 + } + ] +} diff --git a/apps/node-agent/packaging/k1_bootstrap.py b/apps/node-agent/packaging/k1_bootstrap.py new file mode 100644 index 0000000..eddb271 --- /dev/null +++ b/apps/node-agent/packaging/k1_bootstrap.py @@ -0,0 +1,26 @@ +"""Fixed root-owned import path; no user site, environment path or import hooks.""" + +import os +import sys +from pathlib import Path + +root = Path("/var/lib/mission-core-k1-runtime") +reference = root / "active.path" +runtime = Path(reference.read_text().strip()) +if ( + reference.is_symlink() + or runtime.is_symlink() + or runtime.parent != root + or not runtime.name.isalnum() + or runtime.stat().st_uid != 0 + or runtime.stat().st_mode & 0o022 +): + raise RuntimeError("Unsafe K1 runtime") +sys.path[:0] = [ + str(runtime), str(runtime / "rerun_sdk"), + "/usr/lib/mission-core-node/k1/src", "/usr/lib/mission-core-node/sdk", +] +os.environ["MISSIONCORE_DATA_DIR"] = "/var/lib/mission-core-k1" +from k1link.device_plugins.xgrids_k1.node_bridge import main + +main() diff --git a/apps/node-agent/packaging/k1_prepare.py b/apps/node-agent/packaging/k1_prepare.py new file mode 100644 index 0000000..0b8db58 --- /dev/null +++ b/apps/node-agent/packaging/k1_prepare.py @@ -0,0 +1,94 @@ +"""Install only the bundled, hash-pinned Ubuntu K1 runtime; no network I/O.""" + +import hashlib +import json +import os +import shutil +import sys +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +SHARE = Path("/usr/share/mission-core-node/k1") +ROOT = Path("/var/lib/mission-core-k1-runtime") + + +def members(archive): + for info in archive.infolist(): + path = PurePosixPath(info.filename) + if ( + path.is_absolute() + or ".." in path.parts + or (info.external_attr >> 16) & 0o170000 == 0o120000 + or ".data" in path.parts + ): + raise RuntimeError("Unsafe K1 runtime archive") + # Rerun's pinned wheel declares this one static package directory. + # We do not execute .pth files; bootstrap adds the exact directory. + if info.filename.endswith(".pth") and not ( + info.filename == "rerun_sdk.pth" and archive.read(info) == b"rerun_sdk\n" + ): + raise RuntimeError("Unreviewed K1 Python path hook") + # Distribution script/data relocation must be handled deliberately, + # never interpreted as an install hook by the operator's Python. + if any(part.endswith(".data") for part in path.parts): + raise RuntimeError("K1 wheel requires unsupported relocation") + yield info + + +def prepare(): + if os.geteuid() != 0 or os.uname().machine != "x86_64" or sys.version_info[:2] != (3, 12): + raise RuntimeError("K1 runtime requires privileged Ubuntu amd64 Python 3.12 installation") + release = Path("/etc/os-release").read_text() + if "ID=ubuntu" not in release or 'VERSION_ID="24.04"' not in release: + raise RuntimeError("K1 runtime requires Ubuntu 24.04") + manifest = json.loads((SHARE / "bundle.json").read_text()) + revision = manifest["revision"] + if not revision.isalnum(): + raise RuntimeError("Invalid K1 runtime revision") + ROOT.mkdir(mode=0o755, exist_ok=True) + if ROOT.is_symlink() or ROOT.stat().st_uid != 0 or ROOT.stat().st_mode & 0o022: + raise RuntimeError("Unsafe K1 runtime root") + target = ROOT / revision + if target.is_symlink() or (ROOT / "active.path").is_symlink(): + raise RuntimeError("Unsafe K1 runtime reference") + for item in manifest["wheels"]: + path = SHARE / item["name"] + if ( + path.name != item["name"] + or path.is_symlink() + or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"] + ): + raise RuntimeError("K1 runtime checksum mismatch") + if not target.exists(): + stage = Path(tempfile.mkdtemp(prefix=".k1-", dir=ROOT)) + try: + for item in manifest["wheels"]: + with zipfile.ZipFile(SHARE / item["name"]) as archive: + archive.extractall(stage, members=members(archive)) + for path in stage.rglob("*"): + path.chmod(0o755 if path.is_dir() else 0o644) + stage.chmod(0o755) + stage.rename(target) + finally: + if stage.exists(): + shutil.rmtree(stage) + for item in manifest["wheels"]: + with zipfile.ZipFile(SHARE / item["name"]) as archive: + for info in members(archive): + path = target / info.filename + if path.is_symlink() or ( + not info.is_dir() and path.read_bytes() != archive.read(info) + ): + raise RuntimeError("Installed K1 runtime differs from bundled wheel") + fd, name = tempfile.mkstemp(prefix=".active-", dir=ROOT) + with os.fdopen(fd, "w") as stream: + os.fchmod(stream.fileno(), 0o644) + stream.write(str(target)) + stream.flush() + os.fsync(stream.fileno()) + os.replace(name, ROOT / "active.path") + + +if __name__ == "__main__": + prepare() diff --git a/apps/node-agent/packaging/mission-core-k1.service b/apps/node-agent/packaging/mission-core-k1.service new file mode 100644 index 0000000..a077f42 --- /dev/null +++ b/apps/node-agent/packaging/mission-core-k1.service @@ -0,0 +1,38 @@ +[Unit] +Description=Mission Core Node K1 Bridge and acquisition +After=bluetooth.service NetworkManager.service +Wants=bluetooth.service NetworkManager.service + +[Service] +Type=simple +User=mission-core-k1 +Group=mission-core-node +SupplementaryGroups=bluetooth +ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/k1_bootstrap.py +StateDirectory=mission-core-k1 +StateDirectoryMode=0700 +RuntimeDirectory=mission-core-k1 +RuntimeDirectoryMode=0750 +LoadCredentialEncrypted=k1-application +UMask=0007 +Environment=OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 +Restart=on-failure +RestartSec=3 +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +CapabilityBoundingSet= +LockPersonality=yes +TasksMax=128 +MemoryMax=2G +LimitNOFILE=2048 + +[Install] +WantedBy=multi-user.target diff --git a/apps/node-agent/packaging/postinst b/apps/node-agent/packaging/postinst index 1bb0274..cacd52a 100644 --- a/apps/node-agent/packaging/postinst +++ b/apps/node-agent/packaging/postinst @@ -8,6 +8,10 @@ case "$1" in if ! getent passwd mission-core-sensors >/dev/null; then adduser --system --group --home /var/lib/mission-core-sensors --no-create-home --disabled-login mission-core-sensors fi + if ! getent passwd mission-core-k1 >/dev/null; then + adduser --system --home /var/lib/mission-core-k1 --no-create-home --disabled-login --ingroup mission-core-node mission-core-k1 + fi + /usr/bin/python3 -I /usr/lib/mission-core-node/k1_prepare.py # Only bootstrap required to open the GUI. Operational configuration is a # versioned job started by «Настройка окружения → Сконфигурировать». if [ -d /run/systemd/system ]; then @@ -15,6 +19,8 @@ case "$1" in systemctl enable mission-core-node.service systemctl restart mission-core-node.service systemctl try-restart mission-core-realsense.service + systemctl enable mission-core-k1.service + systemctl restart mission-core-k1.service fi ;; esac diff --git a/apps/node-agent/packaging/preinst b/apps/node-agent/packaging/preinst index b49cb8d..cff2e9e 100644 --- a/apps/node-agent/packaging/preinst +++ b/apps/node-agent/packaging/preinst @@ -8,6 +8,12 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then exit 1 fi fi + if [ -S /run/mission-core-k1/driver.sock ]; then + if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then + echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2 + exit 1 + fi + fi mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true) case "$mc_node_device_job" in active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;; @@ -20,6 +26,11 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then exit 1 ;; esac + # End the admitted idle worker before dpkg replaces its Python modules. + # New UI commands now fail unavailable instead of racing the package copy. + if [ -f /usr/lib/systemd/system/mission-core-k1.service ]; then + systemctl stop mission-core-k1.service + fi fi . /etc/os-release if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 24.04 ]; then diff --git a/apps/node-agent/packaging/prerm b/apps/node-agent/packaging/prerm index 08d783d..da15f13 100644 --- a/apps/node-agent/packaging/prerm +++ b/apps/node-agent/packaging/prerm @@ -7,6 +7,12 @@ if [ -d /run/systemd/system ]; then exit 1 fi fi + if [ -S /run/mission-core-k1/driver.sock ]; then + if ! /usr/bin/python3 -I -c 'import http.client,json,socket; c=http.client.HTTPConnection("k1",timeout=5); c.sock=socket.socket(socket.AF_UNIX); c.sock.settimeout(5); c.sock.connect("/run/mission-core-k1/driver.sock"); c.request("GET","/prepare-safe"); r=c.getresponse(); assert r.status==200 and json.load(r).get("safe") is True'; then + echo "Mission Core Node: завершите подключение или запись K1 перед обновлением." >&2 + exit 1 + fi + fi mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true) case "$mc_node_device_job" in active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;; @@ -40,6 +46,8 @@ case "$1" in /usr/sbin/sshd -t systemctl try-reload-or-restart ssh.service fi + systemctl stop mission-core-k1.service + systemctl disable mission-core-k1.service || true systemctl stop mission-core-realsense.service systemctl disable mission-core-realsense.service || true systemctl stop mission-core-node.service diff --git a/apps/node-agent/packaging/resolve_k1_bundle.py b/apps/node-agent/packaging/resolve_k1_bundle.py new file mode 100644 index 0000000..f5828df --- /dev/null +++ b/apps/node-agent/packaging/resolve_k1_bundle.py @@ -0,0 +1,60 @@ +"""Resolve Linux wheel inputs from the frozen monorepo lock, without downloading.""" +import hashlib +import json +import subprocess +import tomllib +from pathlib import Path + +from packaging.markers import default_environment +from packaging.requirements import Requirement +from packaging.tags import compatible_tags, cpython_tags +from packaging.utils import canonicalize_name, parse_wheel_filename + +ROOT = Path(__file__).resolve().parents[1] +REPOSITORY = ROOT.parents[1] + + +def resolve(): + requirements = ROOT / "build/k1-requirements.txt" + requirements.parent.mkdir(exist_ok=True) + subprocess.run(["uv", "export", "--frozen", "--extra", "node-device-media", "--no-dev", + "--no-emit-project", "--no-emit-package", "missioncore-plugin-sdk", "--no-hashes", + "--output-file", str(requirements)], cwd=REPOSITORY, check=True, stdout=subprocess.DEVNULL) + environment = default_environment() + environment.update(sys_platform="linux", platform_system="Linux", platform_machine="x86_64", + python_version="3.12", python_full_version="3.12.3", implementation_name="cpython", + platform_python_implementation="CPython") + platforms = [f"manylinux_2_{n}_x86_64" for n in range(39, 16, -1)] + ["manylinux2014_x86_64", "linux_x86_64"] + tags = list(cpython_tags((3, 12), platforms=platforms)) + list(compatible_tags((3, 12), interpreter="cp312", platforms=platforms)) + ranks = {tag: i for i, tag in enumerate(tags)} + lock_data = (REPOSITORY / "uv.lock").read_bytes() + lock = tomllib.loads(lock_data.decode()) + items = [] + for line in requirements.read_text().splitlines(): + if not line or line.lstrip().startswith("#"): + continue + requirement = Requirement(line) + if requirement.marker and not requirement.marker.evaluate(environment): + continue + name = canonicalize_name(requirement.name) + package = next(v for v in lock["package"] if canonicalize_name(v["name"]) == name and v["version"] in requirement.specifier) + candidates = [] + for wheel in package.get("wheels", []): + filename = wheel["url"].split("/")[-1] + _, _, _, wheel_tags = parse_wheel_filename(filename) + matches = [ranks[tag] for tag in wheel_tags if tag in ranks] + if matches: + candidates.append((min(matches), filename, wheel)) + if not candidates: + raise RuntimeError("No reviewed Linux wheel: " + name) + _, filename, wheel = min(candidates) + items.append({"name": filename, "sha256": wheel["hash"].removeprefix("sha256:"), "bytes": wheel["size"]}) + revision = hashlib.sha256(json.dumps(items, sort_keys=True).encode()).hexdigest()[:24] + manifest = {"schema": "missioncore.node.driver-bundle/v1", "model_id": "xgrids.k1", "revision": revision, + "python": "3.12", "platform": "linux-amd64", "lock_sha256": hashlib.sha256(lock_data).hexdigest(), "wheels": items} + (ROOT / "packaging/k1-bundle.json").write_text(json.dumps(manifest, indent=2) + "\n") + print(json.dumps({"wheels": len(items), "bytes": sum(v["bytes"] for v in items), "revision": revision})) + + +if __name__ == "__main__": + resolve() diff --git a/apps/node-agent/ui/package-lock.json b/apps/node-agent/ui/package-lock.json index 6c318f5..a4c22df 100644 --- a/apps/node-agent/ui/package-lock.json +++ b/apps/node-agent/ui/package-lock.json @@ -11,6 +11,7 @@ "@nodedc/tokens": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", "@nodedc/ui-core": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/ui-react": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", + "@rerun-io/web-viewer": "0.36.3", "react": "19.1.0", "react-dom": "19.1.0" }, @@ -18,7 +19,8 @@ "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "typescript": "^5.8.3", - "vite": "^7.0.0" + "vite": "^7.0.0", + "vite-plugin-wasm": "^3.6.0" } }, "../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens": { @@ -524,6 +526,12 @@ "resolved": "../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "link": true }, + "node_modules/@rerun-io/web-viewer": { + "version": "0.36.3", + "resolved": "https://registry.npmjs.org/@rerun-io/web-viewer/-/web-viewer-0.36.3.tgz", + "integrity": "sha512-LMGnsxRmY5UwiGras2dZrMnEYkow5Xr4v+1hAUSspXWPPiilMqoz9G77jo8Ps/deAaX81TnOq123DFO8iX/Ulw==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.63.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", @@ -1239,6 +1247,16 @@ "optional": true } } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } } } } diff --git a/apps/node-agent/ui/package.json b/apps/node-agent/ui/package.json index 1c77b70..a1d20c7 100644 --- a/apps/node-agent/ui/package.json +++ b/apps/node-agent/ui/package.json @@ -9,6 +9,7 @@ "build": "tsc --noEmit && vite build" }, "dependencies": { + "@rerun-io/web-viewer": "0.36.3", "@nodedc/ui-react": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-react", "@nodedc/ui-core": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/ui-core", "@nodedc/tokens": "file:../../../../NODEDC_DESIGN_GUIDELINE/packages/tokens", @@ -16,6 +17,7 @@ "react-dom": "19.1.0" }, "devDependencies": { + "vite-plugin-wasm": "^3.6.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "typescript": "^5.8.3", diff --git a/apps/node-agent/ui/rerun-runtime.html b/apps/node-agent/ui/rerun-runtime.html new file mode 100644 index 0000000..08963b8 --- /dev/null +++ b/apps/node-agent/ui/rerun-runtime.html @@ -0,0 +1,16 @@ + + + + + + Живой просмотр K1 + + + +
+ + + diff --git a/apps/node-agent/ui/src/NodeSensors.tsx b/apps/node-agent/ui/src/NodeSensors.tsx index 26294e1..248184b 100644 --- a/apps/node-agent/ui/src/NodeSensors.tsx +++ b/apps/node-agent/ui/src/NodeSensors.tsx @@ -1,5 +1,7 @@ +import {xgridsK1SensorUi,K1EnrollmentWindow} from '../../../../plugins/xgrids-k1/frontend/src/sensors/plugin'; +import {createIsolatedRerunHost} from '../../../control-station/src/components/rerun/isolatedRerunHost'; import {SensorWorkspace} from '../../../../packages/sensor-ui/src/SensorWorkspace'; import type {SensorTransport} from '../../../../packages/sensor-ui/src/contracts'; import {request} from './api'; -const transport:SensorTransport={subscribe:(receive,unavailable)=>{const events=new EventSource('/api/devices/events');events.onmessage=e=>{try{receive(JSON.parse(e.data));}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();},inventory:()=>request('/api/devices'),submit:value=>request('/api/devices/operations','POST',value),operation:id=>request('/api/devices/operations/'+encodeURIComponent(id))}; -export function NodeSensors(){return ;} +const transport:SensorTransport={enrollment:{state:()=>request("/api/devices/enrollment"),submit:value=>request("/api/devices/enrollment/operations","POST",value),operation:id=>request("/api/devices/enrollment/operations/"+encodeURIComponent(id))},subscribe:(receive,unavailable)=>{const events=new EventSource('/api/devices/events');events.onmessage=e=>{try{receive(JSON.parse(e.data));}catch{unavailable();}};events.onerror=unavailable;return()=>events.close();},inventory:()=>request('/api/devices'),submit:value=>request('/api/devices/operations','POST',value),operation:id=>request('/api/devices/operations/'+encodeURIComponent(id))}; +export function NodeSensors(){return ;} diff --git a/apps/node-agent/ui/src/rerunRuntime.ts b/apps/node-agent/ui/src/rerunRuntime.ts new file mode 100644 index 0000000..2753a41 --- /dev/null +++ b/apps/node-agent/ui/src/rerunRuntime.ts @@ -0,0 +1 @@ +import "../../../control-station/src/components/rerun/isolatedRerunEntry"; diff --git a/apps/node-agent/ui/tsconfig.json b/apps/node-agent/ui/tsconfig.json index 1162518..9ef65a0 100644 --- a/apps/node-agent/ui/tsconfig.json +++ b/apps/node-agent/ui/tsconfig.json @@ -22,6 +22,9 @@ ], "@nodedc/ui-react": [ "node_modules/@nodedc/ui-react/dist/index.d.ts" + ], + "@mission-core/sensor-sdk": [ + "../../../packages/sensor-ui/src/pluginSdk.ts" ] } }, diff --git a/apps/node-agent/ui/vite.config.js b/apps/node-agent/ui/vite.config.js index 6c16b4f..ee33228 100644 --- a/apps/node-agent/ui/vite.config.js +++ b/apps/node-agent/ui/vite.config.js @@ -1,9 +1,17 @@ import { defineConfig } from "vite"; +import { fileURLToPath } from "node:url"; +import wasm from "vite-plugin-wasm"; export default defineConfig({ + plugins: [wasm()], // Shared sensor TSX lives outside this app tsconfig; use the same JSX runtime. esbuild: { jsx: "automatic" }, // Design Guideline packages are linked during development. Their own React // must never become a second hook dispatcher in the portable production bundle. - resolve: { dedupe: ["react", "react-dom", "@nodedc/ui-react"] }, + resolve: { alias: {"@mission-core/sensor-sdk": fileURLToPath(new URL("../../../packages/sensor-ui/src/pluginSdk.ts", import.meta.url))}, dedupe: ["react", "react-dom", "@nodedc/ui-react"] }, + optimizeDeps: { exclude: ["@rerun-io/web-viewer"] }, + build: { target: "esnext", rollupOptions: { input: { + app: fileURLToPath(new URL("./index.html", import.meta.url)), + rerun: fileURLToPath(new URL("./rerun-runtime.html", import.meta.url)), + } } }, }); diff --git a/docs/04_K1_WIFI_PROVISIONING_PROFILE.md b/docs/04_K1_WIFI_PROVISIONING_PROFILE.md index ce193bb..aa9f5d3 100644 --- a/docs/04_K1_WIFI_PROVISIONING_PROFILE.md +++ b/docs/04_K1_WIFI_PROVISIONING_PROFILE.md @@ -171,6 +171,25 @@ mode. It never fragments or retries the payload automatically. ## Expected transition and evidence of acceptance +### Firmware station error replies + +Offline review of the exact 3.0.2 `lixel_nman` callback found that station +`wifi_connect` returns are passed directly to the 7f01 GATT write result: +4 denotes its SSID-not-found paths and 6 denotes its credentials-required path. +Generic Bluetooth libraries label the same values INVALID_PDU and +REQUEST_NOT_SUPPORTED. Preserve those raw values but do not discard the +reviewed device interpretation. The evidence and disassembly addresses are in +[the callback audit](audits/2026-09-06-k1-station-reply-semantics.md). + +Apply this interpretation only to the selected exact firmware station profile +and its annotated 99-byte write-with-response failure. It does not apply to +Quick Connect, a failed baseline read or arbitrary ATT errors. It grants no +retry, network-state confirmation or ledger resolution. The firmware performs +Wi-Fi work before completing this callback; a delayed reply alone is not proof +that the BLE connection request was the slow stage. + +### Acceptance observations + A completed GATT write only proves transport completion. It does not prove that the K1 joined Wi-Fi or began beaconing. The application polls `7f02`; the observed response frame contains a fixed-width text slot, an address slot, a diff --git a/docs/audits/2026-09-06-k1-bridge-architecture-review.md b/docs/audits/2026-09-06-k1-bridge-architecture-review.md new file mode 100644 index 0000000..22b8298 --- /dev/null +++ b/docs/audits/2026-09-06-k1-bridge-architecture-review.md @@ -0,0 +1,269 @@ +# K1 Bridge: архитектура, эксплуатационные сценарии и граница рефакторинга + +Дата среза: 06.09.2026. Основание: просьба владельца полностью восстановить контекст K1 из Ops перед решением о рефакторинге. Это документальный и статический аудит текущей рабочей копии, а не новая аппаратная приёмка. + +Исходный commit: `020a878915ea32c64963975447650fd8eee29071`. Рабочее дерево уже содержало изменения Node, K1, viewer и UI. При этом аудите исходники, прошивка, состояние сканера, службы и Ops не изменялись. Созданы только этот отчёт и индексы источников. Тесты, сборки, BLE-поиск, provisioning, START/STOP и отключения сети не запускались. + +## 1. Вывод + +Опасение о чрезмерной связности подтверждается кодом. `facade.py` — 35 996 строк; `connect()` — 1 971; `_adopt_existing_lan_connection()` — 1 224; `state()` — 1 028; `_reconcile_acquisition()` — 1 587. Экран `K1ProvisioningPipeline.tsx` — 3 732 строки, runtime hook — 2 135. В экран одновременно входят локальный черновик, выбор режима, scan, попытка provisioning, чтение статуса, восстановление, история физической команды и представление ошибок. + +Однако это не означает, что все проверки лишние. Существенная часть сложности появилась после доказанных инцидентов: повторные команды после неизвестного результата, поздний ответ старого процесса, потеря сети после START, STOP без READY, восстановление камеры, гонка REST/WebSocket, удержание старого Rerun listener. Удалить эти проверки ради короткой функции подключения означало бы вернуть реальные дефекты. + +Нужна декомпозиция владельцев и переходов при сохранении поведения. Уже существуют полезные границы: BLE transport, connection supervisor, network ledger, physical-command coordinator, recovery checkpoint, control transcript. Основной долг находится в их соединении внутри facade и в повторной интерпретации состояния разными UI. + +Текущая ошибка подключения и архитектурный долг — связанные, но разные вопросы. Сам размер файла не доказывает причину ATT-ответа K1. Последние изменения улучшили диагноз и выход из ошибки; успешное подключение последней попытки ими не доказано. + +## 2. Что найдено в Ops + +Через прямой NODE.DC Ops MCP получены все 76 карточек MISSION CORE с описаниями и structured blocks. У 41 карточки есть метка `XGRIDS K1`; значительная часть — потребители уже записанного K1 evidence в LAB. Для 45 карточек получены все страницы активных комментариев: 128 комментариев, без оставшейся пагинации. Полный индекс названий, блоков, дат и comment IDs: [Ops index](2026-09-06-k1-bridge-ops-index.json). + +Глубокая смысловая сверка выполнена для сетевого/control/recovery контура, Node и границ viewer. Индексация лабораторных карточек не выдаётся за повторный аудит всех алгоритмов CV. + +| Карточка | Роль в этом разборе | +|---|---| +| MISSIONCOR-3 — Mission Core. Lixel K1 / XGRIDS Integration | Основная текущая приёмка K1; packet oracle, 14 операций, Bridge/Quick, сон/сеть, камера, baseline производительности | +| MISSIONCOR-49 — K1 · Проблемы сканирования | История отдельных сетевых, control, producer и Rerun отказов; физические повторные циклы; причины предыдущих регрессий | +| MISSIONCOR-76 — Mission Core Node · Архитектурные границы для бортового ПК | Владение устройствами на борту, связь Core/Node, reboot, журнал команд, Linux-паритет; уточнения владельца в комментариях | +| MISSIONCOR-66 — Mission Core. Канон интеграции Rerun | Границы live, Saved Sessions и LAB; запрет менять общий lifecycle ради локальной лаборатории | +| MISSIONCOR-74 — Additional Core · Переносимая кастомизация Rerun | Связанный реестр кастомизаций; индексирован, актуальные различия поверхностей прочитаны в свежем блоке #66 | +| MISSIONCOR-7 — Mission Core. Milestone — canonical K1 control and durable archive acceptance | Историческая физическая START/live/STOP/READY/archive приёмка | +| MISSIONCOR-10 — Operational Core — Real-time Record Limits | Исторические ограничения записи, durability, consumer backpressure; часть описания recovery устарела | +| MISSIONCOR-51 — Технический долг Mission Core | Отдельные полевые и вычислительные ограничения; не источник разрешения ослабить K1 safety | +| MISSIONCOR-1, -2, -5, -50 | Архитектурный и исторический контекст проекта, SDK и границ компонентов | +| MISSIONCOR-4 — Archive. NDC_xgrids-k1-connector — historical evidence | Раннее физическое evidence; явно архивная архитектура | +| MISSIONCOR-8, -11 и остальные K1 LAB-карточки | Downstream camera/perception/calibration/replay; учитывать как потребителей неизменного source-of-record | + +В #76 комментарии 05.09 имеют решающее уточнение: первый бортовой K1 — **только Bridge**; существующий macOS/Quick остаётся тестовым путём. В теле старого baseline ещё написан последующий Linux Quick: это не новая задача. Все действия оператора должны проходить через GUI. Порядок пользовательских уточнений важнее старого шаблона карточки. + +## 3. Восстановленная история и достоверность + +| Дата / источник | Что действительно было подтверждено | Чего это не доказывает | +|---|---|---| +| 16.07, #3, #4 | BLE/Wi-Fi vertical slice; LixelGO/iPhone IP capture; MQTT/RTSP и packet map | IP capture не содержит BLE HCI; не является дампом самого 7f01 provisioning | +| 19.07, #7 | Canonical START/live/STOP/READY и durable archive на одном K1 | Linux, второй K1, другая FW, многочасовая эксплуатация | +| 28.07, #49 | Повторные Quick/Bridge циклы и вход после очистки cache | Успешное соединение не означало исправный Rerun receiver | +| 06.08, #49 | Bridge физически работал; отдельно диагностирован Quick/CoreWLAN helper regression | Нельзя переносить причину Quick helper на Bridge | +| 21–22.08, #49/#3 | Устранены starvation, camera churn, Rerun admission и state-channel проблемы; подтверждён STOP + READY | Число unit tests не заменяет отдельную аппаратную приёмку каждого recovery | +| 23.08, #3 | Bridge → Quick → Bridge → Quick; сон во время запуска; сеть off → сон → wake → сеть on; камера и облако восстановились без повторного START/STOP | Полный reboot Node во время K1 записи, другая ОС/FW, универсальный SLA | +| 05–06.09, #76 | Pairing, heartbeat offline/online при остановке Node-службы, сохранение identity, D455 этап | Аппаратный K1/Linux parity ещё не принят | +| 06.09, локальные incident-аудиты | Есть успешный Bridge, затем ATT-отказы; отдельно найден неверный camera evidence-root | Последние подключения и камера после исправления ещё не приняты новым чистым UI-прогоном | + +Внутренний быстрый baseline — `eaad9de`, `20260822T105904Z_viewer_live`: START до индикации калибровки <5 с, калибровка 21 с, облако +2 с, правая камера +4 с; callback→publication p50/p95 23,839/41,541 ms. Это один физический run на коротком ledger, не гарантированная задержка любого подключения. Recovery-capable baseline `1001a31` имеет другие задержки и расширенные гарантии. Подробности: [internal baseline](../lab/010_K1_MISSION_CORE_INTERNAL_LIVE_BASELINE_20260822.redacted.md). + +### Обнаруженные расхождения источников + +1. Manifest содержит 77 сценариев: 51 `software-covered`, 17 `partial`, 9 `planned`. Это значения файла, не результат новых тестов. Например, переключение Bridge/Quick и sleep/wake ещё отмечены как требующие hardware evidence, хотя более поздняя #3 описывает физическую приёмку. +2. #10 пишет, что MQTT reconnect отсутствует; #3 и текущий recovery-код содержат ограниченное восстановление активного потока. Историческое ограничение нельзя применять как описание текущего пути. +3. #49 хранит старый статус незакрытой Rerun-приёмки; более поздняя #3 закрывает конкретный принятый beta-профиль. Приёмку следует связывать с датой, commit, платформой и точным сценарием. +4. В #3 краткий oracle-блок неточно объединяет финальный PCL и teardown около +0,951 с. Явная ERRATA в комментарии `c1ef70e2-d900-4d42-ad78-d460040a42d7` различает последний PCL PUBLISH +36,415 ms, pose +40,787 ms, RTSP media +54,057 ms и teardown около +951 ms. Эталон должен учитывать исправление. +5. Документ supervision описывает фиксированный шестисекундный scan; текущий scanner использует одно непрерывное окно 6→20 с при отсутствии K1. Это документальный drift после сегодняшнего изменения. +6. В документах есть разная формулировка browser close: завершение operator session и сохранение backend-owned capture. Это разные владельцы. Контракт CONN-62 и код не разрешают WebSocket disconnect инициировать scanner-команду. Для рефакторинга нужно явно зафиксировать отдельно draft, UI connection, lease и acquisition. + +## 4. Bridge по слоям: нормальная последовательность + +```mermaid +flowchart TD + UI[Оператор: найти, выбрать K1, ввести сеть] --> Intent[Один intent: runtime, mode, discovery generation, operation ID] + Intent --> Admission[Проверка владельца, текущего состояния и журналов] + Admission --> BLE[Точный BLE handle: GATT contract и 7f02 baseline] + BLE --> Journal[Сохранить границу dispatch до записи] + Journal --> Write[Одна 99-byte запись в 7f01] + Write --> Status[7f02: подтверждение требуемой сети и адреса] + Status --> Applied[Durable network_applied] + Applied --> Control[Read-only route, TCP, DeviceInfo bootstrap] + Control --> Ready[Текущая identity и готовность управления] + Ready --> Start[Отдельный явный START по каноническому диалогу] + Start --> Raw[Локальная исходная запись] + Start --> Preview[Ограниченный live preview: облако и камера] +``` + +Для LAB весь runtime принадлежит компьютеру оператора. Для борта путь до `Intent` проходит Core → аутентифицированный Node channel → Node broker → тот же K1 runtime. Радио, проверка маршрута к K1, secret provider и recorder принадлежат БК. Сеть Core может отличаться от локальной сети Node/K1. + +### Провода протокола + +Общий Bridge profile FW 3.0.2: service `7f00`, write `7f01`, status `7f02`. Frame — ровно 99 bytes: длина SSID, 32-byte slot, длина password, 64-byte slot, конечный zero. Mission Core использует один `with_response`, как в принятом macOS run. Это не Quick AP-enable, где отдельный 100-byte frame. Источники: [reviewed profile](../04_K1_WIFI_PROVISIONING_PROFILE.md), `ble/wifi_provisioning.py:193`, вызов в `facade.py:11747`. + +`write_gatt_char` ACK доказывает транспортный факт, но не готовность MQTT. Состояние сети, локальный route, TCP endpoint, DeviceInfo identity, control и свежие sensor data — самостоятельные доказательства. Нельзя заменить их единым `connected` или принимать «точки пришли» за право на новую команду. + +Прикладной transcript остаётся непрерывной MQTT-сессией: ordinals 01–10 перед START; ordinal 11 — START; 12 — свежий initialized SCANNING; 13–14 — завершающее read-only обновление. Подготовка включает согласованную синхронизацию времени и не является целиком read-only. Recovery использует inspection-only путь, который её не повторяет. + +Критическая граница сна: после ordinal 12 physical resolve и recovery checkpoint сохраняются до 13–14; public SCANNING/STOP удерживаются transition gate до завершения refresh. Потеря связи в этот момент не должна уничтожить уже доказанный START. Реализация: `protocol/application_session.py:1020`, `facade.py:24291`, тест `test_post_start_refresh_timeout_wakes_existing_read_only_recovery`. + +## 5. Владельцы и состояния, которые нельзя смешать + +| Владелец | Состояние / обязанность | Должен пережить | +|---|---|---| +| UI draft | Выбранный кандидат, SSID/пароль до отправки, локальное ожидание | Ничего, что могло бы восстановить право повторной записи после смены runtime | +| BLE arbiter | Один нативный radio owner, точный handle, cleanup | Отмену coroutine до фактического освобождения нативной операции | +| Network idempotency journal | Один operation ID и неизменность его запроса | Потерю HTTP-ответа и backend restart без повторного write | +| Network mutation ledger | prepared/dispatching/observing/terminal и доказательства 7f02 | Crash на границе отправки | +| Semantic topology store | Последняя подтверждённая конфигурация сети | Restart, но только как configured/offline, без live authority | +| Connection supervisor | Intent, host epoch, route/TCP/DeviceInfo, отдельные control/data planes | Короткую потерю сети через отзыв зависимых полномочий | +| Physical coordinator/ledger | START/STOP и доказанный либо неизвестный физический результат | Потерю сети, process crash и UI reset; не очищается вместе с формой | +| Active recovery checkpoint | Точная lineage acquisition/device/project/evidence и gaps | Поддерживаемый rebind/restart без нового START | +| Recorder/camera producer | Raw evidence и committed prefixes | Закрытие browser, медленного consumer, смену просмотрщика | +| Viewer | Disposable receiver, canvas/media transport, профиль отображения | Пересоздание consumer без управления сканером | +| Core/Node pairing | Постоянное доверие, heartbeat, binding | Обрыв канала и reboot; состояние K1 доказывается отдельно | + +Часть журналов кажется дублированием только по названию: журнал сетевого запроса и журнал физического START отвечают на разные вопросы. Их физическое объединение без анализа crash consistency опасно. При этом хранение однотипных presentation-состояний в нескольких frontend-местах не даёт новых гарантий и является кандидатом на упрощение. + +`state()` сейчас не чистая проекция: он выполняет локальную retirement/reconciliation работу и может инициировать уже разрешённый active-stream recovery. Это видно с `facade.py:7285`. Поэтому простое изменение частоты polling или перенос `state()` в новый UI может затронуть lifecycle. Чистую проекцию можно выделять только вместе с независимым владельцем reconciliation, сохранив все переходы и их порядок. + +## 6. Матрица поведения, которое следует сохранить + +Статусы ниже различают историческую физическую приёмку, наличие реализации/тестов и непроверенный Node parity. Наличие теста здесь не означает его нового запуска. + +| Сценарий | Обязательное поведение | Основание / текущая граница | +|---|---|---| +| Первый scan не сразу видит включённый K1 | Одно ограниченное discovery; никаких скрытых connect/write; отдельное время первого candidate | `ble/scanner.py:1207`; сегодняшнее окно 6→20 с; свежая UI-приёмка нужна | +| Выбор устройства, ввод сети | Только локальный draft; никаких аппаратных действий от selection | #3, canon CONN-06/-70/-74; frontend fences | +| Apply | Не более одной reviewed сетевой mutation, exact captured handle | `wifi_provisioning.py:687`; durable callback перед write | +| Отказ до write | Прямо сообщить отсутствие отправки; освободить завершённую попытку | Network journal/ledger и error annotations | +| Потеря ACK или HTTP после write | Не повторять write; читать результат того же intent | `networkProvisioning.ts:105`; idempotency journal | +| K1 подключился к Wi-Fi, MQTT ещё не готов | network_applied сохраняется; read-only bootstrap не превращается во второй Apply | `facade.py:12001`, `_schedule_control_bootstrap_continuation:4439` | +| Неверная сеть / старый 7f02 | Не принимать чужой/старый target; ожидание и classifier должны согласоваться | `wifi_provisioning.py:709`, `_post_dispatch_network_target:35334`; найдено расхождение завершения polling | +| Повторный scan после STOP | Новый acquisition без stale camera/ingress/session | #49 field acceptance; `test_next_scan_retires_stale_terminal_live_perception_ingress_before_start` | +| Потеря только интернета при живой Node/K1 LAN | Локальный capture не зависит от Core preview; Core показывает потерю канала | #76 ownership; физический Linux K1 gate открыт | +| Потеря маршрута Node/K1 во время записи | Отозвать control; сохранить exact lineage; bounded read-only rebind | #3 23.08 macOS; `test_control_first_loss_freezes_topology_before_ephemeral_retirement` | +| Сеть off → sleep → wake → сеть on | Не повторять START; новый host epoch и новые identity/control proofs; вернуть data consumers | #3 физически принято на Mac; на Node отдельно | +| Потеря сети между ordinal 12 и 13–14 | Сохранить durable START proof, выполнить существующий read-only recovery | `application_session.py:1020`, lifecycle test `test_post_start_refresh_timeout_wakes_existing_read_only_recovery` | +| K1 выключился во время калибровки | Не висеть в ложном ожидании; SCAN_OVER завершает локально, physical ambiguity сохраняется | #3 calibration-loss; `_reconcile_acquisition` | +| K1 вернулся READY после power cycle | Зафиксировать cessation, не объявлять успешный STOP и не начинать новый START | `test_active_stream_recovery_device_standby_never_restarts_scanner` | +| По прежнему IP отвечает другой K1/сервис | Не принимать endpoint за identity; не переписывать pin автоматически | `test_active_stream_recovery_wrong_identity_blocks_without_retry_or_camera_reopen` | +| Node/Core backend restart | Новое runtime поколение, сохранённая pairing/history; никакого replay команд | #76 heartbeat + separate K1 restart checkpoint | +| Restart активной K1 acquisition | Только доказательная rehydration; новый writer/gap, first PCL admission; старое evidence неизменно | `tests/test_xgrids_active_acquisition_restart_rehydration.py:326`; 22 тест-функции в файле; Node E2E ещё не принят | +| STOP ACK есть, READY нет | Не объявлять завершение; standby-unconfirmed/unknown, read-only reconciliation | #49; `test_stop_response_preserves_precise_unknown_outcome_through_predeadline_loss` | +| Принудительное локальное завершение | Закрыть только локальных владельцев; не посылать STOP; поздний recovery не оживляет их | `test_local_force_finish_cleanup_failure_is_visible_retryable_and_fences_late_success` | +| Две вкладки / поздний REST / смена mode | Старый runtime/revision не перетирает новый, команда не дублируется | `stateOrdering.ts`, lifecycle.ts; #3 REST/WS acceptance | +| Browser refresh/cache reset/закрытие | Не влияет на физическую команду и recorder; UI заново читает backend | #49/#76; CONN-62; отдельная clean-cache GUI проверка обязательна | +| Rerun не открылся, камера/данные живы | Viewer-only recovery; не переподключать K1 и не терять raw | #49 listener/admission; #3 live_receiver recovery | +| Камера пропала, MQTT жив | Camera-only recovery точного source/evidence epoch | `test_camera_stall_snapshot_does_not_reconnect_live_mqtt_runtime`, camera watchdog tests | +| Live consumer медленный / worker недоступен | Ограничивать preview, сохранять source-of-record и не менять scanner authority | #10, #66, #76; Node sustained-resource gate открыт | +| Переключение live → Data → LAB | Разные admission/lifecycle/settings policies; исправление Bridge не меняет эти профили | #66; `viewerProfile.ts`, отдельный аудит live camera root | + +Восстановление нельзя свести к правилу «никаких повторов»: запрещены автоматические физические mutations, но ограниченные read-only наблюдения и consumer-only reconnect нужны и уже приняты. Для active-stream exception backoff задан 0,5/1/2/4/5 с с пределом интервала; автоматического terminal timeout нет. Exact READY/SCAN_OVER/fault, изменение lineage и явное локальное завершение имеют разные исходы. Источник: `docs/20_K1_CONNECTION_SUPERVISION_CANON.md:574`. + +## 7. Конкретные проблемы и риски текущей структуры + +### F1 — Перегруженный coordinator и зависимость проекции от lifecycle + +**Подтверждено статически.** `facade.py` соединяет protocol, OS/network, durable stores, process/native leases, acquisition/camera, recovery, ошибки и UI policy. `connect()` содержит Bridge, Direct, Quick, host association, reset/retirement и child bootstrap. В `state()` совмещены чтение и упорядоченное локальное завершение. + +Практический риск: изменение unrelated presentation/polling влияет на момент cleanup или recovery; вынесенный callback может поменять порядок захвата gate и публикации proof. Размер файла сам по себе не обосновывает удаление guard. Первый допустимый structural шаг — выделение неизменных типов/проекций и изолированных функций с сохранением вызовов и их порядка. + +### F2 — Нижний и верхний уровни по-разному заканчивают station observation + +**Подтверждённое расхождение критериев; полевой причинный статус открыт.** `wifi_provisioning.py:725` завершает polling при любом адресе, кроме `None` и AP fallback. Верхний слой `facade.py:11792` проверяет requested network и допускаемый post-dispatch target. Если первая post-write выборка ещё описывает прежнюю LAN, helper больше не ждёт следующую, даже при оставшемся 45-second budget. + +Для исправления потребуется единый reviewed terminal predicate либо передаваемый transport-слою observation criterion. До изменения нужен сценарий «старая LAN → переходный status → запрошенная LAN» с одной записью и несколькими чтениями. Это не объяснение сегодняшнего ATT4: ATT-исключение возникает на write, до этого polling. + +### F3 — UI повторно собирает смысл операции и скрывает полезный контекст + +**Подтверждено кодом и пользовательскими скриншотами.** Экран держит отдельные local attempt, candidate-unavailable, saved reconnect, applied recovery и physical recovery representations; один failure записывается в несколько presentation slots. Ошибка дублируется, а SSID из `ProvisioningAttemptPresentation` не показан в итоговой ошибке. Primary reconnect фактически выполняет проверку существующего состояния, а исправление сети требует другого пути. + +Из успешного private evidence известна подтверждённая сеть; на раннем скриншоте введено другое написание. SSID последней неуспешной операции не сохранён, поэтому обвинять конкретно последний ввод нельзя. UI должен позволять проверить собственный ввод в текущем локальном intent, без публикации SSID в Ops/общие логи и без восстановления password. Требуется согласовать это с прежней формулировкой secret-free error contract, которая запрещает вставлять SSID из backend exception. + +Кандидат на упрощение: одна typed presentation projection из server attempt + текущего локального draft, один операторский error и один явно названный следующий шаг. Backend policy остаётся authority. + +### F4 — Новый Node-путь теряет часть уже существующего recovery-контракта + +**Подтверждено статически; аппаратный Node/K1 запуск не выполнен.** `NodeBridge` переиспользует общий service — это правильная основа. Но `project()` (`node_bridge.py:55`) экспортирует краткие `connected`, `ready_to_start`, `phase`, candidates и runtime; отсутствуют exact connection attempt, typed failure, safe-next-action, child bootstrap progress и snapshot revision. + +`network.provision` возвращает durable network ACK до завершения read-only bootstrap. `DeviceEnrollmentWindow.tsx` получает один projected result и не подписывается на последующее enrollment state; он может показать «связь пока не подтверждена», хотя bootstrap продолжает работать. Это не обязательно отказ соединения. Verify требует `device_id` в текущих candidates (`node_bridge.py:116`): cold saved reconnect после restart не эквивалентен принятому LAB пути. + +Python `/operation` сворачивает все exceptions в 409 с одной фразой; Go `call()` заменяет non-200 ещё одной общей ошибкой, а `execute()` классифицирует её как unknown. Это безопасно по запрету replay, но стирает различие между stale-before-dispatch, отказом устройства и unknown-after-dispatch. Backend typed diagnostics, улучшенные в LAB, не доходят до Node UI. + +Не следует копировать 3 732-строчный LAB-компонент в Node. Нужно довести общий typed operation/recovery контракт через транспортные оболочки и оставить две компактные поверхности над одной семантикой. + +### F5 — Несогласованные deadlines между Node и K1 runtime + +**Подтверждено статически; воспроизведение не проводилось.** Node UI выдаёт request deadline 170 с; Core/Go допускают до 180 с; Go HTTP client имеет 185 с, но сам вызов ограничен command deadline; общий `network.provision` journal задаёт 240 с. Timeout доставки может наступить до terminal outcome нижнего уровня. Политика unknown/no-auto-retry корректна, но оператору недоступно полноценное наблюдение той же операции после таймаута через урезанную проекцию. + +Нужно разделять срок допуска до отправки, ожидание transport-ответа и наблюдение принятой операции. Увеличить все timeout не решает владение и корреляцию. + +### F6 — Матрица приёмки и freezes плохо отражают актуальную систему + +**Подтверждено.** Manifest ссылается в основном на целые test files, а не на конкретный test/scenario/physical run. Один lifecycle файл содержит 446 test-функций. `planned` в старом manifest не доказывает отсутствие реализации сегодня; `software-covered` не доказывает Node parity. + +Guardrail закреплён на `c041a569...` и защищает packet oracle/frozen paths. Он полезен как защита от случайного protocol drift, но не как единственный критерий нового рефакторинга. Отдельные сегодняшние source changes уже выходят за старый файл-freeze. Нельзя просто переснять hashes, объявив поведение сохранённым. + +### F7 — Профильность Rerun частично отделяет lifecycle, но не всю кастомизацию + +**Подтверждено текущим кодом и предыдущим аудитом.** Есть разные live/recorded/LAB profile kinds и remount boundary. Но в Control Station общий `App.tsx:183` хранит `sceneSettings`; live и Saved Sessions используют общий канал настроек, тогда как LAB имеет отдельные result settings. Поэтому формулировка «все три профиля полностью изолированы» сейчас слишком сильна. + +Это самостоятельный долг, не основание трогать viewer в рефакторинге Bridge. Протокол подключения, live camera producer и presentation-профиль нужно принимать отдельно. Исправление camera evidence-root сегодня также не является изменением Wi-Fi протокола. + +## 8. Обязательные границы будущего рефакторинга + +Это предложение для последующего решения, не начатая реализация. + +1. Сохранить текущий рабочий snapshot и сопоставить каждый обязательный scenario с точным existing test, physical run и платформой. Отдельно перечислить Node-only проверки. Исторические версии Ops не переписывать как новую приёмку. +2. Выделить один typed outcome: `not_dispatched`, `network_applied`, `control_ready`, `outcome_unknown`, конкретный отказ; не выводить результат из HTTP-кода. Во всех оболочках сохранять operation/runtime/target correlation и допустимое действие. +3. Разделить normal Bridge provisioning и recovery orchestration. Нормальный путь может быть коротким; recovery обязан сохранять explicit target, physical ledger и ownership. Quick остаётся отдельной принятой strategy; его не переносить на борт и не удалять из LAB. +4. Разделить state projection и reconciliation owner, только после фиксации существующего порядка переходов и lock ownership. Переносить по одной обязанности, без одновременного изменения protocol, camera и viewer. +5. Упростить UI на основе общей семантики результата. Компактность достигается уменьшением повторной интерпретации, а не скрытием unknown или заменой всех отказов словом «подключение». +6. После каждого узкого изменения — соответствующая автоматическая регрессия, затем отдельный физический UI-сценарий. По указанию владельца перед каждым аппаратным/UI-прогоном очистить cache; обычный reload не засчитывать. Сейчас очищенный прогон не выполнен: browser tool не предоставляет доступную очистку. +7. Производительность измерять на одном и том же сценарии и ledger: click→BLE discovery, connect, write-return, status proof, network ACK, route/TCP/DeviceInfo, START, first PCL, first camera. У каждой стадии свой бюджет; таймер ожидания без стадии недостаточен. + +Неприкосновенны: exact wire frame/command order, одна mutation на intent, durability до dispatch, запрет command replay, identity/runtime/host-epoch fences, отделение record от preview, gaps при restart, reader-only recovery и сохранение других Rerun профилей. Менять эти контракты можно только отдельным обоснованным решением, а не попутно при разрезании файлов. + +## 9. Что пока нельзя утверждать + +- Причина всех сегодняшних ATT-ошибок не установлена единым доказательством. FW-specific interpretation ATT4/6 полезна, но не заменяет exact entered-network evidence последнего intent. +- Найденный ранний выход polling — статически установленный риск другого этапа; он не был воспроизведён на физическом K1. +- Холодный reboot БК на несколько минут во время K1 capture не принят этим аудитом. В коде есть restart rehydration, в Ops принят heartbeat/recovery на отдельных конфигурациях; их Linux end-to-end композиция требует своей приёмки. +- Все 77 scenario не перепроверены аппаратно и все тесты не перезапущены. Список нужен для предотвращения регрессии, а не для новой зелёной отметки. +- Рефакторинг не начат. Нет изменения пакета, установки на Mini, restart сервиса или публикации в Ops. + +## 10. Проверяемые источники + +- [Полный индекс Ops](2026-09-06-k1-bridge-ops-index.json): все 76 карточек, 41 K1 label, 128 полученных активных комментариев; основные semantic источники указаны выше. +- [Hashes текущего кода](2026-09-06-k1-bridge-code-snapshot.json): точные bytes критических модулей и test-файлов на момент чтения, без proprietary evidence или credentials. +- [LixelGO IP protocol observation](../lab/002_LIXELGO_IPHONE_LOCAL_PROTOCOL_20260716.redacted.md), [Wi-Fi profile](../04_K1_WIFI_PROVISIONING_PROFILE.md). +- [Connection supervision canon](../20_K1_CONNECTION_SUPERVISION_CANON.md), [acceptance manifest](../k1-connection-acceptance.manifest.json), [recovery runbook](../runbooks/K1_CONNECTION_RECOVERY.md), [physical recovery ADR](../adr/0015-k1-physical-state-recovery.md). +- [Bridge incident](2026-09-06-k1-bridge-connection-incident.md), [station reply semantics](2026-09-06-k1-station-reply-semantics.md), [live camera root](2026-09-06-k1-live-reference-camera-root.md), [Node Bridge implementation and open gates](2026-09-06-node-k1-bridge-implementation.md). + +Чтение Ops и исходников позволило восстановить рабочие гарантии и найти конкретные границы риска. Следующее решение должно выбирать одну такую границу и её приёмку; переписывание всего K1-контура сразу не имеет достаточного доказательного основания. + +## 11. Уточнение цели: необязательная интеграция устройства и переносимость + +Дополнительный запрос владельца: K1 должен быть необязательной интеграцией; macOS — первая принимаемая платформа, Ubuntu — следующая проверка переносимости. Другие модели XGRIDS не должны требовать встраивания их протоколов в Core. Ниже — архитектурное предложение, не новая реализация или аппаратная приёмка. + +### Что уже отделено, а что ещё нет + +- Есть manifest `DevicePlugin`, отдельный frontend пакета `plugins/xgrids-k1`, backend в `src/k1link/device_plugins/xgrids_k1`, нейтральный SDK и загрузчик с проверкой версии, набора действий и handshake. Это реальная существующая основа, которую следует сохранить. +- [Composition frontend](../../apps/control-station/src/composition/devicePlugins.ts) явно включает K1 в сборку. Это правильное место выбора поставляемых модулей, но сейчас выбор статический; независимо устанавливаемый frontend этим не доказан. +- [Backend composition](../../src/k1link/web/device_plugin_composition.py) допускает только `transitional-in-process`. [ADR 0011](../adr/0011-laboratory-plugin-runtime-handshake-and-transport-seam.md) прямо исключает из текущих гарантий crash containment, supervisor, portable media IPC и установку пакетов. Наличие runtime transport не означает, что отдельный процесс уже работает. +- [Общий pyproject](../../pyproject.toml) включает BLE/MQTT-зависимости и K1 CLI. Общий Python wheel содержит весь `src/k1link`. Поэтому независимое удаление драйвера пока нельзя считать принятой возможностью. +- [SensorWorkspace](../../packages/sensor-ui/src/SensorWorkspace.tsx) напрямую импортирует `K1Detail` и выбирает его по `device.kind === 'k1'`. Это конкретная зависимость общей поверхности от устройства; её место — регистрация визуального расширения интеграцией. +- Анализ Python import graph обнаружил 14 модулей `compute`, прямо импортирующих K1 analysis/protocol/replay. Это главным образом работа с данными, а не управление радио. Их нельзя механически удалять вместе с драйвером: нужны отдельные границы чтения архивов и проверка LAB. +- [NodeBridge](../../src/k1link/device_plugins/xgrids_k1/node_bridge.py) уже использует тот же compatibility service с Linux-адаптерами. Следовательно, второй реализации всей K1-логики сейчас нет и создавать её не требуется. Однако полноценная Ubuntu-приёмка и общий контракт проекции результата ещё не завершены. + +Размер и связность `facade.py` — проблема внутреннего устройства плагина. Прямые зависимости общей установки, сенсорного UI и вычислительных модулей — проблема его внешней границы. Перенос одного большого файла в новую папку не решит ни ту, ни другую автоматически. Историческое имя Python namespace `k1link` само по себе не является доказательством зависимости от оборудования. + +### Предлагаемая ответственность + +| Слой | Ответственность | +| --- | --- | +| Mission Core / Node host | Реестр интеграций и execution node, разрешения, общий жизненный цикл операций, хранение evidence, маршрутизация нормализованных потоков, оболочка UI и профили просмотра. | +| K1 domain | Точный протокол BLE/MQTT/RTSP, совместимость модели и прошивки, порядок подключения и acquisition, интерпретация ответов, восстановление и доказательства физического состояния K1. | +| Адаптер платформы | BLE backend, наблюдение сетевого интерфейса и маршрута, доступ к секретам, запуск и остановка локального runtime. Реализации macOS и Ubuntu могут различаться. | +| Представление интеграции | Поля подключения, возможности, настройки и статусы K1 через общий SDK; регистрация в host вместо веток K1 в общих компонентах. | +| Чтение данных | K1 raw codec как явно объявленная зависимость чтения; нормализованные сохранённые данные читаются без активного драйвера оборудования. Совместимость существующих LAB проверяется отдельно. | + +Переносимую логику разумно сохранить на текущем Python, отделяя платформенные вызовы через небольшие интерфейсы. Требование переносимости относится к поведению и контракту, а не к одному бинарнику для всех ОС. SDK уже экспортирует JSON Schema; смена языка возможна позднее при доказанной необходимости, но сейчас добавила бы повторную проверку протокола и recovery. + +Целевая граница исполнения — отдельный runtime интеграции на том компьютере, рядом с которым находится устройство: локально для LAB, на БК для бортового сценария. Core обращается к выбранному execution node; Wi-Fi оператора не подменяет Wi-Fi около БК. Контракт должен покрывать не только команды, но и события операции, состояние, evidence и media. Объявление permissions в manifest не заменяет их фактическое ограничение. + +Плагин имеет общий протокол взаимодействия с host, но собственную семантику устройства. Core не должен знать BLE UUID или порядок команд K1. И наоборот, плагин не должен владеть профилем лабораторного Rerun, глобальной навигацией или реестром аппаратов. Другие модели XGRIDS получают явные model/firmware profiles; общие vendor-компоненты выделяются по подтверждённому совпадению поведения, а не по одному бренду. Обобщение на одновременную работу нескольких устройств — отдельная приёмка, не свойство текущего single-session runtime. + +### Последовательность и критерии завершения + +1. Закрепить существующие сценарии и одинаковый контракт результата/событий для LAB и Node. Получение HTTP-ответа и физическое завершение операции остаются разными событиями. +2. Выделить внутри K1 provisioning, наблюдение/recovery и проекцию состояния, сохранив durable dispatch boundary, владельца операции, блокировки и порядок команд. Не пересобирать одновременно acquisition, камеру и Rerun. +3. Устранить прямые зависимости общих компонентов, разделить поставку драйвера и чтение записей. Проверить запуск Core без установленного K1 и работу других устройств и доступных архивов. +4. Реализовать процессную границу за существующим transport с явными контрактами событий/media. Проверить зависание и падение плагина: Core остаётся доступным, состояние устройства честно меняется, другая запись не прерывается. Перезапуск runtime не переотправляет provisioning, START или STOP автоматически; сначала восстанавливаются журнал и наблюдаемое состояние. +5. Принять macOS, затем Ubuntu Bridge на том же domain-коде. Перенос считается доказанным, когда меняются адаптеры и упаковка, а не K1-логика или Core. Допустимо начать с явного состава сборки; динамическая установка UI, магазин плагинов и hot reload не нужны для первой проверки границы. + +Отдельные обязательные критерии: восстановление связи после потери сети, безопасное обнаружение рестарта устройства и БК, отсутствие повторной физической команды, отсутствие регрессии LAB/recorded/live профилей Rerun. Каждый аппаратный прогон выполняется через UI с предварительной очисткой браузерного кэша по указанию владельца. В этом дополнении выполнены только чтение кода и документирование; проверок на устройстве не было. diff --git a/docs/audits/2026-09-06-k1-bridge-code-snapshot.json b/docs/audits/2026-09-06-k1-bridge-code-snapshot.json new file mode 100644 index 0000000..1e3f277 --- /dev/null +++ b/docs/audits/2026-09-06-k1-bridge-code-snapshot.json @@ -0,0 +1,108 @@ +{ + "schema_version": "missioncore.bridge-analysis-code-snapshot/v1", + "base_commit": "020a878915ea32c64963975447650fd8eee29071", + "working_tree": "dirty; source code not changed by this analysis", + "analysis": "Static inspection and existing evidence only; no tests executed", + "files": [ + { + "path": "src/k1link/device_plugins/xgrids_k1/facade.py", + "sha256": "24c4ec6c33c4d21a99065c4146df12bdc2d81efd5a9da769c077b1ab4bea896d", + "lines": 35996 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/connection_supervisor.py", + "sha256": "2adbd7d61c34816d5be9a66baa788634408df7dc0e4a0d21520241d5e965806a", + "lines": 2252 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/ble/wifi_provisioning.py", + "sha256": "f169a78f66e71d9c85d503e174d5f19f19e010aa04db3fe3ba8cf032d809de55", + "lines": 818 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/ble/scanner.py", + "sha256": "ea6f1fed6db5b3b98735779e14237e0eaf40a3cbe2d0cd8f3196e95308c12153", + "lines": 1306 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/protocol/application_session.py", + "sha256": "f91367b19b3f85ce2b47279acbdeff969cd5a10b64bc5ceb90b843840a1e6b26", + "lines": 1930 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/node_bridge.py", + "sha256": "550f34cf5b947131ca1067a1dbe74731ca7a7601cb42bbb5a9c1a08ebfd9179b", + "lines": 248 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/linux_host.py", + "sha256": "c2e3fa6884e97c9cbd7bbf29cfb1260e539f61fd2e7d18e0dc3c1ab94c59d4d4", + "lines": 214 + }, + { + "path": "src/k1link/fleet/device_enrollment.py", + "sha256": "9856fd5638aaef88317ec82fa482b6e358efb5fd57deecf61fb9e9d0e14c4fa4", + "lines": 173 + }, + { + "path": "apps/node-agent/internal/node/device_enrollment.go", + "sha256": "9e92379ed9e262d6ee4b16c754df7852d129bf6daec324140e85a4b15af9c5e0", + "lines": 323 + }, + { + "path": "plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx", + "sha256": "d6205ce8be004238791471fb7c3eb2eccb0676bedb81a06514771c26419cc0ab", + "lines": 3732, + "hook_mentions": { + "useState": 17, + "useEffect": 13, + "useRef": 10 + } + }, + { + "path": "plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts", + "sha256": "dcc0c1e5ca1c841e963ddc18d24628799ec97d036bc1095eb6f22124cf000565", + "lines": 2135 + }, + { + "path": "plugins/xgrids-k1/frontend/src/networkProvisioning.ts", + "sha256": "45ba4eaf111b1cf8fa0945884aaf95e3920a274cd1d7f0e4797f8da2ee7fd204", + "lines": 241 + }, + { + "path": "packages/sensor-ui/src/DeviceEnrollmentWindow.tsx", + "sha256": "de02c9443f606bd0d73ec476c34f095c05851005497426431271dbe1ba36f064", + "lines": 49, + "hook_mentions": { + "useState": 10, + "useEffect": 1, + "useRef": 1 + } + }, + { + "path": "packages/sensor-ui/src/enrollment.ts", + "sha256": "ad97fbe9ecc1b6567d8ee67d6d32c4cc39a31ce95bf0eb48ef9cbc40a884a16f", + "lines": 37 + }, + { + "path": "docs/k1-connection-acceptance.manifest.json", + "sha256": "477615cdd34685bc10d9815de0c9642fbdfcdea2eb05e04b5e07b7c87a2e9854", + "lines": 93 + }, + { + "path": "tests/test_xgrids_acquisition_lifecycle.py", + "sha256": "97728c489551f16a6ccf2606a19ca077f5a2926adc67681f367666188df008ef", + "lines": 34794 + }, + { + "path": "tests/test_xgrids_active_acquisition_restart_rehydration.py", + "sha256": "3fc85fba32eab54fda12625ac77d54f7e3ae8a454708b4167b71e0ffd4271d61", + "lines": 2482 + }, + { + "path": "tests/test_node_k1_bridge.py", + "sha256": "25af4bd6d40b8df487b231e599730117b62598a78b838d20e5443395920c2c48", + "lines": 204 + } + ] +} diff --git a/docs/audits/2026-09-06-k1-bridge-connection-incident.md b/docs/audits/2026-09-06-k1-bridge-connection-incident.md new file mode 100644 index 0000000..95c6f9d --- /dev/null +++ b/docs/audits/2026-09-06-k1-bridge-connection-incident.md @@ -0,0 +1,140 @@ +# K1 Bridge: provisioning recovery and first-scan discovery + +Scope: the operator-local K1 connection in LAB on canonical Core `8000`. +The onboard installation is a separate pending acceptance task. + +The later [firmware callback audit](2026-09-06-k1-station-reply-semantics.md) +explains the two fresh 19:22/19:23 UTC failures and supersedes the earlier +generic ATT interpretation below with a bounded FW 3.0.2 station diagnosis. + +## Evidence and diagnosis + +Three explicit Bridge attempts returned HTTP 502 on 2026-09-06 at +18:04:50, 18:05:13 and 18:05:43 UTC. All failed at `gatt-write` with +`BleakGATTProtocolError`, ATT 4 `INVALID_PDU`. Each attempted one 99-byte +write-with-response; acknowledgement and a joined Wi-Fi address were absent. +The characteristic advertised `read, write`; the observed write-without-response +capacity was 253 bytes. This capacity alone does not prove a successful ATT write. + +A separately admitted, exact-target Bridge `connection.verify` at 18:15 UTC +read K1 state without a provisioning write or host Wi-Fi switch. K1 answered, +but supplied no shared-LAN address (`connection-verify-address-unavailable`). +A preceding request was rejected at input validation because of the diagnostic +operation identifier format; it performed no device I/O. + +The same ATT error exists in August's private logs. The accepted August Bridge +record uses the same Bleak 3.0.2, write mode, frame length and capacity. Successful +historical writes also include an empty network baseline, so an empty baseline +does not justify adding an AP-enable command or rejecting Bridge in advance. +The exact reason for the peripheral's rejection remains unproved; a wrong Wi-Fi +password, frame-format regression or MTU failure must not be asserted from this +error alone. No alternate transport mode, frame, retry, START or STOP was sent. + +Private evidence is retained under the ignored incident directory, with UTC and +monotonic timestamps, redacted events and SHA-256 artifact index. Credentials and +raw packet contents are absent from this report and source changes. + +## Software causes and refactor + +- The Connect hook had three overlapping result/error branches. Its failed + HTTP path accepted the server snapshot but returned `observedState: null` to + the form; the form therefore lost the exact operation and its ATT diagnosis. +- A completed Bluetooth search suppressed the recovery surface even after the + subsequent, click-owned Connect failed. The disabled credential form instead + promised a future safe continuation despite there being no running operation. +- Recovery guidance claimed an automatic recovery or a completed UI reset that + had not occurred. The lead status fell back to idle despite a terminal failure. +- Durable JSON logging omitted the write-mode, frame-size and GATT-property + fields already supplied by the BLE implementation. + +`networkProvisioning.ts` now owns the single submission, bounded observation of +that exact idempotency key, monotonic snapshot selection and reviewed failure +copy. An existing operation is only observed, never resubmitted. The runtime +hook retains exact applied-network/control-authority checks and React action +ownership. The form renders the returned operation and allows explicit recovery +after its failed attempt even when its preceding search is complete. It still +clears the credential immediately upon submission. + +The frozen BLE frames and canonical MQTT dialogue are unchanged. Ops reference: +MISSIONCOR-3, “Mission Core. Lixel K1 / XGRIDS Integration”, and +`docs/lab/002_LIXELGO_IPHONE_LOCAL_PROTOCOL_20260716.redacted.md`. +That iPhone capture begins at the IP stack and does not contain Bluetooth HCI. +The reviewed BLE profile remains `docs/04_K1_WIFI_PROVISIONING_PROFILE.md`. + +## Validation and remaining acceptance + +Focused frontend: 227 passed, including new behavioral cases for HTTP 502, +existing-key non-replay, lost-response success, pending settlement, stale REST +versus newer WebSocket, superseded intent and unrelated journal rows. The +rendered regression covers completed search followed by failed Connect and +requires enabled recovery choices instead of the trapped form. + +Full frontend suite: 772 passed sequentially; TypeScript and final production build passed. Application architecture: 4 passed. + +BLE and persistent-diagnostic tests: 34 passed. Canonical guardrail: four +immutable LixelGO captures verified in their original checkout, frozen +protocol/Bridge contour unchanged, 32 synthetic sentinels passed. The convenience +script initially failed because raw captures are deliberately absent from the +active worktree; its original-location integrity check passed without copying +or changing the captures. Ruff and diff checks passed. + +A later runtime journal records an explicit successful Bridge write at 18:42 UTC, +followed by application-control acceptance, canonical START and the first point +frame, then a confirmed STOP at 18:44 UTC. These actions occurred while the +assistant was editing/testing discovery; they were not dispatched by this +investigation. The success followed the recovery-UI refactor and preceded the +new discovery implementation. It proves a subsequent successful physical +connection, not the cause or permanent resolution of the intermittent ATT error. +Do not automatically replay the earlier failed attempts. + +## First-scan miss + +The operator reported that an already active K1 is absent from the first search +and appears on the second. This was reproduced on the original six-second +implementation: 18:32 UTC returned four devices and zero K1 candidates; a second +explicit scan at 18:35 UTC returned five devices including the expected K1. +These observations do not distinguish radio advertisement latency from macOS +state and do not establish that the camera was powered off. + +Discovery now opens one native Bleak scanner context. It listens for an initial +six-second window and, if no K1 name has appeared, continues the same context +up to a twenty-second bound, stopping on a later K1 candidate. There is no hidden +second scan, GATT connection, provisioning retry or cache-based candidate +promotion. Name matching only ends discovery; compatibility and connection +still require their separate evidence. The owner arbiter, generation revocation, +native-handle capture and cancellation cleanup remain in place. A caller's +explicit shorter duration is respected. + +The shared frontend request/countdown, backend default and Node Bridge source +use the twenty-second bound. Completed operations and private structured logs +now include scanner startup, total elapsed time, first-candidate time and whether +the initial window was extended. They contain no Wi-Fi credentials. + +After the new canonical process started, the first explicit UI search at +18:51 UTC found the expected K1. Native startup took 766 ms, first K1 detection +2023 ms from scanner construction, and total discovery 6768 ms. Extension was +not needed. The browser showed the fresh candidate and the arbiter returned idle. +This is one successful process-restart test; the browser cache was not cleared. +The operator requested a separate test after browser-cache clearing. That +acceptance is pending: the available in-app browser automation exposes no cache +clearing capability, and the clear-browsing-data keyboard shortcut had no effect. +The operator was asked to clear it; no cache clearing is claimed. + +Additional discovery validation: 73 backend tests passed (scanner, owner arbiter, +persistent diagnostics and Node Bridge), seven facade discovery tests passed, +138 focused frontend tests passed, and architecture checks passed. TypeScript +and production build passed. Final focused discovery checks, Ruff, whitespace +and the frozen protocol/Bridge comparison passed. The previously completed full +772-test frontend pass applies to the recovery refactor; only affected suites +were rerun for the subsequent discovery change. + +Canonical Core remains on port 8000; no listener exists on 8765. Onboard source +is synchronized separately; this investigation does not install or rebuild the +pending Node package. + +## Ops publication + +The existing Ops canon was read. Automatic approval review rejected the attempted +card update because consultation was authorized but publication of internal +technical details was not. No Ops card was changed; this local report is the +reviewable result pending explicit publication authorization. diff --git a/docs/audits/2026-09-06-k1-bridge-ops-index.json b/docs/audits/2026-09-06-k1-bridge-ops-index.json new file mode 100644 index 0000000..d4bc0ed --- /dev/null +++ b/docs/audits/2026-09-06-k1-bridge-ops-index.json @@ -0,0 +1,6720 @@ +{ + "schema_version": "missioncore.bridge-analysis-source-index/v1", + "retrieved_at_utc": "2026-09-06T20:15:45Z", + "source": "NODE.DC Ops direct tasker MCP; project MISSION CORE", + "project_id": "1c1fd1de-b6e8-4d45-9937-af5c4fae138b", + "total_cards": 76, + "k1_label_cards": 41, + "note": "All project card descriptions and structured blocks retrieved. K1-labelled cards indexed, including downstream LAB consumers. Deep semantic review focuses on Bridge, control, recovery, Node and viewer boundaries. Comments retrieved for 45 cards (128 active comments), all returned pages exhausted. No card was changed. Older card statements are dated evidence, not automatically current requirements.", + "cards": [ + { + "key": "MISSIONCOR-1", + "id": "0af3ccdb-5b68-491c-abb5-4a46ee3bc7a8", + "title": "Mission Core. Архитектурный план", + "updated_at": "2026-08-05T06:37:45.470160+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "arch_current", + "title": "Текущая архитектура", + "type": "text" + }, + { + "id": "arch_lifecycle", + "title": "Канонический lifecycle K1", + "type": "text" + }, + { + "id": "arch_archive", + "title": "Архив и воспроизведение", + "type": "text" + }, + { + "id": "arch_worker", + "title": "External AI worker", + "type": "text" + }, + { + "id": "arch_technology", + "title": "Architecture gate после канона", + "type": "text" + }, + { + "id": "arch_limits", + "title": "Открытые архитектурные границы", + "type": "text" + }, + { + "id": "arch_delivery", + "title": "Current delivery state", + "type": "text" + }, + { + "id": "arch_checker", + "title": "Архитектурный gate", + "type": "checker" + }, + { + "id": "audit-roadmap-20260726", + "title": "Архитектурный аудит — критический путь A0–A9", + "type": "text" + }, + { + "id": "a3-implementation-20260726", + "title": "A3 E30 camera-backed evidence — реализация 2026-07-27", + "type": "text" + }, + { + "id": "a3-product-ui-20260726", + "title": "A3 product surface и component boundary", + "type": "text" + }, + { + "id": "a3-engineering-generation-20260727", + "title": "A3 AI-assisted engineering generation — 2026-07-27", + "type": "text" + }, + { + "id": "a3-validation-20260726", + "title": "A3 validation и фактический runtime", + "type": "text" + }, + { + "id": "a4-e31-plan-20260727", + "title": "A4 / E31 — принятый source qualification profile", + "type": "text" + }, + { + "id": "audit-a3-checker-20260726", + "title": "Аудит — gates A0–A8", + "type": "checker" + }, + { + "id": "a5-trackgeometry-plan-20260727", + "title": "A5 / TrackGeometry v1 — принятый contract gate", + "type": "text" + }, + { + "id": "a6-e32-plan-20260727", + "title": "A6 / E32 — принятый full-replay result", + "type": "text" + }, + { + "id": "a6-e32-accounting-20260727", + "title": "A6 / E32 — закрытое accounting и ограничения", + "type": "text" + }, + { + "id": "a7-e33-plan-20260727", + "title": "A7 / E33 — принятый worker-shadow result", + "type": "text" + }, + { + "id": "a7-e33-source-20260727", + "title": "A7 / E33 — source evidence и immutable chain", + "type": "text" + }, + { + "id": "a7-e33-method-runtime-20260727", + "title": "A7 / E33 — метод, worker и runtime", + "type": "text" + }, + { + "id": "a7-e33-validation-20260727", + "title": "A7 / E33 — реализация и validation", + "type": "text" + }, + { + "id": "a7-e33-resources-limits-20260727", + "title": "A7 / E33 — resources, regressions и ограничения", + "type": "text" + }, + { + "id": "a7-e33-decision-20260727", + "title": "A7 / E33 — decision и authority", + "type": "text" + }, + { + "id": "a7-e33-acceptance-20260727", + "title": "A7 / E33 — acceptance checker", + "type": "checker" + }, + { + "id": "a8-e34-e35-plan-20260727", + "title": "A8 / E34+E35 — следующий critical-path gate", + "type": "text" + }, + { + "id": "a8-e34-result-20260727", + "title": "A8 / E34 — принятый temporal-layer result", + "type": "text" + }, + { + "id": "a8-e34-correction-20260727", + "title": "A8 / E34 — fail-closed history и архитектурная коррекция", + "type": "text" + }, + { + "id": "a8-e34-product-validation-20260727", + "title": "A8 / E34 — product surface и validation", + "type": "text" + }, + { + "id": "a8-e34-decision-20260727", + "title": "A8 / E34 — решение и authority", + "type": "text" + }, + { + "id": "a8-e35-result-20260727", + "title": "A8 / E35 — принятый degradation/recovery result", + "type": "text" + }, + { + "id": "a8-e35-safety-20260727", + "title": "A8 / E35 — safe-state accounting", + "type": "text" + }, + { + "id": "a8-e35-product-validation-20260727", + "title": "A8 / E35 — product surface и validation", + "type": "text" + }, + { + "id": "a8-e35-decision-20260727", + "title": "A8 / E35 — решение и переход к A9", + "type": "text" + } + ], + "comments": [ + { + "id": "667d933d-0bcb-4764-af39-38ad435e3679", + "created_at": "2026-07-24T12:15:00.798290+00:00", + "updated_at": "2026-07-24T12:15:00.798316+00:00" + }, + { + "id": "5d101c19-eb96-4e97-9e61-ee22c5942c16", + "created_at": "2026-07-27T07:52:56.067761+00:00", + "updated_at": "2026-07-27T07:52:56.067819+00:00" + }, + { + "id": "0edb4744-4782-4101-b00f-a3cccbabd168", + "created_at": "2026-07-27T08:03:58.584770+00:00", + "updated_at": "2026-07-27T08:03:58.584799+00:00" + }, + { + "id": "4bee5a94-439d-4ecd-9d6b-efbf690dbb51", + "created_at": "2026-07-27T08:35:13.805794+00:00", + "updated_at": "2026-07-27T08:35:13.805822+00:00" + }, + { + "id": "3bad1856-c6c9-4eae-85be-39d36fee0924", + "created_at": "2026-07-27T08:48:14.696527+00:00", + "updated_at": "2026-07-27T08:48:14.696556+00:00" + }, + { + "id": "d3382111-e65f-4a38-9b63-0f21a28d7c49", + "created_at": "2026-07-27T09:52:21.241585+00:00", + "updated_at": "2026-07-27T09:52:21.241612+00:00" + }, + { + "id": "b3bdc6fb-2d21-4867-9640-2f870b49e9fb", + "created_at": "2026-07-27T10:46:28.786656+00:00", + "updated_at": "2026-07-27T10:46:28.786681+00:00" + }, + { + "id": "4dbdcb84-ff18-473d-8274-b036f344bce2", + "created_at": "2026-07-27T13:59:20.158741+00:00", + "updated_at": "2026-07-27T13:59:20.158778+00:00" + }, + { + "id": "a6b320c3-30d1-440d-bb13-ed87b0738e27", + "created_at": "2026-07-27T14:58:23.028807+00:00", + "updated_at": "2026-07-27T14:58:23.028837+00:00" + }, + { + "id": "664ead4b-b608-41d5-9c3f-d181d77a0e99", + "created_at": "2026-07-27T15:24:15.859230+00:00", + "updated_at": "2026-07-27T15:24:15.859267+00:00" + }, + { + "id": "3d4e320c-c371-4dbc-9494-6071e05e125f", + "created_at": "2026-07-27T15:27:05.105492+00:00", + "updated_at": "2026-07-27T15:27:05.105521+00:00" + }, + { + "id": "c3279041-964c-4b47-bfaf-2cb3d106de07", + "created_at": "2026-07-27T16:37:09.498689+00:00", + "updated_at": "2026-07-27T16:37:09.498713+00:00" + }, + { + "id": "d3adf4c9-7bb1-43ea-b7e9-10f221cb66fb", + "created_at": "2026-07-27T16:39:53.402582+00:00", + "updated_at": "2026-07-27T16:39:53.402609+00:00" + }, + { + "id": "0d3372d1-3740-4431-a854-4a0db2c9a200", + "created_at": "2026-08-05T06:55:43.163700+00:00", + "updated_at": "2026-08-05T06:55:43.163724+00:00" + }, + { + "id": "4128ca5a-a045-4368-ae14-427647c588de", + "created_at": "2026-08-05T06:56:04.480486+00:00", + "updated_at": "2026-08-05T06:56:04.480514+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-2", + "id": "3e4d7128-ca62-408c-bdf8-820435a79a89", + "title": "Mission Core. Status Card", + "updated_at": "2026-08-27T12:49:06.738298+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "status-current-stack", + "title": "Текущий доказанный pipeline", + "type": "text" + }, + { + "id": "status-m49-isolated", + "title": "M4.9 · полный изолированный TGS shadow", + "type": "text" + }, + { + "id": "status-m49-integrated", + "title": "M4.9 · интегрированный graph + TGS load gate", + "type": "text" + }, + { + "id": "status-lab-playback", + "title": "LAB · sealed spatial playback", + "type": "text" + }, + { + "id": "status-runtime-recovery", + "title": "Runtime и восстановление процессов", + "type": "text" + }, + { + "id": "status-delivery", + "title": "Доставка и worktree truth", + "type": "text" + }, + { + "id": "status-open-gates", + "title": "Что честно остаётся открытым", + "type": "text" + }, + { + "id": "status-history-20260719", + "title": "Исторический status snapshot · 2026-07-19", + "type": "text" + }, + { + "id": "status-checker", + "title": "Current acceptance status", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-3", + "id": "9b75d0cd-4c55-44a6-ada3-558587991be2", + "title": "Mission Core. Lixel K1 / XGRIDS Integration", + "updated_at": "2026-08-23T13:26:26.407630+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "k1_status_20260823", + "title": "Текущий статус — рабочий beta-профиль", + "type": "text" + }, + { + "id": "k1_acceptance_scope", + "title": "Принятая граница продукта", + "type": "text" + }, + { + "id": "k1_lixelgo_capture_oracle", + "title": "Эталонный сетевой дамп LixelGO", + "type": "text" + }, + { + "id": "k1_internal_latency_baselines", + "title": "Внутренние эталоны Mission Core", + "type": "text" + }, + { + "id": "k1_control_protocol", + "title": "Канонический control transcript", + "type": "text" + }, + { + "id": "k1_topology_and_intent", + "title": "Topology, intent и переключение режимов", + "type": "text" + }, + { + "id": "k1_quick_connect_contract", + "title": "Quick Connect: BLE, AP и CoreWLAN", + "type": "text" + }, + { + "id": "k1_connection_and_ui", + "title": "Connection reducer и операторский интерфейс", + "type": "text" + }, + { + "id": "k1_camera", + "title": "Правая камера и live presentation", + "type": "text" + }, + { + "id": "k1_calibration_loss", + "title": "Потеря устройства во время калибровки", + "type": "text" + }, + { + "id": "k1_sleep_wake_recovery", + "title": "Сон ноутбука и потеря сети после START", + "type": "text" + }, + { + "id": "k1_performance_and_durability", + "title": "Durability и hot-path performance", + "type": "text" + }, + { + "id": "k1_safety", + "title": "Safety boundaries", + "type": "text" + }, + { + "id": "k1_code_map", + "title": "Карта реализации", + "type": "text" + }, + { + "id": "k1_validation_20260823", + "title": "Validation и physical acceptance", + "type": "text" + }, + { + "id": "k1_limits", + "title": "Оставшиеся ограничения", + "type": "text" + }, + { + "id": "k1_acceptance", + "title": "K1 connection acceptance", + "type": "checker" + } + ], + "comments": [ + { + "id": "5e4d4d0f-8c64-4683-bb85-3cf57b448039", + "created_at": "2026-07-16T16:14:00.516099+00:00", + "updated_at": "2026-07-16T16:14:00.516124+00:00" + }, + { + "id": "89257a3b-e558-459b-8267-163801375b61", + "created_at": "2026-07-16T16:14:02.686126+00:00", + "updated_at": "2026-07-16T16:14:02.686154+00:00" + }, + { + "id": "9bc177f2-a14d-4f56-93f7-f140bc4da3bf", + "created_at": "2026-07-16T16:14:04.423044+00:00", + "updated_at": "2026-07-16T16:14:04.423071+00:00" + }, + { + "id": "fbb01817-5de5-4a24-89d8-041ef9fcd665", + "created_at": "2026-07-16T16:23:50.651362+00:00", + "updated_at": "2026-07-16T16:23:50.651388+00:00" + }, + { + "id": "c1ef70e2-d900-4d42-ad78-d460040a42d7", + "created_at": "2026-07-16T16:32:19.074060+00:00", + "updated_at": "2026-07-16T16:32:19.074084+00:00" + }, + { + "id": "b8ecb12c-ca25-4797-8640-b8c59b5d4284", + "created_at": "2026-07-16T16:34:14.131984+00:00", + "updated_at": "2026-07-16T16:34:14.132021+00:00" + }, + { + "id": "18da84e8-29da-4c08-9a33-2419214c5b23", + "created_at": "2026-07-18T10:54:47.237027+00:00", + "updated_at": "2026-07-18T10:54:47.237060+00:00" + }, + { + "id": "118e160d-e1d5-4915-8e9a-eabe00de6683", + "created_at": "2026-07-18T11:14:53.625872+00:00", + "updated_at": "2026-07-18T11:14:53.625900+00:00" + }, + { + "id": "d83b3dda-f002-42de-8ff8-b06b0f3b928c", + "created_at": "2026-07-18T11:38:15.140055+00:00", + "updated_at": "2026-07-18T11:38:15.140083+00:00" + }, + { + "id": "ce49f737-d738-44f5-9194-690d97688de0", + "created_at": "2026-07-18T12:18:44.774628+00:00", + "updated_at": "2026-07-18T12:18:44.774657+00:00" + }, + { + "id": "1c0b373e-dc96-4818-9382-41fb9e12f431", + "created_at": "2026-07-19T18:26:10.365515+00:00", + "updated_at": "2026-07-19T18:26:10.365543+00:00" + }, + { + "id": "56a86710-a2af-430f-b93b-52cd53aa2a7e", + "created_at": "2026-07-19T22:42:11.741362+00:00", + "updated_at": "2026-07-19T22:42:11.741387+00:00" + }, + { + "id": "2d56730f-2e09-469e-a200-3db6bec34569", + "created_at": "2026-08-22T10:11:17.814574+00:00", + "updated_at": "2026-08-22T10:11:17.814603+00:00" + }, + { + "id": "3b4378a1-1653-49e2-9ce4-e8b0cf5eae7e", + "created_at": "2026-08-22T13:18:49.870086+00:00", + "updated_at": "2026-08-22T13:18:49.870115+00:00" + }, + { + "id": "467c6747-2e7e-42a0-9beb-fe3a05dc79e9", + "created_at": "2026-08-23T13:26:25.111906+00:00", + "updated_at": "2026-08-23T13:26:25.111934+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-4", + "id": "23fed5bd-86c0-47d8-804e-28d3d88a720d", + "title": "Archive. NDC_xgrids-k1-connector — historical evidence", + "updated_at": "2026-07-16T14:08:06.297447+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "legacy_role", + "title": "Назначение архива", + "type": "text" + }, + { + "id": "legacy_truth", + "title": "Исторически подтверждённые факты", + "type": "text" + }, + { + "id": "legacy_metrics", + "title": "Исторический live evidence", + "type": "text" + }, + { + "id": "legacy_retired", + "title": "Что больше не является актуальным", + "type": "text" + }, + { + "id": "legacy_preservation", + "title": "Как сохранена историческая правда", + "type": "text" + }, + { + "id": "legacy_checker", + "title": "Архивная классификация", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-5", + "id": "ea07dd70-a290-4824-b43b-26d2dc8594f1", + "title": "Mission Core. Milestone — Plugin SDK v0alpha2 и Architecture Gate", + "updated_at": "2026-07-16T14:03:10.283501+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "ms_scope", + "title": "Результат milestone", + "type": "text" + }, + { + "id": "ms_sdk", + "title": "Plugin SDK v0alpha2", + "type": "text" + }, + { + "id": "ms_domain", + "title": "Локальная доменная модель", + "type": "text" + }, + { + "id": "ms_profile", + "title": "Manifest и compatibility profile", + "type": "text" + }, + { + "id": "ms_lifecycle", + "title": "Lifecycle, операции и секреты", + "type": "text" + }, + { + "id": "ms_data", + "title": "Canonical data plane", + "type": "text" + }, + { + "id": "ms_frontend", + "title": "Frontend и операторские gates", + "type": "text" + }, + { + "id": "ms_files", + "title": "Ключевые области реализации", + "type": "text" + }, + { + "id": "ms_validation", + "title": "Верификация", + "type": "text" + }, + { + "id": "ms_evidence", + "title": "Offline regression реального evidence", + "type": "text" + }, + { + "id": "ms_limits", + "title": "Честные ограничения", + "type": "text" + }, + { + "id": "ms_next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "ms_checker", + "title": "Milestone acceptance", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-6", + "id": "c202c4a6-8396-4e97-8429-05ad9c6f6051", + "title": "Mission Core. Архив наблюдения и Rerun playback", + "updated_at": "2026-07-18T22:12:41.530984+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "archive_current", + "title": "Текущий archive lifecycle", + "type": "text" + }, + { + "id": "archive_delete", + "title": "Удаление и исправленный race", + "type": "text" + }, + { + "id": "archive_physical", + "title": "Физическое acceptance", + "type": "text" + }, + { + "id": "archive_ux", + "title": "UX архива", + "type": "text" + }, + { + "id": "archive_limits", + "title": "Ограничения", + "type": "text" + }, + { + "id": "archive_delivery", + "title": "Доставка и проверки", + "type": "text" + }, + { + "id": "archive_checker", + "title": "Archive acceptance", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-7", + "id": "0eeb2b00-eedb-4b0f-9b34-a29bfd7901ca", + "title": "Mission Core. Milestone — canonical K1 control and durable archive acceptance", + "updated_at": "2026-07-18T22:13:45.979131+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "milestone_result", + "title": "Результат milestone", + "type": "text" + }, + { + "id": "milestone_protocol", + "title": "Принятый control dialogue", + "type": "text" + }, + { + "id": "milestone_physical", + "title": "Физическое свидетельство", + "type": "text" + }, + { + "id": "milestone_archive", + "title": "Архив и media evidence", + "type": "text" + }, + { + "id": "milestone_fixes", + "title": "Закрытые дефекты", + "type": "text" + }, + { + "id": "milestone_safety", + "title": "Safety boundaries", + "type": "text" + }, + { + "id": "milestone_validation", + "title": "Регрессия и визуальная приёмка", + "type": "text" + }, + { + "id": "milestone_delivery", + "title": "Доставка", + "type": "text" + }, + { + "id": "milestone_limits", + "title": "Граница утверждения", + "type": "text" + }, + { + "id": "milestone_checker", + "title": "Milestone acceptance", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-8", + "id": "ef110d2b-3542-42c9-9492-10e518725849", + "title": "Mission Core. Edge perception: K1 → RTX 4090 → Rerun", + "updated_at": "2026-08-21T19:05:29.938503+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "review_verdict", + "title": "Вердикт по исходному документу", + "type": "text" + }, + { + "id": "actual_baseline", + "title": "Фактическая база после recorded vertical", + "type": "text" + }, + { + "id": "known_limits", + "title": "Доказанные ограничения", + "type": "text" + }, + { + "id": "physical-calibration-20260720", + "title": "Физическая заводская калибровка K1", + "type": "text" + }, + { + "id": "target_topology", + "title": "Принятая целевая топология", + "type": "text" + }, + { + "id": "technology_boundary", + "title": "Граница технологий", + "type": "text" + }, + { + "id": "minimal_contract", + "title": "Минимальный межмашинный контракт", + "type": "text" + }, + { + "id": "storage_policy", + "title": "Запись и производные данные", + "type": "text" + }, + { + "id": "p0-overlay-experiment-20260720", + "title": "P0 recorded overlay — первый физический результат", + "type": "text" + }, + { + "id": "acceptance", + "title": "Текущее состояние acceptance", + "type": "text" + }, + { + "id": "phase_zero", + "title": "A. Инвентаризация 4090 и воспроизводимый runtime", + "type": "checker" + }, + { + "id": "phase_recorded", + "title": "B. Сохранённая вертикаль", + "type": "checker" + }, + { + "id": "phase_live", + "title": "C. Live, деградация и внешний маршрут", + "type": "checker" + }, + { + "id": "phase_decisions", + "title": "D. Архитектурные ворота после вертикали", + "type": "checker" + }, + { + "id": "phase_calibrated_perception", + "title": "E. Calibrated panoptic + 3D vertical", + "type": "checker" + }, + { + "id": "p0-segmentation-fusion-20260720", + "title": "P0 recorded segmentation, distance и Boxes3D", + "type": "text" + }, + { + "id": "phase-recorded-segmentation-probe", + "title": "F. Recorded segmentation/fusion probe", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-9", + "id": "ef3597cd-221f-4b6c-a6a0-864b2cb15206", + "title": "Mission Core AI Server Worker — RTX 4090", + "updated_at": "2026-07-19T18:20:47.735264+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "worker-boundary", + "title": "Роль и граница authority", + "type": "text" + }, + { + "id": "worker-host", + "title": "Живой host inventory 2026-07-19", + "type": "text" + }, + { + "id": "worker-substrate", + "title": "WSL2 и Docker substrate", + "type": "text" + }, + { + "id": "worker-layout", + "title": "Файловая и Git-раскладка", + "type": "text" + }, + { + "id": "worker-ssh", + "title": "SSH, сеть и firewall", + "type": "text" + }, + { + "id": "worker-triton", + "title": "Активный Triton deployment", + "type": "text" + }, + { + "id": "worker-startup", + "title": "Запуск и процессы", + "type": "text" + }, + { + "id": "worker-model", + "title": "Принятый model profile", + "type": "text" + }, + { + "id": "worker-acceptance", + "title": "Recorded acceptance", + "type": "text" + }, + { + "id": "worker-rerun", + "title": "Возврат результата в Mission Core", + "type": "text" + }, + { + "id": "worker-cotenancy", + "title": "Resource co-tenancy", + "type": "text" + }, + { + "id": "worker-limits", + "title": "Честная граница принятия", + "type": "text" + }, + { + "id": "worker-checks", + "title": "Acceptance и следующие gates", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-10", + "id": "0620b66c-a6ea-47a6-8e6b-0469c2bd892d", + "title": "Operational Core — Real-time Record Limits", + "updated_at": "2026-08-08T07:47:01.713873+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "purpose", + "title": "Назначение и статус документа", + "type": "text" + }, + { + "id": "canonical-flow", + "title": "Канонический поток и владение данными", + "type": "text" + }, + { + "id": "record-duration", + "title": "Длительность записи и реальные terminal conditions", + "type": "text" + }, + { + "id": "mqtt", + "title": "MQTT: LiDAR, pose и telemetry", + "type": "text" + }, + { + "id": "camera", + "title": "Камера: RTSP, FFmpeg и fMP4 archive", + "type": "text" + }, + { + "id": "rerun-live", + "title": "Live Rerun: память, очередь и смысл деградации", + "type": "text" + }, + { + "id": "rrd-cache", + "title": "Derived scene.rrd: диск, подготовка и открытие", + "type": "text" + }, + { + "id": "recorded-video", + "title": "Recorded video: браузер и Range delivery", + "type": "text" + }, + { + "id": "perception-worker", + "title": "External perception / AI worker — текущий принятый профиль", + "type": "text" + }, + { + "id": "durability", + "title": "Durability, recovery и retention", + "type": "text" + }, + { + "id": "rav-sizing", + "title": "Фактическое sizing по RAVNOVES00", + "type": "text" + }, + { + "id": "rav-physical-acceptance-20260720", + "title": "Физическая приёмка RAVNOVES00 — 20.07.2026", + "type": "text" + }, + { + "id": "failure-matrix", + "title": "Матрица деградации", + "type": "text" + }, + { + "id": "risks", + "title": "Открытые архитектурные риски", + "type": "text" + }, + { + "id": "evidence", + "title": "Кодовые источники и правила актуализации", + "type": "text" + }, + { + "id": "acceptance", + "title": "Обязательные проверки перед длительной миссией", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-11", + "id": "323b91f7-56fd-4712-bc16-fcf16b769218", + "title": "K1 — Заводская калибровка и P0-проекция", + "updated_at": "2026-07-20T12:13:41.060047+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "status", + "title": "Текущий статус", + "type": "text" + }, + { + "id": "physical-evidence", + "title": "Физическое evidence устройства", + "type": "text" + }, + { + "id": "source-mapping", + "title": "Каноническое соответствие камер", + "type": "text" + }, + { + "id": "transform-contract", + "title": "Геометрический контракт", + "type": "text" + }, + { + "id": "read-safety", + "title": "Read-only и fail-closed граница", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и проверки", + "type": "text" + }, + { + "id": "experiment", + "title": "Следующий recorded-эксперимент", + "type": "text" + }, + { + "id": "p0-checker", + "title": "P0 acceptance", + "type": "checker" + }, + { + "id": "segmentation-fusion-evidence", + "title": "Калибровка в mask→LiDAR fusion", + "type": "text" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-12", + "id": "bcd9e684-98f2-486b-93c4-6363fc2bcf87", + "title": "Mission Core — M4.8S RF-DETR semantics + reference graph", + "updated_at": "2026-08-25T19:46:31.004084+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "semantic-status", + "title": "Текущий статус", + "type": "text" + }, + { + "id": "semantic-boundary", + "title": "Архитектурная граница", + "type": "text" + }, + { + "id": "semantic-accepted", + "title": "Что уже принято", + "type": "text" + }, + { + "id": "semantic-load-prerequisite", + "title": "Закрытый prerequisite", + "type": "text" + }, + { + "id": "semantic-open", + "title": "Открытый вопрос", + "type": "text" + }, + { + "id": "semantic-next", + "title": "Следующий этап — M4.8T semantic quality + temporal identity", + "type": "text" + }, + { + "id": "semantic-checker", + "title": "M4.8S / следующий gate", + "type": "checker" + } + ], + "comments": [ + { + "id": "a5bccb8e-024b-4f94-951b-1057a93d2370", + "created_at": "2026-08-06T08:31:37.922516+00:00", + "updated_at": "2026-08-06T08:31:37.922572+00:00" + }, + { + "id": "50b5842a-6310-4886-a209-34ff354a190e", + "created_at": "2026-08-25T13:57:29.357792+00:00", + "updated_at": "2026-08-25T13:57:29.357819+00:00" + }, + { + "id": "f2b939f2-d9d4-490b-b9e7-754ba6a2fa3e", + "created_at": "2026-08-25T14:52:23.315172+00:00", + "updated_at": "2026-08-25T14:52:23.315197+00:00" + }, + { + "id": "c3b74dea-808c-45ce-a390-e58c6eb611f3", + "created_at": "2026-08-25T15:15:44.106345+00:00", + "updated_at": "2026-08-25T15:15:44.106372+00:00" + }, + { + "id": "436c790a-12d1-4da1-9dd1-05fb54f679bd", + "created_at": "2026-08-25T16:01:11.625258+00:00", + "updated_at": "2026-08-25T16:01:11.625288+00:00" + }, + { + "id": "d1d81bd0-1430-4b2c-9c7e-ec43f546a1fa", + "created_at": "2026-08-25T19:40:37.133346+00:00", + "updated_at": "2026-08-25T19:40:37.133373+00:00" + }, + { + "id": "82a1e516-74c8-4d39-b32a-bcfc97ede9e8", + "created_at": "2026-08-25T19:46:42.820447+00:00", + "updated_at": "2026-08-25T19:46:42.820472+00:00" + }, + { + "id": "d7ddeca7-50f1-40c3-87f0-a7b2e5e1f843", + "created_at": "2026-08-25T23:26:12.556665+00:00", + "updated_at": "2026-08-25T23:26:12.556692+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-13", + "id": "8f4d2e9f-7fdf-4b9b-a727-0c732e9d259b", + "title": "Mission Core — Perception Run Reports", + "updated_at": "2026-07-20T14:10:18.463405+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "report-contract", + "title": "Контракт отчёта прогона", + "type": "text" + }, + { + "id": "publication-contract", + "title": "Правила публикации", + "type": "text" + }, + { + "id": "ravnoves00-baseline", + "title": "RAVNOVES00 · baseline 2026-07-20", + "type": "text" + }, + { + "id": "acceptance", + "title": "Готовность вертикального среза", + "type": "checker" + } + ], + "comments": [ + { + "id": "38f6dc5e-ce8f-49af-9b53-d21c6f10a24a", + "created_at": "2026-07-20T14:10:56.456803+00:00", + "updated_at": "2026-07-20T14:10:56.456827+00:00" + }, + { + "id": "6fa937ad-b249-4d7e-bf34-77d74b01a740", + "created_at": "2026-07-20T15:38:50.292627+00:00", + "updated_at": "2026-07-20T15:38:50.292663+00:00" + }, + { + "id": "81d8e835-7a5e-451b-bd27-a839f4f6b485", + "created_at": "2026-07-20T15:46:50.718434+00:00", + "updated_at": "2026-07-20T15:46:50.718470+00:00" + }, + { + "id": "0c110d5d-8d8e-42b8-86d9-b4b98db68d1c", + "created_at": "2026-07-20T16:16:27.807523+00:00", + "updated_at": "2026-07-20T16:16:27.807550+00:00" + }, + { + "id": "ecfd4705-d555-493d-8acd-31d34045d999", + "created_at": "2026-07-20T16:32:13.987552+00:00", + "updated_at": "2026-07-20T16:32:13.987577+00:00" + }, + { + "id": "ecc7a513-41b7-42c8-98e2-cf8e8c01b871", + "created_at": "2026-07-20T16:56:43.438776+00:00", + "updated_at": "2026-07-20T16:56:43.438810+00:00" + }, + { + "id": "43c342bf-8318-4563-9203-511dd2d4246e", + "created_at": "2026-07-20T18:10:30.815121+00:00", + "updated_at": "2026-07-20T18:10:30.815149+00:00" + }, + { + "id": "a4b9ed7c-aa4f-45ac-99e2-42e2081b2750", + "created_at": "2026-07-20T19:04:27.538593+00:00", + "updated_at": "2026-07-20T19:04:27.538624+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-14", + "id": "624613ef-95ae-4447-92e7-e5c73df3bf18", + "title": "LAB E0 · 2026-07-20 · RAVNOVES00 · Panoptic 2D + diagnostic 3D baseline", + "updated_at": "2026-07-20T15:46:24.314122+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "e0_identity", + "title": "Идентичность и статус эксперимента", + "type": "text" + }, + { + "id": "e0_input", + "title": "Входные данные", + "type": "text" + }, + { + "id": "e0_calibration", + "title": "Калибровка, камера и синхронизация", + "type": "text" + }, + { + "id": "e0_worker", + "title": "Вычислительный контур", + "type": "text" + }, + { + "id": "e0_models", + "title": "Модели и точная конфигурация", + "type": "text" + }, + { + "id": "e0_execution", + "title": "Фактическая схема исполнения", + "type": "text" + }, + { + "id": "e0_timing", + "title": "Хронометраж", + "type": "text" + }, + { + "id": "e0_resources", + "title": "Нагрузка и аппаратные пределы наблюдения", + "type": "text" + }, + { + "id": "e0_2d", + "title": "Результат 2D и признаки качества", + "type": "text" + }, + { + "id": "e0_3d", + "title": "3D fusion и диагностические кубы", + "type": "text" + }, + { + "id": "e0_artifacts", + "title": "Immutable-артефакты и контрольные суммы", + "type": "text" + }, + { + "id": "e0_limits", + "title": "Лимиты и их фактическое влияние", + "type": "text" + }, + { + "id": "e0_failures", + "title": "Хронология сбоев и защита от повторения", + "type": "text" + }, + { + "id": "e0_qa", + "title": "Проверка интерфейса и сборки", + "type": "text" + }, + { + "id": "e0_conclusion", + "title": "Вывод E0 и контракт сравнения", + "type": "text" + }, + { + "id": "e0_acceptance", + "title": "Завершённость лабораторного отчёта E0", + "type": "checker" + } + ], + "comments": [ + { + "id": "e0831f24-a5d5-4b5d-8fce-83f070b67315", + "created_at": "2026-07-20T14:51:10.082394+00:00", + "updated_at": "2026-07-20T14:51:10.082422+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-15", + "id": "f6265db0-cc83-4734-8b45-d52a79ae23f6", + "title": "LAB E1 · 2026-07-20 · RAVNOVES00 · Valid-FOV mask + crop A/B/C", + "updated_at": "2026-07-20T15:46:27.719628+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "e1_identity", + "title": "Идентичность и статус эксперимента", + "type": "text" + }, + { + "id": "e1_objective", + "title": "Вопрос эксперимента", + "type": "text" + }, + { + "id": "e1_input", + "title": "Immutable вход и qualification slice", + "type": "text" + }, + { + "id": "e1_mask", + "title": "Каноническая valid-FOV mask", + "type": "text" + }, + { + "id": "e1_execution", + "title": "Контролируемая схема A/B/C", + "type": "text" + }, + { + "id": "e1_software", + "title": "Software и кодовые поколения", + "type": "text" + }, + { + "id": "e1_result", + "title": "Результат и артефакты", + "type": "text" + }, + { + "id": "e1_performance", + "title": "Скорость по стадиям", + "type": "text" + }, + { + "id": "e1_quality", + "title": "Прокси качества без ground truth", + "type": "text" + }, + { + "id": "e1_resources", + "title": "Ресурсы worker", + "type": "text" + }, + { + "id": "e1_failures", + "title": "Непубликованные попытки и fail-closed", + "type": "text" + }, + { + "id": "e1_decision", + "title": "Решение E1", + "type": "text" + }, + { + "id": "e1_next", + "title": "Следующий лабораторный контур E2", + "type": "text" + }, + { + "id": "e1_validation", + "title": "Валидация реализации", + "type": "text" + }, + { + "id": "e1_acceptance", + "title": "Завершённость E1", + "type": "checker" + } + ], + "comments": [ + { + "id": "9119da37-06c3-4ead-b63b-cff69bf60ce7", + "created_at": "2026-07-20T15:38:48.991576+00:00", + "updated_at": "2026-07-20T15:38:48.991606+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-16", + "id": "ae045c01-af96-4d84-955f-15a1eeaf4602", + "title": "LAB E2 · 2026-07-20 · RAVNOVES00 · Evaluation pack + annotation gate", + "updated_at": "2026-07-20T16:56:07.727466+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "e2_identity", + "title": "Идентичность и статус", + "type": "text" + }, + { + "id": "e2_objective", + "title": "Цель эксперимента", + "type": "text" + }, + { + "id": "e2_input", + "title": "Вход и наследование", + "type": "text" + }, + { + "id": "e2_review", + "title": "Candidate review и отбор", + "type": "text" + }, + { + "id": "e2_coverage", + "title": "Покрытие сцен", + "type": "text" + }, + { + "id": "e2_calibration", + "title": "Калибровка и preprocessing", + "type": "text" + }, + { + "id": "e2_artifact", + "title": "Канонический immutable pack", + "type": "text" + }, + { + "id": "e2_contract", + "title": "Контракт разметки", + "type": "text" + }, + { + "id": "e2_metrics", + "title": "Метрики и критерии", + "type": "text" + }, + { + "id": "e2_execution", + "title": "Исполнение и ресурсы", + "type": "text" + }, + { + "id": "e2_implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "e2_validation", + "title": "Валидация", + "type": "text" + }, + { + "id": "e2_superseded", + "title": "Superseded preparation generation", + "type": "text" + }, + { + "id": "e2_annotation_workspace", + "title": "CVAT annotation workspace", + "type": "text" + }, + { + "id": "e2_prelabel_identity_roundtrip", + "title": "Prelabel identity round-trip defect", + "type": "text" + }, + { + "id": "e2_limits", + "title": "Что E2 уже доказал и чего ещё нет", + "type": "text" + }, + { + "id": "e2_next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "e2_acceptance", + "title": "Завершённость LAB E2", + "type": "checker" + } + ], + "comments": [ + { + "id": "50fdb9e0-2735-41d3-858a-086ae78520de", + "created_at": "2026-07-20T16:31:51.313811+00:00", + "updated_at": "2026-07-20T16:31:51.313835+00:00" + }, + { + "id": "8fb356ae-639e-4079-927a-dccea65ae775", + "created_at": "2026-07-20T16:56:38.804024+00:00", + "updated_at": "2026-07-20T16:56:38.804059+00:00" + }, + { + "id": "34dbc784-c60f-41b7-8346-ee29075e124a", + "created_at": "2026-07-20T18:10:13.327477+00:00", + "updated_at": "2026-07-20T18:10:13.327504+00:00" + }, + { + "id": "5d0ffb3a-0c46-4ae2-bad0-0e675db205ff", + "created_at": "2026-07-29T11:13:24.031944+00:00", + "updated_at": "2026-07-29T11:13:24.031969+00:00" + }, + { + "id": "cdeb88db-3726-4be1-8421-66a9c6480c8a", + "created_at": "2026-07-29T11:35:15.519935+00:00", + "updated_at": "2026-07-29T11:35:15.519961+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-17", + "id": "0e55bdfe-e019-4283-8cfa-198eea732e6c", + "title": "LAB E3 — K1 rectified segmentation baseline", + "updated_at": "2026-07-20T19:03:47.161454+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "scope", + "title": "Контур и immutable inputs", + "type": "text" + }, + { + "id": "architecture", + "title": "Фактическая конфигурация", + "type": "text" + }, + { + "id": "model", + "title": "Model/software identity", + "type": "text" + }, + { + "id": "result", + "title": "Результат и производительность", + "type": "text" + }, + { + "id": "resources", + "title": "Worker resources и D-only storage", + "type": "text" + }, + { + "id": "quality", + "title": "Integrity, visual audit и ограничения", + "type": "text" + }, + { + "id": "provenance", + "title": "Зафиксированный legacy-дефект LAB E2", + "type": "text" + }, + { + "id": "decision", + "title": "Решение и следующий gate", + "type": "text" + }, + { + "id": "checklist", + "title": "Гейты LAB E3", + "type": "checker" + } + ], + "comments": [ + { + "id": "9fe586de-a720-498e-b01c-3f83bfe808f3", + "created_at": "2026-07-20T19:04:09.143710+00:00", + "updated_at": "2026-07-20T19:04:09.143738+00:00" + }, + { + "id": "cd82713d-78fd-4b1b-a2e1-6c838f59467c", + "created_at": "2026-07-20T19:13:53.950283+00:00", + "updated_at": "2026-07-20T19:13:53.950308+00:00" + }, + { + "id": "c26df492-7884-491c-a4ca-3373173b995b", + "created_at": "2026-07-20T19:15:28.572577+00:00", + "updated_at": "2026-07-20T19:15:28.572603+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-18", + "id": "aec5dc5b-d6c4-490e-b43b-f109cd3d22ef", + "title": "LAB E4 — Full-session EoMT playback", + "updated_at": "2026-07-21T07:37:13.766726+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель и границы", + "type": "text" + }, + { + "id": "baseline", + "title": "Зафиксированный baseline", + "type": "text" + }, + { + "id": "input-result", + "title": "Вход и immutable result", + "type": "text" + }, + { + "id": "timings", + "title": "Фактическая производительность", + "type": "text" + }, + { + "id": "resources", + "title": "GPU, память и сервисы", + "type": "text" + }, + { + "id": "storage", + "title": "Storage и безопасность", + "type": "text" + }, + { + "id": "artifacts", + "title": "Артефакты и целостность", + "type": "text" + }, + { + "id": "ui-proof", + "title": "Публикация и реальный UI proof", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "limitations", + "title": "Ограничения результата", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "gates", + "title": "Гейты LAB E4", + "type": "checker" + } + ], + "comments": [ + { + "id": "00bebb0b-d940-43c1-9452-fbae0b74efc0", + "created_at": "2026-07-21T07:37:03.289969+00:00", + "updated_at": "2026-07-21T07:37:03.290003+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-19", + "id": "c6834891-5ef5-47d8-8b67-02b92c3490e1", + "title": "LAB E5 — Instance tracking qualification", + "updated_at": "2026-07-21T08:53:32.139334+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель, результат и границы", + "type": "text" + }, + { + "id": "provenance", + "title": "Вход и калибровочная привязка", + "type": "text" + }, + { + "id": "configuration", + "title": "Зафиксированный detector/tracker config", + "type": "text" + }, + { + "id": "pilots", + "title": "Пилот и freeze конфигурации", + "type": "text" + }, + { + "id": "run_metrics", + "title": "Основной qualification run", + "type": "text" + }, + { + "id": "quality", + "title": "Качество и честные ограничения", + "type": "text" + }, + { + "id": "resources", + "title": "GPU, storage и сервисная безопасность", + "type": "text" + }, + { + "id": "artifacts", + "title": "Artifacts и независимая проверка", + "type": "text" + }, + { + "id": "decision", + "title": "Решение и LAB E6 gate", + "type": "text" + }, + { + "id": "gates", + "title": "Гейты LAB E5", + "type": "checker" + } + ], + "comments": [ + { + "id": "15a5bf4e-75ca-4dc8-a6df-1dd720a93349", + "created_at": "2026-07-21T08:53:30.406348+00:00", + "updated_at": "2026-07-21T08:53:30.406379+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-20", + "id": "a6cd59f4-6bba-43f0-af52-438519a428b2", + "title": "LAB E10 — Integrated real-time perception qualification", + "updated_at": "2026-07-22T05:44:47.411919+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель и принятый результат", + "type": "text" + }, + { + "id": "input", + "title": "Вход, камера и калибровка", + "type": "text" + }, + { + "id": "configuration", + "title": "Зафиксированный pipeline и планировщик", + "type": "text" + }, + { + "id": "metrics", + "title": "Производительность и fusion", + "type": "text" + }, + { + "id": "resources", + "title": "GPU, память, диск и сервисы", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и независимая проверка", + "type": "text" + }, + { + "id": "ui", + "title": "Mission Core / Rerun proof", + "type": "text" + }, + { + "id": "limits", + "title": "Честные ограничения", + "type": "text" + }, + { + "id": "gates", + "title": "Гейты LAB E10", + "type": "checker" + } + ], + "comments": [ + { + "id": "bc95fc89-9052-4541-a87b-412aea0dcfcf", + "created_at": "2026-07-22T05:45:05.280375+00:00", + "updated_at": "2026-07-22T05:45:05.280402+00:00" + }, + { + "id": "5c4522dd-806d-49a7-ae24-07c37e6e1dc0", + "created_at": "2026-07-22T06:32:46.735323+00:00", + "updated_at": "2026-07-22T06:32:46.735349+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-21", + "id": "d88acd79-501f-4383-b963-36273886d4bb", + "title": "LAB E10-N1 — Semantic-worker loss control", + "updated_at": "2026-07-22T05:44:50.669677+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель и ответ", + "type": "text" + }, + { + "id": "configuration", + "title": "Failure injection и неизменные входы", + "type": "text" + }, + { + "id": "continuity", + "title": "Непрерывность detector и world state", + "type": "text" + }, + { + "id": "freshness", + "title": "Freshness и bounded queue accounting", + "type": "text" + }, + { + "id": "fusion", + "title": "Fail-closed LiDAR fusion", + "type": "text" + }, + { + "id": "resources", + "title": "Ресурсы и storage safety", + "type": "text" + }, + { + "id": "evidence", + "title": "Immutable result и независимая проверка", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения и следующий gate", + "type": "text" + }, + { + "id": "gates", + "title": "Гейты LAB E10-N1", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-22", + "id": "6e345cc9-0d9a-4ca7-9508-c68c74870074", + "title": "LAB E11 — Full-session integrated perception stability", + "updated_at": "2026-07-22T06:32:16.594997+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель эксперимента", + "type": "text" + }, + { + "id": "inputs", + "title": "Входные данные и калибровка", + "type": "text" + }, + { + "id": "lidar-pack", + "title": "Полный LiDAR replay pack", + "type": "text" + }, + { + "id": "configuration", + "title": "Замороженная конфигурация", + "type": "text" + }, + { + "id": "acceptance", + "title": "Acceptance gates", + "type": "checker" + }, + { + "id": "results", + "title": "Принятый результат", + "type": "text" + }, + { + "id": "resources", + "title": "Ресурсы worker", + "type": "text" + }, + { + "id": "disk", + "title": "Диск и безопасность", + "type": "text" + }, + { + "id": "artifacts", + "title": "Артефакты и отчёт", + "type": "text" + }, + { + "id": "limitations", + "title": "Границы доказательства", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "checker" + }, + { + "id": "preflight-history", + "title": "Контролируемый preflight reject", + "type": "text" + } + ], + "comments": [ + { + "id": "e253dafe-2eee-43d7-9291-98f7e3c7c460", + "created_at": "2026-07-22T07:05:47.730338+00:00", + "updated_at": "2026-07-22T07:05:47.730375+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-23", + "id": "2a4ea843-4070-4d18-b100-81160b0163c8", + "title": "LAB E12 — Bounded shadow transport to AI worker", + "updated_at": "2026-07-22T07:05:46.033880+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель эксперимента", + "type": "text" + }, + { + "id": "architecture", + "title": "Реализованная архитектура", + "type": "text" + }, + { + "id": "input", + "title": "Вход и идентичность", + "type": "text" + }, + { + "id": "acceptance", + "title": "Acceptance gates", + "type": "checker" + }, + { + "id": "nominal", + "title": "Номинальный результат", + "type": "text" + }, + { + "id": "negative", + "title": "Worker-disconnect negative control", + "type": "text" + }, + { + "id": "security", + "title": "Transport и безопасность", + "type": "text" + }, + { + "id": "artifacts", + "title": "Артефакты и отчёт", + "type": "text" + }, + { + "id": "limits", + "title": "Границы доказательства", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "checker" + } + ], + "comments": [ + { + "id": "65134762-b5d9-4b09-881f-0c718003d535", + "created_at": "2026-07-22T07:57:54.047049+00:00", + "updated_at": "2026-07-22T07:57:54.047075+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-24", + "id": "c50224dd-36e1-41d0-9aa8-442e6c9281ab", + "title": "LAB E13 — Class-aware amodal 3D cuboids", + "updated_at": "2026-07-22T07:57:39.063326+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "outcome", + "title": "Результат", + "type": "text" + }, + { + "id": "input", + "title": "Вход и выборка", + "type": "text" + }, + { + "id": "architecture", + "title": "Геометрический контур", + "type": "text" + }, + { + "id": "identity", + "title": "Конфигурация и воспроизводимость", + "type": "text" + }, + { + "id": "metrics", + "title": "Near-live метрики", + "type": "text" + }, + { + "id": "geometry_metrics", + "title": "Геометрия и стабильность", + "type": "text" + }, + { + "id": "ab", + "title": "A/B итерации", + "type": "text" + }, + { + "id": "rejections", + "title": "Fail-closed accounting", + "type": "text" + }, + { + "id": "artifacts", + "title": "Артефакты", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения", + "type": "text" + }, + { + "id": "next", + "title": "LAB E14 — следующий gate", + "type": "checker" + }, + { + "id": "done", + "title": "Проверки E13", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-25", + "id": "4192e198-7a2c-407d-8db8-a04a6adb6ca6", + "title": "LAB E14 — Full-session recorded near-live qualification", + "updated_at": "2026-07-22T16:20:38.403132+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "outcome", + "title": "Результат", + "type": "text" + }, + { + "id": "input", + "title": "Вход и выборка", + "type": "text" + }, + { + "id": "configuration", + "title": "Конфигурация и воспроизводимость", + "type": "text" + }, + { + "id": "timing", + "title": "Время выполнения", + "type": "text" + }, + { + "id": "latency", + "title": "Latency и очереди", + "type": "text" + }, + { + "id": "geometry", + "title": "3D geometry", + "type": "text" + }, + { + "id": "resources", + "title": "Worker и ресурсы", + "type": "text" + }, + { + "id": "publication", + "title": "Mission Core publication", + "type": "text" + }, + { + "id": "presentation_defect", + "title": "Исправление режима Кубы 3D", + "type": "text" + }, + { + "id": "artifacts", + "title": "Артефакты", + "type": "text" + }, + { + "id": "limits", + "title": "Границы доказательства", + "type": "text" + }, + { + "id": "checks", + "title": "Проверки E14", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий physical gate", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-26", + "id": "41c343ab-bcce-43e9-af9e-86ff263f9eaa", + "title": "LAB E15 — Live shadow inference qualification (accepted)", + "updated_at": "2026-07-22T17:31:02.736398+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "e15_outcome", + "title": "Результат", + "type": "text" + }, + { + "id": "e15_scope", + "title": "Граница доказательства", + "type": "text" + }, + { + "id": "e15_attempts", + "title": "Сравнение попыток", + "type": "text" + }, + { + "id": "e15_transport", + "title": "Transport и bounded state", + "type": "text" + }, + { + "id": "e15_latency", + "title": "Latency и ресурсы", + "type": "text" + }, + { + "id": "e15_provenance", + "title": "Конфигурация и provenance", + "type": "text" + }, + { + "id": "e15_storage", + "title": "Storage и артефакты", + "type": "text" + }, + { + "id": "e15_next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "e15_checks", + "title": "Проверено", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-27", + "id": "f84d8d55-8c25-46db-968f-caf5bbb791be", + "title": "LAB E16 · Persistent worker + unified live Rerun layers", + "updated_at": "2026-07-22T18:25:28.396109+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "scope", + "title": "Контур эксперимента", + "type": "text" + }, + { + "id": "worker", + "title": "Persistent worker", + "type": "text" + }, + { + "id": "identity", + "title": "Immutable identity", + "type": "text" + }, + { + "id": "transport", + "title": "Live result transport", + "type": "text" + }, + { + "id": "scene", + "title": "Единая сцена", + "type": "text" + }, + { + "id": "validation", + "title": "Валидация", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения и честная граница", + "type": "text" + }, + { + "id": "checks", + "title": "Приёмка LAB E16", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + } + ], + "comments": [ + { + "id": "a6e0be52-51b7-4433-b750-91ba562f58cc", + "created_at": "2026-07-31T19:17:07.062715+00:00", + "updated_at": "2026-07-31T19:17:07.062740+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-28", + "id": "9b6393bf-3000-4701-a025-6aa49c066d55", + "title": "LAB E17 · Fast recorded admission + live AI observability", + "updated_at": "2026-07-22T20:01:21.131309+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "scope", + "title": "Контур эксперимента", + "type": "text" + }, + { + "id": "source", + "title": "Полный источник данных", + "type": "text" + }, + { + "id": "projection", + "title": "Операторская проекция", + "type": "text" + }, + { + "id": "timing", + "title": "Хронометраж", + "type": "text" + }, + { + "id": "presentation", + "title": "AI и визуальная приёмка", + "type": "text" + }, + { + "id": "live_metrics", + "title": "Live AI observability", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "validation", + "title": "Валидация", + "type": "text" + }, + { + "id": "limits", + "title": "Честная граница результата", + "type": "text" + }, + { + "id": "acceptance", + "title": "Приёмка LAB E17", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-29", + "id": "58307669-7791-4e1d-86ab-31902faa8e82", + "title": "LAB E18 · Dense latest-frame AI scene + cache isolation", + "updated_at": "2026-07-22T20:58:23.723972+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "scope", + "title": "Контур эксперимента", + "type": "text" + }, + { + "id": "symptom", + "title": "Наблюдавшийся дефект", + "type": "text" + }, + { + "id": "root_cause", + "title": "Доказанная причина", + "type": "text" + }, + { + "id": "projection", + "title": "Принятая операторская проекция v12", + "type": "text" + }, + { + "id": "camera", + "title": "Единая камера и слои", + "type": "text" + }, + { + "id": "cache", + "title": "Изоляция производного кэша", + "type": "text" + }, + { + "id": "timing", + "title": "Размер и хронометраж", + "type": "text" + }, + { + "id": "visual", + "title": "Визуальная приёмка", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "validation", + "title": "Валидация", + "type": "text" + }, + { + "id": "limits", + "title": "Честная граница результата", + "type": "text" + }, + { + "id": "checks", + "title": "Приёмка LAB E18", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + } + ], + "comments": [ + { + "id": "706e98b9-20b3-47ca-81f1-8bb3f50e6cad", + "created_at": "2026-07-22T21:01:32.133075+00:00", + "updated_at": "2026-07-22T21:01:32.133100+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-30", + "id": "de8c0008-35ec-483a-b8cc-abf3df394038", + "title": "LAB E19 · Ground-aware 3D cuboids without cloud loss", + "updated_at": "2026-07-23T05:24:18.232330+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "purpose", + "title": "Цель", + "type": "text" + }, + { + "id": "cause", + "title": "Доказанная причина визуального дефекта", + "type": "text" + }, + { + "id": "configuration", + "title": "Конфигурация LAB E19", + "type": "text" + }, + { + "id": "counterfactual", + "title": "Полный refusion replay", + "type": "text" + }, + { + "id": "presentation", + "title": "Rerun presentation contract", + "type": "text" + }, + { + "id": "materialization", + "title": "Immutable result и операторский RRD", + "type": "text" + }, + { + "id": "visual", + "title": "Визуальная приёмка", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "validation", + "title": "Валидация", + "type": "text" + }, + { + "id": "limits", + "title": "Честная граница результата", + "type": "text" + }, + { + "id": "checks", + "title": "Приёмка LAB E19", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-31", + "id": "911e30c9-d0d1-46a3-8468-33d1c704f68b", + "title": "LAB E20 · Stable operator camera and pointer navigation", + "updated_at": "2026-07-23T14:19:29.552804+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "scope", + "title": "Контур и цель", + "type": "text" + }, + { + "id": "symptoms", + "title": "Исходные симптомы", + "type": "text" + }, + { + "id": "root_causes", + "title": "Подтверждённые причины", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализованный контракт", + "type": "text" + }, + { + "id": "build_identity", + "title": "Сборочная идентичность", + "type": "text" + }, + { + "id": "validation", + "title": "Проверка результата", + "type": "text" + }, + { + "id": "limits", + "title": "Границы доказательства", + "type": "text" + }, + { + "id": "recorded_color_follow", + "title": "Recorded color и follow-pivot", + "type": "text" + }, + { + "id": "acceptance", + "title": "Критерии завершения", + "type": "checker" + }, + { + "id": "next", + "title": "Следующий эксперимент", + "type": "text" + } + ], + "comments": [ + { + "id": "e662d236-d411-4f3c-b289-a0e4e274e390", + "created_at": "2026-07-23T08:44:34.744960+00:00", + "updated_at": "2026-07-23T08:44:34.744984+00:00" + }, + { + "id": "fc34a931-22bb-467c-bbfa-fdc275808cab", + "created_at": "2026-07-23T09:36:05.149540+00:00", + "updated_at": "2026-07-23T09:36:05.149574+00:00" + }, + { + "id": "610448a4-a519-4394-92ad-44a74bc3fb9f", + "created_at": "2026-07-23T12:15:30.393323+00:00", + "updated_at": "2026-07-23T12:15:30.393350+00:00" + }, + { + "id": "8330087d-5251-43d0-9347-511f486e195b", + "created_at": "2026-07-23T14:19:31.993280+00:00", + "updated_at": "2026-07-23T14:19:31.993306+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-32", + "id": "94b9fa75-4023-49e8-9d5b-b6f573ae7165", + "title": "LAB E21 · Real-time replay envelope", + "updated_at": "2026-07-23T16:03:44.314315+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "e21-objective", + "title": "Цель эксперимента", + "type": "text" + }, + { + "id": "e21-contract", + "title": "Контракт runtime", + "type": "text" + }, + { + "id": "e21-matrix", + "title": "Фактически проверенный контур", + "type": "text" + }, + { + "id": "e21-metrics", + "title": "Канонический результат", + "type": "text" + }, + { + "id": "e21-resources", + "title": "Ресурсный envelope", + "type": "text" + }, + { + "id": "e21-defects", + "title": "Найденные и исправленные дефекты", + "type": "text" + }, + { + "id": "e21-gates", + "title": "Интерпретация gate", + "type": "text" + }, + { + "id": "e21-boundaries", + "title": "Границы доказательства", + "type": "text" + }, + { + "id": "e21-implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "e21-next", + "title": "Следующий лабораторный шаг", + "type": "text" + }, + { + "id": "e21-checklist", + "title": "Чекер LAB E21", + "type": "checker" + } + ], + "comments": [ + { + "id": "55d2efb2-215a-4add-a746-f600b471fd00", + "created_at": "2026-07-23T16:02:55.721888+00:00", + "updated_at": "2026-07-23T16:02:55.721915+00:00" + }, + { + "id": "f5dc73a5-fee6-4707-86b3-903af96d0ddd", + "created_at": "2026-07-23T17:14:52.853160+00:00", + "updated_at": "2026-07-23T17:14:52.853186+00:00" + }, + { + "id": "cc349f00-3c84-4393-9e1c-2e8403085ece", + "created_at": "2026-07-23T18:17:41.553674+00:00", + "updated_at": "2026-07-23T18:17:41.553704+00:00" + }, + { + "id": "88f20ddf-8f49-49af-8475-6da9731249c8", + "created_at": "2026-07-23T19:34:39.757022+00:00", + "updated_at": "2026-07-23T19:34:39.757054+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-33", + "id": "65870245-aaf4-4fe1-a750-afd88974366c", + "title": "LAB E22 — temporal stability 2D/3D/semantic", + "updated_at": "2026-07-23T20:24:53.550713+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "goal", + "title": "Цель и связь с real-time", + "type": "text" + }, + { + "id": "input", + "title": "Вход и конфигурация", + "type": "text" + }, + { + "id": "result", + "title": "Результат", + "type": "text" + }, + { + "id": "load", + "title": "Нагрузка и границы", + "type": "text" + }, + { + "id": "publish", + "title": "Публикация и проверка", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения", + "type": "text" + }, + { + "id": "acceptance", + "title": "Acceptance", + "type": "checker" + }, + { + "id": "next-gate", + "title": "Следующий gate", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-34", + "id": "223f5d48-2f32-4c2c-a0b9-ab6b1fdeb90e", + "title": "LAB E23 — inline temporal stability 1×", + "updated_at": "2026-08-21T19:05:43.074555+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "verdict", + "title": "Результат", + "type": "text" + }, + { + "id": "runs", + "title": "Прогоны и решение", + "type": "text" + }, + { + "id": "input", + "title": "Вход и синхронизация", + "type": "text" + }, + { + "id": "software", + "title": "Конфигурация софта", + "type": "text" + }, + { + "id": "throughput", + "title": "Throughput и задержки", + "type": "text" + }, + { + "id": "quality", + "title": "Raw vs inline quality", + "type": "text" + }, + { + "id": "bounds", + "title": "Bounds и ресурсы", + "type": "text" + }, + { + "id": "publication", + "title": "Публикация и проверка", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и validation", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения доказательства", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "acceptance", + "title": "Фиксация LAB E23", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-35", + "id": "5ce13a58-d1b6-4fbf-ae95-0fac4ff10ec0", + "title": "LAB E24 — world-frame motion qualification", + "updated_at": "2026-07-24T08:20:02.405117+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "source", + "title": "Входные данные и конфигурация", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "runs", + "title": "Серия прогонов", + "type": "text" + }, + { + "id": "benchmark", + "title": "Результат benchmark", + "type": "text" + }, + { + "id": "conclusion", + "title": "Вывод", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate — E25", + "type": "text" + }, + { + "id": "validation", + "title": "Проверка и воспроизводимость", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-36", + "id": "86895760-08c3-4f37-ade5-b403c629e4f5", + "title": "LAB E25 — persistent support motion evidence", + "updated_at": "2026-07-24T09:52:06.717549+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "source", + "title": "Входные данные и неизменяемые источники", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "benchmark", + "title": "Benchmark и исправление разметки", + "type": "text" + }, + { + "id": "runs", + "title": "Серия прогонов", + "type": "text" + }, + { + "id": "result", + "title": "Фактический результат", + "type": "text" + }, + { + "id": "performance", + "title": "Производительность и replay", + "type": "text" + }, + { + "id": "conclusion", + "title": "Вывод", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate — E26", + "type": "text" + }, + { + "id": "validation", + "title": "Проверка и воспроизводимость", + "type": "checker" + } + ], + "comments": [ + { + "id": "1932c5c3-2f11-4832-9f63-816d2339fb16", + "created_at": "2026-07-24T09:51:49.833713+00:00", + "updated_at": "2026-07-24T09:51:49.833739+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-37", + "id": "118fbe1d-f34c-4754-af81-9254c34e0cf2", + "title": "LAB E26 — camera + ego-motion evidence", + "updated_at": "2026-07-24T10:39:26.154891+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель эксперимента", + "type": "text" + }, + { + "id": "inputs", + "title": "Неизменяемые входы", + "type": "text" + }, + { + "id": "algorithm", + "title": "Реализованный контур", + "type": "text" + }, + { + "id": "fusion", + "title": "Правила объединения и безопасность", + "type": "text" + }, + { + "id": "result", + "title": "Результат E26.1", + "type": "text" + }, + { + "id": "runtime", + "title": "Производительность и операторский путь", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "limits", + "title": "Ограничения доказательства", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate E27", + "type": "text" + }, + { + "id": "acceptance", + "title": "Приёмка LAB E26", + "type": "checker" + } + ], + "comments": [ + { + "id": "71702bac-bb67-465d-9481-f1e4b93c7260", + "created_at": "2026-07-24T10:39:10.898081+00:00", + "updated_at": "2026-07-24T10:39:10.898108+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-38", + "id": "8907bead-3e90-4607-8706-b22d73023127", + "title": "Полигон — PX4/Gazebo simulation branch", + "updated_at": "2026-07-24T11:21:46.084794+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "strategy", + "title": "Стратегическая цель", + "type": "text" + }, + { + "id": "boundaries", + "title": "Границы продуктовых модулей", + "type": "text" + }, + { + "id": "target-architecture", + "title": "Целевая архитектура", + "type": "text" + }, + { + "id": "ui", + "title": "Модуль Mission Core «Полигон»", + "type": "text" + }, + { + "id": "internal-modules", + "title": "Планируемые внутренние модули", + "type": "text" + }, + { + "id": "contracts", + "title": "Канонические контракты данных", + "type": "text" + }, + { + "id": "upstream-core", + "title": "Upstream стек первой очереди", + "type": "text" + }, + { + "id": "worlds", + "title": "Миры, модели и генерация сценариев", + "type": "text" + }, + { + "id": "navigation-baseline", + "title": "Навигационный baseline", + "type": "text" + }, + { + "id": "benchmarks", + "title": "Closed-loop полигоны и benchmark-наборы", + "type": "text" + }, + { + "id": "datasets", + "title": "Реальные датасеты для perception/replay", + "type": "text" + }, + { + "id": "sim-s0", + "title": "SIM S0 — compatibility и infrastructure spike", + "type": "text" + }, + { + "id": "sim-s1", + "title": "SIM S1 — stock vehicle в stock world", + "type": "text" + }, + { + "id": "sim-s2", + "title": "SIM S2 — navigation baseline и препятствия", + "type": "text" + }, + { + "id": "sim-s3", + "title": "SIM S3 — виртуальные сенсоры и Mission Core perception", + "type": "text" + }, + { + "id": "sim-s4", + "title": "SIM S4 — replay datasets и RAVNOVES00 shadow", + "type": "text" + }, + { + "id": "sim-s5", + "title": "SIM S5 — RAVNOVES00 digital twin", + "type": "text" + }, + { + "id": "sim-s6", + "title": "SIM S6 — actual trike model, HIL и shadow-only переход", + "type": "text" + }, + { + "id": "metrics", + "title": "Единый формат каждого прогона", + "type": "text" + }, + { + "id": "safety", + "title": "Authority и безопасность", + "type": "text" + }, + { + "id": "risks", + "title": "Ключевые риски и решения", + "type": "text" + }, + { + "id": "phase-checker", + "title": "Этапы ветки «Полигон»", + "type": "checker" + }, + { + "id": "foundation-checker", + "title": "Архитектурные решения до реализации", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-39", + "id": "c4b87476-a1f7-45cb-8e34-37d8958fcb93", + "title": "Полигон — канонический архитектурный план и SRS", + "updated_at": "2026-07-24T21:32:41.350691+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "canonical-role", + "title": "Роль карточки", + "type": "text" + }, + { + "id": "product-value", + "title": "Продуктовая ценность", + "type": "text" + }, + { + "id": "scope", + "title": "Границы продукта", + "type": "text" + }, + { + "id": "non-goals", + "title": "Явные non-goals первой очереди", + "type": "text" + }, + { + "id": "architecture", + "title": "Целевая архитектура", + "type": "text" + }, + { + "id": "deployment-topology-adr0016", + "title": "Размещение продукта, worker и бортового контура", + "type": "text" + }, + { + "id": "domain", + "title": "Каноническая доменная модель", + "type": "text" + }, + { + "id": "run-kinds", + "title": "Виды прогонов", + "type": "text" + }, + { + "id": "time", + "title": "Контракт времени", + "type": "text" + }, + { + "id": "frames", + "title": "Контракт координат", + "type": "text" + }, + { + "id": "authority", + "title": "Authority и safety boundary", + "type": "text" + }, + { + "id": "artifacts", + "title": "Source-of-record и артефакты", + "type": "text" + }, + { + "id": "versions", + "title": "Базовая матрица S0", + "type": "text" + }, + { + "id": "control-adapter", + "title": "Первая управляющая интеграция", + "type": "text" + }, + { + "id": "reproducibility", + "title": "Воспроизводимость", + "type": "text" + }, + { + "id": "s0", + "title": "SIM S0 — compatibility и infrastructure", + "type": "text" + }, + { + "id": "s1", + "title": "SIM S1 — stock rover loop", + "type": "text" + }, + { + "id": "s2", + "title": "SIM S2 — navigation qualification baseline", + "type": "text" + }, + { + "id": "s3-s6", + "title": "SIM S3–S6 — развитие", + "type": "text" + }, + { + "id": "delivery-plan", + "title": "Рабочая декомпозиция", + "type": "text" + }, + { + "id": "ui-gate", + "title": "UI и навигация", + "type": "text" + }, + { + "id": "s1-bootstrap-ui-gates-e1809ac", + "title": "S1 target bootstrap и UI gates — 2026-07-24", + "type": "text" + }, + { + "id": "s1b-real-provider-6cb1495", + "title": "S1B real-provider boundary — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "ui0-readonly-b790f29", + "title": "UI-0 read-only boundary — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "deployment-topology-359f0d1", + "title": "Deployment topology — ADR 0016, commit 359f0d1", + "type": "text" + }, + { + "id": "ui2-worker-gated-60e7916", + "title": "UI-2 worker-gated 3D Polygon — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "s1d-command-motion-49b0f47", + "title": "S1D Ackermann command/motion — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "provider-neutral-unreal-37c24b5", + "title": "S1E provider-neutral boundary / Unreal plan — commit 37c24b5", + "type": "text" + }, + { + "id": "change-policy", + "title": "Управление изменениями", + "type": "text" + }, + { + "id": "architecture-checker", + "title": "Архитектурный foundation gate", + "type": "checker" + }, + { + "id": "s0-checker", + "title": "SIM S0 acceptance", + "type": "checker" + }, + { + "id": "phase-checker", + "title": "Этапы реализации", + "type": "checker" + }, + { + "id": "implementation-8176106", + "title": "Реализация P0 / первый инкремент S0 — commit 8176106", + "type": "text" + }, + { + "id": "implementation-6290fcb", + "title": "SIM S0 worker bootstrap — commit 6290fcb", + "type": "text" + }, + { + "id": "s0-go-a19053d", + "title": "SIM S0 acceptance — GO", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-40", + "id": "613af3e9-b8b2-44ae-8db5-6ff91a64a98f", + "title": "SIM S0 — D-only version lock и infrastructure doctor", + "updated_at": "2026-07-24T14:25:50.445713+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "scope", + "title": "Scope", + "type": "text" + }, + { + "id": "target", + "title": "Целевой профиль", + "type": "text" + }, + { + "id": "deliverables", + "title": "Deliverables", + "type": "text" + }, + { + "id": "verdict", + "title": "Правило verdict", + "type": "text" + }, + { + "id": "s0-checker", + "title": "SIM S0 work", + "type": "checker" + }, + { + "id": "implementation-8176106", + "title": "Репозиторный инкремент — 8176106", + "type": "text" + }, + { + "id": "implementation-6290fcb", + "title": "Worker bootstrap и stock-rover evidence — 6290fcb", + "type": "text" + }, + { + "id": "s0-go-a19053d", + "title": "Финальная приёмка SIM S0", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-41", + "id": "4dafbe4d-b7ab-443a-9d7b-dd2bd4abac67", + "title": "SIM S1 — Simulation Orchestrator и stock rover control", + "updated_at": "2026-07-24T21:33:24.037423+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "scope", + "title": "Scope и результат", + "type": "text" + }, + { + "id": "value", + "title": "Продуктовая ценность", + "type": "text" + }, + { + "id": "s0-input", + "title": "Принятый вход", + "type": "text" + }, + { + "id": "domain", + "title": "Доменный контракт", + "type": "text" + }, + { + "id": "architecture", + "title": "Архитектура S1", + "type": "text" + }, + { + "id": "lifecycle", + "title": "Lifecycle и ownership", + "type": "text" + }, + { + "id": "commands", + "title": "Canonical command boundary", + "type": "text" + }, + { + "id": "safety", + "title": "TTL, watchdog и failsafe", + "type": "text" + }, + { + "id": "telemetry", + "title": "State и telemetry", + "type": "text" + }, + { + "id": "artifacts", + "title": "Persistence и evidence", + "type": "text" + }, + { + "id": "cases", + "title": "Acceptance cases", + "type": "text" + }, + { + "id": "delivery", + "title": "Порядок реализации", + "type": "text" + }, + { + "id": "implementation-s1a-20260724", + "title": "Фактическая реализация S1A — 2026-07-24", + "type": "text" + }, + { + "id": "implementation-s1b-20260724", + "title": "Фактическая реализация S1B ownership foundation — 2026-07-24", + "type": "text" + }, + { + "id": "target-bootstrap-630d1ae", + "title": "Target bootstrap 630d1ae — PASS", + "type": "text" + }, + { + "id": "implementation-s1b-real-6cb1495", + "title": "S1B real-provider acceptance — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "implementation-ui0-b790f29", + "title": "UI-0 read-only Run view — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "ui-milestones-20260724", + "title": "Фактический результат в интерфейсе — 2026-07-24", + "type": "text" + }, + { + "id": "implementation-s1c-b7ccca3", + "title": "S1C registered live worker — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "implementation-ui2-60e7916", + "title": "UI-2 worker-gated 3D workspace — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "implementation-s1d-49b0f47", + "title": "S1D Ackermann command/motion — PASS, 2026-07-24", + "type": "text" + }, + { + "id": "implementation-s1e-37c24b5", + "title": "S1E provider-neutral worker contract — PASS, 2026-07-25", + "type": "text" + }, + { + "id": "s1-checker", + "title": "SIM S1 work", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-42", + "id": "9b3a56c8-b6ff-43ce-a671-5df473672a4b", + "title": "LiDAR worker — evidence, quality и 3D perception", + "updated_at": "2026-07-31T11:38:44.512917+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "canonical-role", + "title": "Роль и продуктовая ценность", + "type": "text" + }, + { + "id": "product-surface-boundaries", + "title": "Продуктовые поверхности и навигационная граница", + "type": "text" + }, + { + "id": "current-truth", + "title": "Текущая архитектурная правда K1", + "type": "text" + }, + { + "id": "representation-truth", + "title": "Почему кольца dataset и плотная K1 карта выглядят по-разному", + "type": "text" + }, + { + "id": "dataset-gateway", + "title": "Dataset Gateway и новая очередь работ", + "type": "text" + }, + { + "id": "existing-results", + "title": "Что сохраняем из текущего стека", + "type": "text" + }, + { + "id": "market-stack", + "title": "Выбранные внешние компоненты", + "type": "text" + }, + { + "id": "target-shape", + "title": "Целевая форма worker", + "type": "text" + }, + { + "id": "authority-gates", + "title": "Authority и запреты", + "type": "text" + }, + { + "id": "acceptance", + "title": "Метрики и acceptance", + "type": "text" + }, + { + "id": "implementation-3f549f9", + "title": "L0 implementation — 3f549f9", + "type": "text" + }, + { + "id": "implementation-e4fdd9f", + "title": "L1 implementation — e4fdd9f", + "type": "text" + }, + { + "id": "implementation-75a3e66", + "title": "L2 diagnostic implementation — 75a3e66", + "type": "text" + }, + { + "id": "implementation-2dfb34e", + "title": "L2 firmware evidence и visual review — 2dfb34e", + "type": "text" + }, + { + "id": "implementation-3333e9a", + "title": "L2 RAVNOVES00 field review — 3333e9a", + "type": "text" + }, + { + "id": "implementation-f57d64b", + "title": "Dataset Gateway S0 — f57d64b", + "type": "text" + }, + { + "id": "l0-checker", + "title": "L0 — evidence truth и универсальный adapter", + "type": "checker" + }, + { + "id": "dataset-gateway-checker", + "title": "L2.5 — Dataset Gateway и public baseline", + "type": "checker" + }, + { + "id": "l1-l3-checker", + "title": "L1–L3 — real LiDAR evidence и detector", + "type": "checker" + }, + { + "id": "l4-l6-checker", + "title": "L4–L6 — live, occupancy и mapping", + "type": "checker" + }, + { + "id": "implementation-c01712c", + "title": "LiDAR product surfaces — c01712c", + "type": "text" + }, + { + "id": "product-surface-checker", + "title": "Product surface cleanup — c01712c", + "type": "checker" + }, + { + "id": "implementation-881e973", + "title": "Viewer design и постоянный Polygon — 881e973", + "type": "text" + }, + { + "id": "implementation-951b40c", + "title": "GOOSE validation baseline — 951b40c", + "type": "text" + }, + { + "id": "implementation-60ba640", + "title": "GOOSE Patchwork++ A/B — 60ba640", + "type": "text" + }, + { + "id": "implementation-cea63f9", + "title": "GOOSE full qualification и Polygon Runs — cea63f9", + "type": "text" + }, + { + "id": "implementation-2e5c4ba", + "title": "Operator full-frame review — 2e5c4ba", + "type": "text" + }, + { + "id": "implementation-e97573e", + "title": "GOOSE source/run boundary — e97573e", + "type": "text" + }, + { + "id": "implementation-677dbdb", + "title": "E29 camera-first geometry fusion — 677dbdb", + "type": "text" + }, + { + "id": "implementation-05413e3", + "title": "Operator surface ownership — 05413e3", + "type": "text" + }, + { + "id": "operator-surface-restructure-checker", + "title": "Operator surface ownership", + "type": "checker" + }, + { + "id": "implementation-42041b3", + "title": "Operator surface correction — 42041b3", + "type": "text" + }, + { + "id": "implementation-453d760", + "title": "Environment shell и laboratory UI — 453d760", + "type": "text" + }, + { + "id": "environment-shell-checker", + "title": "Environment shell и laboratory UI — 453d760", + "type": "checker" + }, + { + "id": "implementation-dc55ff1", + "title": "Ревизия лабораторных данных — dc55ff1", + "type": "text" + }, + { + "id": "laboratory-evidence-audit-checker", + "title": "Ревизия лабораторных данных — dc55ff1", + "type": "checker" + }, + { + "id": "laboratory-run-canon-f50405d", + "title": "LAB evidence canon — f50405d", + "type": "text" + }, + { + "id": "laboratory-run-canon-checks-f50405d", + "title": "LAB run canon — контроль", + "type": "checker" + }, + { + "id": "post-e29-research-cycle-a44d762", + "title": "Следующий исследовательский цикл — LAB E30–E36", + "type": "text" + }, + { + "id": "post-e29-research-checker-a44d762", + "title": "LAB E30–E36 — очередь исполнения", + "type": "checker" + }, + { + "id": "l3-pointpillars-transfer-20260731", + "title": "L3 — KITTI: отдельный публичный PointPillars benchmark", + "type": "text" + }, + { + "id": "l31-pointpillars-ravnoves-20260731", + "title": "L3.1 — PointPillars на RAVNOVES00", + "type": "text" + } + ], + "comments": [ + { + "id": "37ad16bc-290e-4a66-abdf-eaee94155182", + "created_at": "2026-07-24T22:01:38.541199+00:00", + "updated_at": "2026-07-24T22:01:38.541231+00:00" + }, + { + "id": "dba5b264-793f-40fe-86f9-c0a9905c0822", + "created_at": "2026-07-24T22:37:36.725394+00:00", + "updated_at": "2026-07-24T22:37:36.725420+00:00" + }, + { + "id": "22107ce5-6072-4408-b35f-ced4ac8786b2", + "created_at": "2026-07-24T23:12:10.508994+00:00", + "updated_at": "2026-07-24T23:12:10.509035+00:00" + }, + { + "id": "d65fba60-f546-451c-93e2-4d1fa12ef513", + "created_at": "2026-07-25T06:47:14.975375+00:00", + "updated_at": "2026-07-25T06:47:14.975408+00:00" + }, + { + "id": "abbae810-7e7a-4eba-bd12-7b9a4507aa0d", + "created_at": "2026-07-25T07:28:41.748515+00:00", + "updated_at": "2026-07-25T07:28:41.748539+00:00" + }, + { + "id": "8c8dc486-192e-4c1e-8ca4-cf7959ce8cfc", + "created_at": "2026-07-25T08:23:56.033858+00:00", + "updated_at": "2026-07-25T08:23:56.033886+00:00" + }, + { + "id": "28120ecd-771d-41f6-85bb-452ceb2bdb58", + "created_at": "2026-07-25T10:08:56.027206+00:00", + "updated_at": "2026-07-25T10:08:56.027230+00:00" + }, + { + "id": "312ac6f6-b55f-4fa7-baba-6f15dbd4da77", + "created_at": "2026-07-25T10:24:53.217284+00:00", + "updated_at": "2026-07-25T10:24:53.217309+00:00" + }, + { + "id": "bf52367c-a00f-4c84-8a98-c2ecc72ddbc5", + "created_at": "2026-07-25T11:15:25.615454+00:00", + "updated_at": "2026-07-25T11:15:25.615479+00:00" + }, + { + "id": "67564d08-3baa-4a59-8878-b84b7ad63ec0", + "created_at": "2026-07-25T11:16:01.200330+00:00", + "updated_at": "2026-07-25T11:16:01.200362+00:00" + }, + { + "id": "3aec9bfe-3309-46b7-a59d-cdadc1b9e9bc", + "created_at": "2026-07-25T12:26:15.604280+00:00", + "updated_at": "2026-07-25T12:26:15.604311+00:00" + }, + { + "id": "98234880-c820-4496-97ff-e1f437216c98", + "created_at": "2026-07-25T13:22:17.924431+00:00", + "updated_at": "2026-07-25T13:22:17.924455+00:00" + }, + { + "id": "0437e843-113b-420c-aa4c-e5fe9339efda", + "created_at": "2026-07-25T14:04:17.569467+00:00", + "updated_at": "2026-07-25T14:04:17.569491+00:00" + }, + { + "id": "dfc9509e-d7d5-48ba-ae15-ae36308f24ce", + "created_at": "2026-07-25T14:06:14.609857+00:00", + "updated_at": "2026-07-25T14:06:14.609881+00:00" + }, + { + "id": "b02d0f6c-b18e-406f-a886-c8ec4812313b", + "created_at": "2026-07-25T15:43:14.379377+00:00", + "updated_at": "2026-07-25T15:43:14.379405+00:00" + }, + { + "id": "4bc7ee38-9be8-4218-b194-46a61a46024e", + "created_at": "2026-07-26T08:51:00.706514+00:00", + "updated_at": "2026-07-26T08:51:00.706538+00:00" + }, + { + "id": "24b114a1-ba56-42b3-ae04-3c4992e6f605", + "created_at": "2026-07-26T09:52:04.778779+00:00", + "updated_at": "2026-07-26T09:52:04.778803+00:00" + }, + { + "id": "c73ebd76-5368-4d6f-8208-df85a72f1e09", + "created_at": "2026-07-26T10:45:08.132483+00:00", + "updated_at": "2026-07-26T10:45:08.132511+00:00" + }, + { + "id": "36b21974-556b-4a79-87cd-858307cfb4c7", + "created_at": "2026-07-26T12:38:09.542004+00:00", + "updated_at": "2026-07-26T12:38:09.542035+00:00" + }, + { + "id": "a9f356df-7969-4aa6-85e1-4556511f3224", + "created_at": "2026-07-26T13:44:11.760323+00:00", + "updated_at": "2026-07-26T13:44:11.760350+00:00" + }, + { + "id": "ce0db5c9-815f-42ef-a138-78fb7f04497b", + "created_at": "2026-07-26T14:07:34.530706+00:00", + "updated_at": "2026-07-26T14:07:34.530731+00:00" + }, + { + "id": "9da0340c-97d0-4b9c-91ac-f3044b3f8908", + "created_at": "2026-07-26T15:00:15.214475+00:00", + "updated_at": "2026-07-26T15:00:15.214502+00:00" + }, + { + "id": "5f9682ff-a047-4b35-ad6b-61214172d1c3", + "created_at": "2026-07-26T15:30:46.351482+00:00", + "updated_at": "2026-07-26T15:30:46.351515+00:00" + }, + { + "id": "503e0b09-fb4e-49c9-b3ab-e2a991a04b93", + "created_at": "2026-07-31T07:53:31.338623+00:00", + "updated_at": "2026-07-31T07:53:31.338651+00:00" + }, + { + "id": "d4207e12-7369-410e-bd20-c2bbb2a3285c", + "created_at": "2026-07-31T08:48:32.512744+00:00", + "updated_at": "2026-07-31T08:48:32.512780+00:00" + }, + { + "id": "fe3ab803-e4e4-4dd7-95e2-a42647ac9321", + "created_at": "2026-07-31T11:39:07.900655+00:00", + "updated_at": "2026-07-31T11:39:07.900698+00:00" + }, + { + "id": "9d03fd7b-93f3-4d1e-97d5-24fe0a90393a", + "created_at": "2026-07-31T21:59:17.292755+00:00", + "updated_at": "2026-07-31T21:59:17.292783+00:00" + }, + { + "id": "71cfc484-61a5-458b-af76-915920d815e9", + "created_at": "2026-08-01T06:44:08.698961+00:00", + "updated_at": "2026-08-01T06:44:08.698987+00:00" + }, + { + "id": "607d129e-4be5-4015-a84b-46964f1824b2", + "created_at": "2026-08-01T07:54:41.115637+00:00", + "updated_at": "2026-08-01T07:54:41.115669+00:00" + }, + { + "id": "07eefcf4-09a4-4351-8f68-92f744ab80fb", + "created_at": "2026-08-04T08:06:24.506378+00:00", + "updated_at": "2026-08-04T08:06:24.506407+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-43", + "id": "506ec4ee-a8ed-4b53-ae6b-4ad56665d04e", + "title": "LAB E34 · short-TTL occupied/unknown temporal layer", + "updated_at": "2026-07-27T13:58:21.312194+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "e34_objective", + "title": "Задача и архитектурный этап", + "type": "text" + }, + { + "id": "e34_inputs", + "title": "Неизменяемые входы и профиль", + "type": "text" + }, + { + "id": "e34_method", + "title": "Метод и алгоритмы", + "type": "text" + }, + { + "id": "e34_failed_run", + "title": "Отклонённый первый прогон и коррекция", + "type": "text" + }, + { + "id": "e34_result", + "title": "Принятый результат", + "type": "text" + }, + { + "id": "e34_metrics", + "title": "Гейты, timing и bounds", + "type": "text" + }, + { + "id": "e34_product", + "title": "LAB и продуктовая материализация", + "type": "text" + }, + { + "id": "e34_authority", + "title": "Решение и границы полномочий", + "type": "text" + }, + { + "id": "e34_delivery", + "title": "Реализация, доставка и проверки", + "type": "text" + }, + { + "id": "e34_next", + "title": "Следующий критический gate", + "type": "text" + }, + { + "id": "e34_acceptance", + "title": "E34 acceptance", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-44", + "id": "5c5cd924-a0e0-43f5-ba4d-a5a312db91b4", + "title": "LAB E35 · deterministic degradation and recovery", + "updated_at": "2026-07-27T14:56:05.553534+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "e35_objective", + "title": "Задача и архитектурный этап", + "type": "text" + }, + { + "id": "e35_inputs", + "title": "Неизменяемые входы и профиль", + "type": "text" + }, + { + "id": "e35_scenarios", + "title": "Сценарии отказа и predeclared gates", + "type": "text" + }, + { + "id": "e35_method", + "title": "Метод и алгоритмы", + "type": "text" + }, + { + "id": "e35_result", + "title": "Принятый результат", + "type": "text" + }, + { + "id": "e35_safety", + "title": "Безопасная деградация и accounting", + "type": "text" + }, + { + "id": "e35_product", + "title": "LAB и продуктовая материализация", + "type": "text" + }, + { + "id": "e35_authority", + "title": "Решение и границы полномочий", + "type": "text" + }, + { + "id": "e35_delivery", + "title": "Реализация, доставка и проверки", + "type": "text" + }, + { + "id": "e35_next", + "title": "Следующий критический gate", + "type": "text" + }, + { + "id": "e35_acceptance", + "title": "E35 acceptance", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-45", + "id": "936eacb3-8db6-4d28-81aa-726ed8156566", + "title": "A9 / E36 · аудит второго mounted real source", + "updated_at": "2026-07-27T16:36:58.739463+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "e36-question", + "title": "Задача и граница", + "type": "text" + }, + { + "id": "e36-result", + "title": "Immutable audit result", + "type": "text" + }, + { + "id": "e36-candidate", + "title": "Ближайший кандидат — TEST007", + "type": "text" + }, + { + "id": "e36-other", + "title": "Остальной каталог", + "type": "text" + }, + { + "id": "e36-decision", + "title": "Решение и authority", + "type": "text" + }, + { + "id": "e36-artifacts", + "title": "Репозиторные артефакты", + "type": "text" + }, + { + "id": "e36-checker", + "title": "A9 / E36 gate", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-46", + "id": "c97c6c59-deb7-401b-93a8-a2a58280b769", + "title": "RAVNOVES00 · reference-source product maturation", + "updated_at": "2026-08-04T14:41:16.363225+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "rav-decision", + "title": "Программное решение", + "type": "text" + }, + { + "id": "rav-methodology", + "title": "Коррекция методологии", + "type": "text" + }, + { + "id": "rav-quality", + "title": "Текущий quality status", + "type": "text" + }, + { + "id": "rav-boundary", + "title": "Prediction / evaluation boundary", + "type": "text" + }, + { + "id": "rav-stability", + "title": "Архитектурная стабилизация", + "type": "text" + }, + { + "id": "rav-e45", + "title": "E45 · binding sensitivity", + "type": "text" + }, + { + "id": "rav-truth-island", + "title": "E46–E47 · слепой detector gate", + "type": "text" + }, + { + "id": "rav-future", + "title": "Будущий независимый transfer", + "type": "text" + }, + { + "id": "rav-ui", + "title": "Interface phase boundary", + "type": "text" + }, + { + "id": "rav-authority", + "title": "Что результат не доказывает", + "type": "text" + }, + { + "id": "rav-live-monitoring-20260728", + "title": "Live-monitoring и native lifecycle · 28–29.07.2026", + "type": "text" + }, + { + "id": "rav-e50", + "title": "E50 · exact-content reference index", + "type": "text" + }, + { + "id": "rav-checker", + "title": "Reference-source gates", + "type": "checker" + }, + { + "id": "rav-e46e-ready-stack", + "title": "E46E · NVIDIA ready-stack baseline", + "type": "text" + }, + { + "id": "rav-e46f-dashcam-bakeoff", + "title": "E46F · DashCamNet detector bake-off", + "type": "text" + }, + { + "id": "rav-e46g-rectified-bakeoff", + "title": "E46G · calibrated NVIDIA detector bake-off", + "type": "text" + }, + { + "id": "rav-e46h-full-front", + "title": "E46H · full calibrated FRONT replay", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-47", + "id": "70b960f4-825c-4d76-be83-9ea879156f6e", + "title": "[Mission Core] Worker telemetry: agent → MQTT → Timescale", + "updated_at": "2026-07-29T09:30:53.562328+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "scope-status", + "title": "Назначение и текущий статус", + "type": "text" + }, + { + "id": "product-surface-decision-20260728", + "title": "Продуктовое решение по двум поверхностям", + "type": "text" + }, + { + "id": "architecture-flow", + "title": "Фактическая архитектура и поток данных", + "type": "text" + }, + { + "id": "component-responsibilities", + "title": "Компоненты и ответственность", + "type": "text" + }, + { + "id": "docker-identity", + "title": "Docker identity и namespace", + "type": "text" + }, + { + "id": "communication-contract", + "title": "Коммуникация и topic contract", + "type": "text" + }, + { + "id": "storage-contract", + "title": "Нормализация и модель хранения", + "type": "text" + }, + { + "id": "security-boundary", + "title": "Security и trust boundary", + "type": "text" + }, + { + "id": "current-runtime", + "title": "Текущий локальный runtime Worker 006", + "type": "text" + }, + { + "id": "bootstrap-prerequisites", + "title": "Развёртывание с нуля: prerequisites", + "type": "text" + }, + { + "id": "bootstrap-stack", + "title": "Развёртывание с нуля: telemetry stack", + "type": "text" + }, + { + "id": "bootstrap-agent", + "title": "Развёртывание с нуля: Worker agent", + "type": "text" + }, + { + "id": "operations", + "title": "Эксплуатация и диагностика", + "type": "text" + }, + { + "id": "validation-20260728", + "title": "Фактическая приёмка 28–29.07.2026", + "type": "text" + }, + { + "id": "known-limitations", + "title": "Известные ограничения и технический долг", + "type": "text" + }, + { + "id": "deploy-canon", + "title": "Путь внешнего production deployment по DCPLATFORM-21", + "type": "text" + }, + { + "id": "prod-acceptance", + "title": "Production acceptance criteria", + "type": "text" + }, + { + "id": "implemented-checklist", + "title": "Реализованный local telemetry plane", + "type": "checker" + }, + { + "id": "accepted-near-term-order-20260728", + "title": "Принятый ближайший порядок", + "type": "checker" + }, + { + "id": "prod-plan-foundation", + "title": "Prod phase 1 — воспроизводимость и hardening", + "type": "checker" + }, + { + "id": "prod-plan-product", + "title": "Prod phase 2 — продуктовый pipeline и portable fleet", + "type": "checker" + }, + { + "id": "prod-plan-deploy", + "title": "Prod phase 3 — канонический server rollout", + "type": "checker" + }, + { + "id": "implementation-network-profile-20260729", + "title": "Реализация управляемого сетевого профиля 29.07.2026", + "type": "text" + }, + { + "id": "implementation-native-lifecycle-20260729", + "title": "Реализация native pipeline lifecycle 29.07.2026", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-48", + "id": "e123411e-60d8-4f19-b665-104d6eb71e01", + "title": "RAVNOVES00 · E41–E44 pre-capture architecture stabilization", + "updated_at": "2026-07-28T14:48:12.094330+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "stab-scope", + "title": "Scope", + "type": "text" + }, + { + "id": "stab-e41", + "title": "E41 evidence", + "type": "text" + }, + { + "id": "stab-e42-e44", + "title": "E42–E44 evidence", + "type": "text" + }, + { + "id": "stab-telemetry", + "title": "Telemetry boundary", + "type": "text" + }, + { + "id": "stab-limitations", + "title": "Open boundaries", + "type": "text" + }, + { + "id": "stab-checker", + "title": "Delivery gates", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-49", + "id": "77b191cb-3945-4163-b5d4-d7b961566e4c", + "title": "K1 · Проблемы сканирования", + "updated_at": "2026-08-21T19:05:13.317800+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "current-status", + "title": "Текущий статус", + "type": "text" + }, + { + "id": "field-acceptance", + "title": "Физическая приёмка связи", + "type": "text" + }, + { + "id": "production-ownership-model", + "title": "Production-модель владения соединением", + "type": "text" + }, + { + "id": "quick-connect-model", + "title": "Quick Connect · текущий контракт", + "type": "text" + }, + { + "id": "bridge-model", + "title": "Bridge · текущий контракт", + "type": "text" + }, + { + "id": "scan-control-lifecycle", + "title": "Control и acquisition lifecycle", + "type": "text" + }, + { + "id": "safety-boundaries", + "title": "Границы безопасности", + "type": "text" + }, + { + "id": "resolved-root-causes", + "title": "Установленные причины incident", + "type": "text" + }, + { + "id": "incident-evidence", + "title": "Доказательства incidents", + "type": "text" + }, + { + "id": "viewer-recovery-model", + "title": "Rerun · текущий recovery-контракт", + "type": "text" + }, + { + "id": "implementation-current", + "title": "Текущая реализация", + "type": "text" + }, + { + "id": "diagnostics-current", + "title": "Диагностика и индексирование", + "type": "text" + }, + { + "id": "validation-current", + "title": "Автоматическая и визуальная проверка", + "type": "text" + }, + { + "id": "remaining-scope", + "title": "Оставшийся объём", + "type": "text" + }, + { + "id": "acceptance-current", + "title": "Актуальная приёмка", + "type": "checker" + }, + { + "id": "connection-regression-20260806", + "title": "Инцидент подключения 06.08.2026 · причины и исправление", + "type": "text" + }, + { + "id": "connection-regression-validation-20260806", + "title": "Проверка исправления 06.08.2026", + "type": "text" + }, + { + "id": "connection-regression-acceptance-20260806", + "title": "Приёмка connection regression · 06.08.2026", + "type": "checker" + } + ], + "comments": [ + { + "id": "7aaa81ff-351a-490f-9113-b51753679693", + "created_at": "2026-07-28T17:33:57.121776+00:00", + "updated_at": "2026-07-28T17:33:57.121802+00:00" + }, + { + "id": "0d3c3aa5-c302-4fa5-88af-a0f522353026", + "created_at": "2026-08-21T21:05:09.483530+00:00", + "updated_at": "2026-08-21T21:05:09.483566+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-50", + "id": "709bb579-b79a-4e91-8418-b028d9957fa3", + "title": "Фундаментальная архитектура Mission Core", + "updated_at": "2026-08-27T12:41:28.428789+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "architecture-registry-purpose", + "title": "Назначение и границы", + "type": "text" + }, + { + "id": "architecture-registry-topology", + "title": "Текущая системная топология", + "type": "text" + }, + { + "id": "architecture-registry-central-store", + "title": "Необязательный шлюз артефактов", + "type": "text" + }, + { + "id": "architecture-registry-local-cache", + "title": "Локальные данные и ограниченный cache", + "type": "text" + }, + { + "id": "architecture-registry-field-model", + "title": "Полевая модель эксплуатации", + "type": "text" + }, + { + "id": "architecture-registry-ravnoves00", + "title": "Первый принятый пакет: RAVNOVES00", + "type": "text" + }, + { + "id": "architecture-registry-e40-authority", + "title": "Граница авторитетности E40 и camera-first", + "type": "text" + }, + { + "id": "architecture-registry-e53-camera-first-shadow", + "title": "E53 · регрессионная проверка camera-first", + "type": "text" + }, + { + "id": "architecture-registry-readiness", + "title": "Эксплуатационная готовность и наблюдаемость", + "type": "text" + }, + { + "id": "architecture-registry-implementation", + "title": "Реализация и верификация", + "type": "text" + }, + { + "id": "architecture-registry-lossless-replay", + "title": "Lossless replay и ресурсная граница", + "type": "text" + }, + { + "id": "architecture-registry-observation-boundary", + "title": "Граница диагностических наблюдений", + "type": "text" + }, + { + "id": "architecture-registry-boundaries", + "title": "Открытые границы и порядок ведения", + "type": "text" + }, + { + "id": "architecture-registry-acceptance", + "title": "Принятые архитектурные основания", + "type": "checker" + }, + { + "id": "architecture-registry-ready-stack-provider", + "title": "Готовый detector/tracker provider вместо route tuning", + "type": "text" + }, + { + "id": "architecture-registry-fisheye-boundary", + "title": "E46F · fisheye normalization boundary", + "type": "text" + }, + { + "id": "architecture-registry-e46g-rectified-provider", + "title": "E46G · calibration-derived source adapter и FRONT-only provider", + "type": "text" + }, + { + "id": "architecture-registry-e46h-full-front", + "title": "E46H · full-route FRONT qualification и semantic fail-closed", + "type": "text" + }, + { + "id": "architecture-registry-m49-terrain-stack", + "title": "M4.8R3/M4.9 · terrain и static-obstacle pipeline", + "type": "text" + }, + { + "id": "architecture-registry-sealed-lab-playback", + "title": "LAB · sealed spatial playback data plane", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-51", + "id": "06b48971-8800-4197-a4a0-4326dad98224", + "title": "Технический долг Mission Core", + "updated_at": "2026-08-08T07:46:20.583321+00:00", + "k1_label": true, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "purpose", + "title": "Назначение и граница", + "type": "text" + }, + { + "id": "deferred-live-run", + "title": "Отложенный физический live-прогон K1", + "type": "text" + }, + { + "id": "operator-setup", + "title": "Что включить и как подключить", + "type": "text" + }, + { + "id": "worker-preflight", + "title": "Предпусковая проверка Worker006", + "type": "text" + }, + { + "id": "code-trigger", + "title": "Как инициировать ограниченный worker-run", + "type": "text" + }, + { + "id": "evidence", + "title": "Что сохранить как доказательство", + "type": "text" + }, + { + "id": "acceptance", + "title": "Критерии приёмки", + "type": "text" + }, + { + "id": "stop-rollback", + "title": "Остановка, сбой и откат", + "type": "text" + }, + { + "id": "known-debt", + "title": "Связанный незакрытый долг", + "type": "text" + }, + { + "id": "live-gate-checklist", + "title": "Чекер физического live-gate", + "type": "checker" + }, + { + "id": "debt-classification-20260805", + "title": "Классификация долга и правило блокировки", + "type": "text" + }, + { + "id": "lab-three-tier-policy-20260805", + "title": "LAB: canonical, experimental и legacy", + "type": "text" + }, + { + "id": "worker-contour-reality-20260805", + "title": "Multi-worker: что уже есть и что реально отсутствует", + "type": "text" + }, + { + "id": "worker-bootstrap-scope-20260805", + "title": "Принятый объём bootstrap без универсального установщика", + "type": "text" + }, + { + "id": "telemetry-local-stabilization-20260805", + "title": "Telemetry: обязательная локальная стабилизация", + "type": "text" + }, + { + "id": "telemetry-production-deferred-20260805", + "title": "Telemetry: намеренно отложенный production/fleet долг", + "type": "text" + }, + { + "id": "intentional-nonblockers-20260805", + "title": "Намеренно принятый неблокирующий долг", + "type": "text" + }, + { + "id": "architecture-freeze-blockers-20260805", + "title": "Реальные блокеры архитектурной заморозки", + "type": "text" + }, + { + "id": "architecture-freeze-checker-20260805", + "title": "Gate до продолжения основного CV", + "type": "checker" + }, + { + "id": "repository-checkpoint-6d0abbc-20260805", + "title": "Репозиторный checkpoint 6d0abbc", + "type": "text" + }, + { + "id": "static-analysis-debt-20260805", + "title": "Статический baseline legacy, не скрытый clean claim", + "type": "text" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-52", + "id": "766d8fab-99df-4c1e-a73e-bdabfae8083a", + "title": "Milestone 4 · Recorded-realtime object CV · 2026-08-05", + "updated_at": "2026-08-21T19:05:19.090822+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "scope", + "title": "Назначение и изолированная граница", + "type": "text" + }, + { + "id": "verdict", + "title": "Проверенный архитектурный вердикт", + "type": "text" + }, + { + "id": "product", + "title": "Object-centric продуктовый контракт", + "type": "text" + }, + { + "id": "source", + "title": "Источник и поправка RAVNOVES", + "type": "text" + }, + { + "id": "evidence", + "title": "Что уже доказано и переиспользуется", + "type": "text" + }, + { + "id": "corrections", + "title": "Поправки к исходному аудиту", + "type": "text" + }, + { + "id": "graph", + "title": "ReferencePerceptionGraphV1", + "type": "text" + }, + { + "id": "contracts", + "title": "Контракты и dependency rule", + "type": "text" + }, + { + "id": "gates", + "title": "Product и runtime gates", + "type": "text" + }, + { + "id": "debt", + "title": "Намеренно неблокирующий долг", + "type": "text" + }, + { + "id": "repo", + "title": "Полная репозиторная фиксация", + "type": "text" + }, + { + "id": "impl-m40-m41", + "title": "Реализация M4.0–M4.1 · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m42", + "title": "Реализация M4.2 · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m43-provider", + "title": "M4.3 provider increment · промежуточная фиксация · 2026-08-05", + "type": "text" + }, + { + "id": "m40", + "title": "M4.0 · Baseline freeze", + "type": "checker" + }, + { + "id": "m41", + "title": "M4.1 · Product contracts", + "type": "checker" + }, + { + "id": "m42", + "title": "M4.2 · Source-neutral graph", + "type": "checker" + }, + { + "id": "m43", + "title": "M4.3 · DetectorProvider", + "type": "checker" + }, + { + "id": "m44", + "title": "M4.4 · Geometry/range", + "type": "checker" + }, + { + "id": "m45", + "title": "M4.5 · Temporal/motion", + "type": "checker" + }, + { + "id": "m46", + "title": "M4.6 · Replay threat", + "type": "checker" + }, + { + "id": "blocker-lio-increment-rolling-map", + "title": "RESOLVED · lio_pcl increment → rolling local map · frame 1880", + "type": "text" + }, + { + "id": "m45r", + "title": "M4.5R · K1 increment → rolling local obstacle map", + "type": "checker" + }, + { + "id": "m45r-m46-v3-scope", + "title": "M4.5R/M4.6 v3 · цель, решение и границы · 2026-08-05", + "type": "text" + }, + { + "id": "m45r-m46-v3-implementation", + "title": "M4.5R/M4.6 v3 · реализация и доказательства · 2026-08-05", + "type": "text" + }, + { + "id": "m45r-m46-v3-result", + "title": "M4.5R/M4.6 v3 · результат, ограничения и следующий gate · 2026-08-05", + "type": "text" + }, + { + "id": "m47", + "title": "M4.7 · Worker 006 shadow/cutover", + "type": "checker" + }, + { + "id": "m48", + "title": "M4.8 · Object-centric quality", + "type": "checker" + }, + { + "id": "m49", + "title": "M4.9 · Recorded-realtime RC", + "type": "checker" + }, + { + "id": "impl-m43-execution", + "title": "M4.3 execution seam · промежуточная фиксация · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m43-portable", + "title": "M4.3 portable worker seam · промежуточная фиксация · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m43-shadow-artifact", + "title": "M4.3 принято на Worker 006 · Mission Core-only · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m44-geometry", + "title": "M4.4 принято · canonical geometry/range · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m45-temporal", + "title": "M4.5 принято · bounded temporal/motion · 2026-08-05", + "type": "text" + }, + { + "id": "impl-m46-threat", + "title": "M4.6 body-frame v2 реализован · overall gate reopened · 2026-08-05", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-53", + "id": "e96ee79b-8e28-4c14-a125-8243b6bc4a0c", + "title": "LAB M4.8 · independent object-centric quality gate", + "updated_at": "2026-08-24T10:33:34.037579+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "m48-current-basis", + "title": "Текущая доказанная база", + "type": "text" + }, + { + "id": "m48-contract", + "title": "Контракт M4.8", + "type": "text" + }, + { + "id": "m48-visual", + "title": "Визуальное доказательство", + "type": "text" + }, + { + "id": "m48-acceptance", + "title": "Предзарегистрированные пороги", + "type": "text" + }, + { + "id": "m48-authority", + "title": "Граница authority", + "type": "text" + }, + { + "id": "m48-checker", + "title": "M4.8 execution gate", + "type": "checker" + }, + { + "id": "m48-implementation-20260824", + "title": "Реализация и архитектурная верификация · 2026-08-24", + "type": "text" + }, + { + "id": "m48-viewer-stabilization-20260824", + "title": "Стабилизация и hold recorded LAB viewer · 2026-08-24", + "type": "text" + }, + { + "id": "m48-runtime-status-20260824", + "title": "Проверенный runtime-статус · 2026-08-24", + "type": "text" + }, + { + "id": "m48-camera-spatial-sync-20260824", + "title": "CAMERA + 3D source-paced acceptance · 2026-08-24", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-54", + "id": "2d0853d3-800d-48c8-a6f9-7d47464cfb11", + "title": "LAB M48S · geometry-bound urban object semantics", + "updated_at": "2026-08-25T11:47:37.375887+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "m48s-boundary", + "title": "Архитектурная граница", + "type": "text" + }, + { + "id": "m48s-current", + "title": "Реализованный shadow · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-run", + "title": "Проверенные ранние Worker-прогоны · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-all-coco", + "title": "Фактическая реализация YOLOX all-COCO/v2", + "type": "text" + }, + { + "id": "m48s-fixed-tournament", + "title": "Fixed-class detector tournament · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-rfdetr-implementation", + "title": "RF-DETR TensorRT/Triton implementation · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-rfdetr-load", + "title": "30-minute Worker detector-load gate · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-limitations", + "title": "Решение и ограничения", + "type": "text" + }, + { + "id": "m48s-product-lab-20260825", + "title": "Canonical product LAB · 2026-08-25", + "type": "text" + }, + { + "id": "m48s-next-architecture", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "m48s-checker", + "title": "M48S execution gate", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-55", + "id": "4410bc1e-294c-4f89-ac9a-5b871768c87d", + "title": "Mission Core — M4.8S repeated load envelope / Worker 006", + "updated_at": "2026-08-25T19:45:54.919297+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "load-status", + "title": "Итог", + "type": "text" + }, + { + "id": "load-boundary", + "title": "Граница принятия", + "type": "text" + }, + { + "id": "load-method", + "title": "Метод", + "type": "text" + }, + { + "id": "load-results", + "title": "Принятые worst-case результаты", + "type": "text" + }, + { + "id": "load-capacity", + "title": "Worker envelope", + "type": "text" + }, + { + "id": "load-artifacts", + "title": "Immutable-доказательства", + "type": "text" + }, + { + "id": "load-implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "load-checker", + "title": "Acceptance gates", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-56", + "id": "b6cdcf1a-8398-4a21-afb8-6b95f7783a7c", + "title": "Mission Core — M4.8T semantic quality + temporal identity", + "updated_at": "2026-08-25T20:40:50.891415+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "m48t-status", + "title": "Текущий статус", + "type": "text" + }, + { + "id": "m48t-question", + "title": "Проверяемый вопрос", + "type": "text" + }, + { + "id": "m48t-boundary", + "title": "Граница этапа", + "type": "text" + }, + { + "id": "m48t-profile", + "title": "Замороженный профиль", + "type": "text" + }, + { + "id": "m48t-provenance", + "title": "Источник и воспроизводимость", + "type": "text" + }, + { + "id": "m48t-worker", + "title": "Worker 006 execution", + "type": "text" + }, + { + "id": "m48t-quality", + "title": "Semantic quality result", + "type": "text" + }, + { + "id": "m48t-failures", + "title": "Failure topology", + "type": "text" + }, + { + "id": "m48t-temporal", + "title": "Temporal semantic identity", + "type": "text" + }, + { + "id": "m48t-invariants", + "title": "Инварианты и authority", + "type": "text" + }, + { + "id": "m48t-lab", + "title": "Immutable LAB и проверка", + "type": "text" + }, + { + "id": "m48t-decision-next", + "title": "Решение и следующий этап", + "type": "text" + }, + { + "id": "m48t-checker", + "title": "Acceptance gates", + "type": "checker" + } + ], + "comments": [ + { + "id": "29af95db-bb19-40db-87bb-e9977d39da9c", + "created_at": "2026-08-25T20:46:41.723090+00:00", + "updated_at": "2026-08-25T20:46:41.723115+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-57", + "id": "d6da107b-23b0-42b3-8696-b8978f17d0e4", + "title": "Mission Core — RF-DETR upstream / TensorRT COCO parity", + "updated_at": "2026-08-25T21:47:13.215084+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "parity-result", + "title": "Итог", + "type": "text" + }, + { + "id": "parity-contract", + "title": "Строгий контракт", + "type": "text" + }, + { + "id": "parity-metrics", + "title": "Полный Worker 006 результат", + "type": "text" + }, + { + "id": "parity-diagnosis", + "title": "Локализация причины", + "type": "text" + }, + { + "id": "parity-evidence", + "title": "Immutable evidence", + "type": "text" + }, + { + "id": "parity-implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "parity-next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "parity-checker", + "title": "Acceptance gates", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-58", + "id": "a0d167d4-2fd6-4165-8f70-47a588c8efc9", + "title": "Mission Core — M4.8N native raw-KB4 RF-DETR shadow", + "updated_at": "2026-08-25T23:26:01.702505+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "m48n-result", + "title": "Итог", + "type": "text" + }, + { + "id": "m48n-contract", + "title": "Нативный контракт", + "type": "text" + }, + { + "id": "m48n-parity", + "title": "Parity и граница качества", + "type": "text" + }, + { + "id": "m48n-worker", + "title": "Worker 006 envelope", + "type": "text" + }, + { + "id": "m48n-evidence", + "title": "Immutable evidence", + "type": "text" + }, + { + "id": "m48n-implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "m48n-next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "m48n-checker", + "title": "Runtime acceptance gates", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-59", + "id": "00fb74c7-3714-441e-948a-84e16d15d322", + "title": "Mission Core — M4.8R2 static occupancy qualification", + "updated_at": "2026-08-26T08:38:16.585292+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "m48r2-objective", + "title": "Цель и архитектурный этап", + "type": "text" + }, + { + "id": "m48r2-question", + "title": "Проверяемый вопрос и гипотеза", + "type": "text" + }, + { + "id": "m48r2-source", + "title": "Immutable source evidence и границы", + "type": "text" + }, + { + "id": "m48r2-method", + "title": "Метод и конфигурация", + "type": "text" + }, + { + "id": "m48r2-runtime", + "title": "Runtime topology и ресурсная политика", + "type": "text" + }, + { + "id": "m48r2-implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "m48r2-validation", + "title": "Проверка и воспроизводимость", + "type": "text" + }, + { + "id": "m48r2-results", + "title": "Количественные и качественные результаты", + "type": "text" + }, + { + "id": "m48r2-limits", + "title": "Ограничения и отклонённые подходы", + "type": "text" + }, + { + "id": "m48r2-decision", + "title": "Решение", + "type": "text" + }, + { + "id": "m48r2-next", + "title": "Следующий этап", + "type": "text" + }, + { + "id": "m48r2-checker", + "title": "Acceptance checker", + "type": "checker" + } + ], + "comments": [ + { + "id": "8a92f7b3-731b-4b05-8a2c-18e6875d2fc1", + "created_at": "2026-08-26T08:53:34.119529+00:00", + "updated_at": "2026-08-26T08:53:34.119559+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-60", + "id": "3c69a3f5-fb89-4d52-a4ef-ae6eb84232fc", + "title": "Mission Core — M4.8R3 static occupancy Worker shadow", + "updated_at": "2026-08-26T11:28:45.287158+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "method", + "title": "Метод и реализация", + "type": "text" + }, + { + "id": "gates", + "title": "Принятые ворота", + "type": "text" + }, + { + "id": "resources", + "title": "Нагрузка Worker", + "type": "text" + }, + { + "id": "output", + "title": "Результат и доказательства", + "type": "text" + }, + { + "id": "authority", + "title": "Граница доказанного и решение", + "type": "text" + }, + { + "id": "checker", + "title": "Критерии приёмки M4.8R3", + "type": "checker" + } + ], + "comments": [ + { + "id": "0e4d5ec8-8abf-40e5-ad0a-377503576055", + "created_at": "2026-08-26T08:53:45.176534+00:00", + "updated_at": "2026-08-26T08:53:45.176563+00:00" + }, + { + "id": "4099fec9-fdd0-4f4e-b70e-db1c14c7e4ab", + "created_at": "2026-08-26T11:28:37.578927+00:00", + "updated_at": "2026-08-26T11:28:37.578956+00:00" + }, + { + "id": "ad6e6a20-431d-4e25-8488-352f996cf75b", + "created_at": "2026-08-26T12:30:51.216130+00:00", + "updated_at": "2026-08-26T12:30:51.216159+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-61", + "id": "1aeb44df-e2cd-4e47-bb4b-0a53a6258f5c", + "title": "Mission Core — M4.9T4 TRAVEL TGS fail-closed visual gate", + "updated_at": "2026-08-27T12:51:40.650315+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "compatibility", + "title": "Совместимость и решение по upstream", + "type": "text" + }, + { + "id": "evidence", + "title": "Immutable evidence", + "type": "text" + }, + { + "id": "runtime", + "title": "Нагрузка и границы", + "type": "text" + }, + { + "id": "publication", + "title": "Каноническая LAB-публикация", + "type": "text" + }, + { + "id": "validation", + "title": "Проверка реализации", + "type": "text" + }, + { + "id": "decision", + "title": "Текущее решение", + "type": "text" + }, + { + "id": "next", + "title": "Следующий gate", + "type": "text" + }, + { + "id": "checker", + "title": "Acceptance checker M4.9T4", + "type": "checker" + }, + { + "id": "historical-status-and-supersession", + "title": "Исторический статус и supersession", + "type": "text" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-62", + "id": "de886070-856f-440d-8f01-742174694577", + "title": "Mission Core — M4.9T5 full source-paced TGS shadow", + "updated_at": "2026-08-27T12:54:48.733532+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "identity", + "title": "Frozen identity", + "type": "text" + }, + { + "id": "full-run", + "title": "Полный recorded-source результат", + "type": "text" + }, + { + "id": "fail-closed", + "title": "Fail-closed поведение", + "type": "text" + }, + { + "id": "publication", + "title": "LAB и playback", + "type": "text" + }, + { + "id": "decision", + "title": "Решение и граница authority", + "type": "text" + }, + { + "id": "checker", + "title": "Acceptance checker M4.9T5", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-63", + "id": "3950165b-ede5-4db1-b6dc-1e86285592dc", + "title": "Mission Core — M4.9 integrated TGS + reference graph load gate", + "updated_at": "2026-08-27T12:56:35.104625+00:00", + "k1_label": true, + "review_scope": "downstream-k1-evidence-consumer-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "identity", + "title": "Frozen identity", + "type": "text" + }, + { + "id": "runtime", + "title": "Интегрированный runtime result", + "type": "text" + }, + { + "id": "resources", + "title": "Resource envelope", + "type": "text" + }, + { + "id": "recovery", + "title": "Найденный runtime lifecycle defect", + "type": "text" + }, + { + "id": "decision", + "title": "Решение и граница authority", + "type": "text" + }, + { + "id": "checker", + "title": "Integrated load acceptance", + "type": "checker" + } + ], + "comments": [], + "comments_complete": true + }, + { + "key": "MISSIONCOR-64", + "id": "37cbbc27-2121-4c0b-b1f7-37cd3748fa7b", + "title": "Mission Core — LAB sealed spatial playback data plane", + "updated_at": "2026-08-27T12:58:12.660497+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "objective", + "title": "Цель", + "type": "text" + }, + { + "id": "problem", + "title": "Исходная проблема", + "type": "text" + }, + { + "id": "products", + "title": "Sealed playback products", + "type": "text" + }, + { + "id": "delivery", + "title": "Локальная доставка", + "type": "text" + }, + { + "id": "browser", + "title": "Browser acceptance", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "authority", + "title": "Граница authority", + "type": "text" + }, + { + "id": "checker", + "title": "Playback acceptance", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-65", + "id": "88aff22a-43aa-48fb-b7eb-ac424d987d37", + "title": "LAB V1 — Vegetation semantics and mission terrain policy tournament", + "updated_at": "2026-08-29T11:29:12.983732+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "objective", + "title": "Цель и архитектурный этап", + "type": "text" + }, + { + "id": "current", + "title": "Текущая архитектура", + "type": "text" + }, + { + "id": "planned", + "title": "Планируемая архитектура", + "type": "text" + }, + { + "id": "sources", + "title": "Кандидаты источников и моделей", + "type": "text" + }, + { + "id": "method", + "title": "Метод и приёмка кандидатов", + "type": "text" + }, + { + "id": "mission_policy", + "title": "Контракт правил миссии", + "type": "text" + }, + { + "id": "safety", + "title": "Ограничения безопасности", + "type": "text" + }, + { + "id": "phases", + "title": "Блоки выполнения", + "type": "checker" + }, + { + "id": "definition_done", + "title": "Критерии допуска к следующему этапу", + "type": "checker" + }, + { + "id": "implementation_20260828_layers_archive_fov", + "title": "Реализация 2026-08-28 — слои, архив и valid FOV", + "type": "text" + }, + { + "id": "implementation_20260829_rav004tree_full_worker_pass", + "title": "Реализация 2026-08-29 — полный RAVNOVES004TREE на Worker 006", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-66", + "id": "baa6f4b9-c1fa-4778-811b-ac5f58475687", + "title": "Mission Core. Канон интеграции Rerun", + "updated_at": "2026-09-05T06:23:13.197385+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "rerun-audit-20260905", + "title": "Актуальная реализация и карта переноса · 5 сентября 2026", + "type": "text" + }, + { + "id": "rerun_role", + "title": "Назначение и граница ответственности", + "type": "text" + }, + { + "id": "rerun_owners", + "title": "Канонические поверхности и владельцы", + "type": "text" + }, + { + "id": "rerun_forbidden", + "title": "Запрещённые изменения", + "type": "text" + }, + { + "id": "rerun_lifecycle", + "title": "Lifecycle 3D-сцены", + "type": "text" + }, + { + "id": "rerun_recorded_camera", + "title": "Записанная камера и fMP4", + "type": "text" + }, + { + "id": "rerun_clock", + "title": "Единая временная шкала", + "type": "text" + }, + { + "id": "rerun_progressive", + "title": "Автономность и производительность", + "type": "text" + }, + { + "id": "rerun_errors", + "title": "Ошибки и восстановление", + "type": "text" + }, + { + "id": "rerun_change_protocol", + "title": "Протокол изменения", + "type": "text" + }, + { + "id": "rerun_incident_memory", + "title": "Память об инцидентах", + "type": "text" + }, + { + "id": "rerun_current_baseline", + "title": "Проверенный baseline на 2026-08-29", + "type": "text" + }, + { + "id": "rerun_prechange_gate", + "title": "Обязательный pre-change gate", + "type": "checker" + }, + { + "id": "rerun_acceptance_gate", + "title": "Обязательная replay-регрессия", + "type": "checker" + }, + { + "id": "rerun_review_rule", + "title": "Правило приёмки изменения", + "type": "checker" + } + ], + "comments": [ + { + "id": "7a95f1c0-f2c4-429a-9b3d-dd688425471c", + "created_at": "2026-08-29T07:19:08.818864+00:00", + "updated_at": "2026-08-29T07:19:08.818890+00:00" + } + ], + "comments_complete": true + }, + { + "key": "MISSIONCOR-67", + "id": "5ce88021-6d6a-4092-a2c4-f0b88467e473", + "title": "SIM Gaussian — XGRIDS-миры и AutoCap", + "updated_at": "2026-08-29T12:17:34.169960+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "gaussian-current", + "title": "Текущая архитектура", + "type": "text" + }, + { + "id": "gaussian-worker", + "title": "Worker 006", + "type": "text" + }, + { + "id": "gaussian-autocap", + "title": "AutoCap MAROSEYKA", + "type": "text" + }, + { + "id": "gaussian-integration", + "title": "Интеграция Mission Core", + "type": "text" + }, + { + "id": "gaussian-implementation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "gaussian-limitations", + "title": "Решение и ограничения", + "type": "text" + }, + { + "id": "gaussian-checker", + "title": "Контроль фазы", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-68", + "id": "0cad0189-6df7-4c36-a51c-44f0491a779c", + "title": "SIM UGV — калибруемая физика PlayCanvas", + "updated_at": "2026-08-29T16:12:08.844289+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "ugv-goal", + "title": "Цель и граница", + "type": "text" + }, + { + "id": "ugv-baseline", + "title": "Текущий baseline", + "type": "text" + }, + { + "id": "ugv-observations", + "title": "Наблюдаемые эффекты", + "type": "text" + }, + { + "id": "ugv-physics", + "title": "Физическая модель", + "type": "text" + }, + { + "id": "ugv-acceptance", + "title": "Калибровка и приёмка", + "type": "text" + }, + { + "id": "ugv-shortcuts", + "title": "Запрещённые shortcuts", + "type": "text" + }, + { + "id": "ugv-validation", + "title": "Реализация и проверка", + "type": "text" + }, + { + "id": "ugv-checker", + "title": "Контроль UGV", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-69", + "id": "764f9de3-a62e-4cec-8c22-fec2df2720d6", + "title": "SIM Gaussian — AI Regeneration и visual repair", + "updated_at": "2026-08-29T12:17:19.321536+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "status", + "title": "Статус и назначение", + "type": "text" + }, + { + "id": "verified-current-state", + "title": "Подтверждённое текущее состояние", + "type": "text" + }, + { + "id": "core-decision", + "title": "Ключевое техническое решение", + "type": "text" + }, + { + "id": "xgrids-input", + "title": "Требуемые данные XGRIDS", + "type": "text" + }, + { + "id": "algorithm-selection", + "title": "Выбранный экспериментальный стек", + "type": "text" + }, + { + "id": "comparison-baseline", + "title": "Обязательный A/B baseline", + "type": "text" + }, + { + "id": "alternatives", + "title": "Исследованные альтернативы", + "type": "text" + }, + { + "id": "worker-architecture", + "title": "Целевая worker-архитектура", + "type": "text" + }, + { + "id": "request-contract", + "title": "Контракт запроса", + "type": "text" + }, + { + "id": "result-contract", + "title": "Контракт результата и provenance", + "type": "text" + }, + { + "id": "product-surface", + "title": "Продуктовая интеграция Mission Core", + "type": "text" + }, + { + "id": "safety-boundary", + "title": "Границы доверия и риски", + "type": "text" + }, + { + "id": "estimates", + "title": "Предварительная оценка", + "type": "text" + }, + { + "id": "references", + "title": "Артефакты и первичные источники", + "type": "text" + }, + { + "id": "resume-gate", + "title": "Фаза 0 — условия возобновления", + "type": "checker" + }, + { + "id": "poc", + "title": "Фаза 1 — ROI PoC", + "type": "checker" + }, + { + "id": "product", + "title": "Фаза 2 — продуктовый контур", + "type": "checker" + }, + { + "id": "acceptance", + "title": "Критерии приёмки", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-70", + "id": "d9ee39f6-7107-467a-9895-60de823cf7a4", + "title": "CODEX_V5", + "updated_at": "2026-08-30T09:50:21.784029+00:00", + "k1_label": false, + "review_scope": "project-context-index" + }, + { + "key": "MISSIONCOR-71", + "id": "3f318724-bc92-40a5-95f4-c8235d489300", + "title": "Milestone 5 · OSS-конструктор Mission Core", + "updated_at": "2026-08-30T16:25:41.770003+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "m5-control", + "title": "Control Header", + "type": "text" + }, + { + "id": "m5-purpose", + "title": "Purpose / Goal — Milestone 5", + "type": "text" + }, + { + "id": "m5-hierarchy", + "title": "Governing hierarchy · Codex V5", + "type": "text" + }, + { + "id": "m5-done", + "title": "Acceptance / Done — Milestone 5", + "type": "text" + }, + { + "id": "m5-sources", + "title": "Authoritative Context / Sources", + "type": "text" + }, + { + "id": "m5-current", + "title": "Architecture — CURRENT · verified 2026-08-30", + "type": "text" + }, + { + "id": "m5-target", + "title": "Architecture — TARGET · global M5", + "type": "text" + }, + { + "id": "m5-components", + "title": "TARGET component ownership map", + "type": "text" + }, + { + "id": "m5-planes", + "title": "Dependency model and plane separation", + "type": "text" + }, + { + "id": "m5-oss-map", + "title": "OSS module map and isolation policy", + "type": "text" + }, + { + "id": "m5-gap", + "title": "CURRENT → TARGET gap map", + "type": "text" + }, + { + "id": "m5-boundaries", + "title": "Hard Constraints / Owner Boundaries", + "type": "text" + }, + { + "id": "m5-m4", + "title": "Milestone 4 and branch boundary", + "type": "text" + }, + { + "id": "m5-codex", + "title": "CODEX_V5 execution contract", + "type": "text" + }, + { + "id": "m5-freshness", + "title": "Documentation Freshness control", + "type": "text" + }, + { + "id": "m5-route", + "title": "Global Milestone Route", + "type": "text" + }, + { + "id": "m5-0-plan", + "title": "M5.0 · Baseline, architecture and execution freeze", + "type": "text" + }, + { + "id": "m5-0-check", + "title": "M5.0 acceptance", + "type": "checker" + }, + { + "id": "m5-1-plan", + "title": "M5.1 · Integration seam proven by Observatory / LAB Reset / CV Qualification", + "type": "text" + }, + { + "id": "m5-1-findings", + "title": "M5.1 · Surprises that changed the route", + "type": "text" + }, + { + "id": "m5-1-seam", + "title": "M5.1-A · Generic integration seam", + "type": "text" + }, + { + "id": "m5-1-observation", + "title": "M5.1-B · Observation Bundle and Playback Broker", + "type": "text" + }, + { + "id": "m5-1-render", + "title": "M5.1-C · Observatory and renderer gate", + "type": "text" + }, + { + "id": "m5-1-lab", + "title": "M5.1-D · LAB evidence extraction and runtime retirement", + "type": "text" + }, + { + "id": "m5-1-cv", + "title": "M5.1-E/F · Perception qualification truth and authority", + "type": "text" + }, + { + "id": "m5-1-cv-gates", + "title": "M5.1 · Proposed CV pass/fail gates", + "type": "text" + }, + { + "id": "m5-1-gates", + "title": "M5.1 gate route", + "type": "text" + }, + { + "id": "m5-1-check", + "title": "M5.1 acceptance", + "type": "checker" + }, + { + "id": "m5-1-branch", + "title": "M5.1 branch and safety policy", + "type": "text" + }, + { + "id": "m5-1-implementation-023151c", + "title": "M5.1 implementation checkpoint · 023151c", + "type": "text" + }, + { + "id": "m5-2-plan", + "title": "M5.2 · Semantic mission model and bounded orchestrator spike", + "type": "text" + }, + { + "id": "m5-2-check", + "title": "M5.2 acceptance", + "type": "checker" + }, + { + "id": "m5-3-plan", + "title": "M5.3 · Generic Research Workbench adapters", + "type": "text" + }, + { + "id": "m5-3-check", + "title": "M5.3 acceptance", + "type": "checker" + }, + { + "id": "m5-4-plan", + "title": "M5.4 · Video Data Plane / MediaMTX", + "type": "text" + }, + { + "id": "m5-4-check", + "title": "M5.4 acceptance", + "type": "checker" + }, + { + "id": "m5-5-plan", + "title": "M5.5 · Session interoperability / MCAP", + "type": "text" + }, + { + "id": "m5-5-check", + "title": "M5.5 acceptance", + "type": "checker" + }, + { + "id": "m5-6-plan", + "title": "M5.6 · Global product composition, cutover and documentation", + "type": "text" + }, + { + "id": "m5-6-check", + "title": "M5.6 acceptance", + "type": "checker" + }, + { + "id": "m5-progress", + "title": "Global Progress", + "type": "checker" + }, + { + "id": "m5-discoveries", + "title": "Surprises & Discoveries", + "type": "text" + }, + { + "id": "m5-decisions", + "title": "Decision Log", + "type": "text" + }, + { + "id": "m5-validation", + "title": "Validation & Evidence matrix", + "type": "text" + }, + { + "id": "m5-recovery", + "title": "Recovery and stopping rules", + "type": "text" + }, + { + "id": "m5-handoff", + "title": "Handoff / Resume Point", + "type": "text" + }, + { + "id": "m5-outcomes", + "title": "Outcomes & Retrospective", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-72", + "id": "4d8079b2-6a3e-4822-9e12-dcd4a4f20017", + "title": "Observatory — сохранённый M49 replay с Core", + "updated_at": "2026-09-03T10:24:31.424312+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "objective", + "title": "Цель и архитектурный этап", + "type": "text" + }, + { + "id": "question", + "title": "Вопрос и гипотеза", + "type": "text" + }, + { + "id": "source", + "title": "Неизменяемые источники и границы", + "type": "text" + }, + { + "id": "method", + "title": "Метод и состав", + "type": "text" + }, + { + "id": "runtime", + "title": "Runtime и ресурсы", + "type": "text" + }, + { + "id": "implementation", + "title": "Реализация", + "type": "text" + }, + { + "id": "validation", + "title": "Проверки и воспроизведение", + "type": "text" + }, + { + "id": "results", + "title": "Результат и измерения", + "type": "text" + }, + { + "id": "limits", + "title": "Регрессии, отклонённые решения и ограничения", + "type": "text" + }, + { + "id": "decision", + "title": "Решение", + "type": "text" + }, + { + "id": "next", + "title": "Следующий этап и запрещённые полномочия", + "type": "text" + }, + { + "id": "acceptance", + "title": "Приёмка saved replay / 2B", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-73", + "id": "a6e5bea8-5441-44ca-957f-1c117b1decf3", + "title": "AI Inference · История перехода к модульным профилям", + "updated_at": "2026-09-05T07:00:00.753453+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "ai-inference-history-current-20260905", + "title": "Актуальная точка входа · AI Inference #75", + "type": "text" + }, + { + "id": "modular-handoff-1", + "title": "Objective and architecture stage", + "type": "text" + }, + { + "id": "modular-handoff-2", + "title": "Decision question and hypothesis", + "type": "text" + }, + { + "id": "modular-handoff-3", + "title": "Immutable evidence and bounds", + "type": "text" + }, + { + "id": "modular-handoff-4", + "title": "Method, models, algorithms and identities", + "type": "text" + }, + { + "id": "modular-handoff-5", + "title": "Worker/runtime topology and resource policy", + "type": "text" + }, + { + "id": "modular-handoff-6", + "title": "Implementation and bounded cleanup", + "type": "text" + }, + { + "id": "modular-handoff-7", + "title": "Validation and reproduced evidence", + "type": "text" + }, + { + "id": "modular-handoff-8", + "title": "Results retained, not newly computed", + "type": "text" + }, + { + "id": "modular-handoff-9", + "title": "Limitations and rejected approaches", + "type": "text" + }, + { + "id": "modular-handoff-10", + "title": "Decision", + "type": "text" + }, + { + "id": "modular-handoff-11", + "title": "Next stage and forbidden authority", + "type": "text" + }, + { + "id": "modular-handoff-checker", + "title": "Acceptance checker", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-74", + "id": "b1c600bd-b6a2-4f82-ac6a-2e9f9db100d4", + "title": "Additional Core · Переносимая кастомизация Rerun", + "updated_at": "2026-09-05T06:22:32.405177+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "audit-baseline-20260905", + "title": "Зафиксированное состояние · 5 сентября 2026", + "type": "text" + }, + { + "id": "audit-provenance", + "title": "Официальное ядро и проверка происхождения", + "type": "text" + }, + { + "id": "architecture", + "title": "Граница и владельцы надстроек", + "type": "text" + }, + { + "id": "audit-runtime", + "title": "Изолированный runtime, протокол и освобождение ресурсов", + "type": "text" + }, + { + "id": "audit-sources-clock", + "title": "Источники, admission и единые часы", + "type": "text" + }, + { + "id": "audit-blueprint", + "title": "Blueprint: слои, идентичность и внутренние Python API", + "type": "text" + }, + { + "id": "audit-leases", + "title": "Blueprint lease, bounds и цветовые cache", + "type": "text" + }, + { + "id": "audit-camera", + "title": "Навигация и FOLLOW: фактический механизм", + "type": "text" + }, + { + "id": "audit-layout", + "title": "Viewport UI, разделитель и native chrome", + "type": "text" + }, + { + "id": "audit-capabilities", + "title": "Конфигурация, онтология и точный смысл кнопок", + "type": "text" + }, + { + "id": "audit-settings", + "title": "Display profile LAB: хранение и применяемые поля", + "type": "text" + }, + { + "id": "audit-producers", + "title": "RRD producers и данные для официального viewer", + "type": "text" + }, + { + "id": "audit-legacy", + "title": "Архивный форк 0.34.1 · сохранён, не активен", + "type": "text" + }, + { + "id": "acceptance", + "title": "Что остаётся чувствительным и что не подтверждено", + "type": "text" + }, + { + "id": "audit-verification", + "title": "Свидетельства этой ревизии и воспроизведение", + "type": "text" + }, + { + "id": "upgrade", + "title": "Обязательная проверка при следующем обновлении", + "type": "checker" + }, + { + "id": "follow-transition-20260905", + "title": "Исправление FOLLOW · 5 сентября 2026", + "type": "text" + } + ] + }, + { + "key": "MISSIONCOR-75", + "id": "e5768b6c-0817-4e9e-a786-f9167586c7dc", + "title": "AI Inference · Принципы работы и архитектура", + "updated_at": "2026-09-05T07:06:18.941912+00:00", + "k1_label": false, + "review_scope": "project-context-index", + "blocks": [ + { + "id": "ai-inference-20260905-1", + "title": "01. Решение и границы закрытия", + "type": "text" + }, + { + "id": "ai-inference-20260905-2", + "title": "02. Что является источником правды", + "type": "text" + }, + { + "id": "ai-inference-20260905-3", + "title": "03. Основные сущности и их владельцы", + "type": "text" + }, + { + "id": "ai-inference-20260905-4", + "title": "04. Пользовательский путь", + "type": "text" + }, + { + "id": "ai-inference-20260905-5", + "title": "05. Каталог модулей и зависимости", + "type": "text" + }, + { + "id": "ai-inference-20260905-6", + "title": "06. Граф: декларация и реальное исполнение", + "type": "text" + }, + { + "id": "ai-inference-20260905-7", + "title": "07. Схема коммуникации", + "type": "text" + }, + { + "id": "ai-inference-20260905-8", + "title": "08. Адресация, туннель и доверие", + "type": "text" + }, + { + "id": "ai-inference-20260905-9", + "title": "09. Lease, очередь и восстановление", + "type": "text" + }, + { + "id": "ai-inference-20260905-10", + "title": "10. Docker: что действительно упаковано", + "type": "text" + }, + { + "id": "ai-inference-20260905-11", + "title": "11. Зафиксированные образы, модели и releases", + "type": "text" + }, + { + "id": "ai-inference-20260905-12", + "title": "12. Ресурсы и изоляция compute", + "type": "text" + }, + { + "id": "ai-inference-20260905-13", + "title": "13. Передача данных и фактический кэш", + "type": "text" + }, + { + "id": "ai-inference-20260905-14", + "title": "14. Результат и публикация", + "type": "text" + }, + { + "id": "ai-inference-20260905-15", + "title": "15. Что действительно есть в текущем каталоге", + "type": "text" + }, + { + "id": "ai-inference-20260905-16", + "title": "16. Реальное покрытие и смысл измерений", + "type": "text" + }, + { + "id": "ai-inference-20260905-17", + "title": "17. Предыдущий streaming experiment и его результат", + "type": "text" + }, + { + "id": "ai-inference-20260905-18", + "title": "18. Локальная онтология и связи с оборудованием", + "type": "text" + }, + { + "id": "ai-inference-20260905-19", + "title": "19. Rerun: один runtime, локальные контролы и сохранение", + "type": "text" + }, + { + "id": "ai-inference-20260905-20", + "title": "20. Переносимость — что возможно и чего ещё нет", + "type": "text" + }, + { + "id": "ai-inference-20260905-21", + "title": "21. Архив Desktop без потери контекста", + "type": "text" + }, + { + "id": "ai-inference-20260905-22", + "title": "22. Проверки этого изменения и открытые границы", + "type": "text" + }, + { + "id": "ai-inference-20260905-23", + "title": "23. Изменённые файлы и эксплуатационная фиксация", + "type": "text" + }, + { + "id": "ai-inference-20260905-publication", + "title": "Опубликованный baseline · main 57f2537", + "type": "text" + }, + { + "id": "ai-inference-20260905-acceptance", + "title": "Фиксация среза и оставшаяся приёмка", + "type": "checker" + } + ] + }, + { + "key": "MISSIONCOR-76", + "id": "e48dc205-e73b-42c9-8b5c-b8d52f70267a", + "title": "Mission Core Node · Архитектурные границы для бортового ПК", + "updated_at": "2026-09-05T17:33:19.881635+00:00", + "k1_label": false, + "review_scope": "bridge-or-adjacent-contract", + "blocks": [ + { + "id": "baseline-governance", + "title": "00 · Базовая версия и порядок изменений", + "type": "text" + }, + { + "id": "requirements", + "title": "01 · Базовые требования владельца", + "type": "text" + }, + { + "id": "scope-host", + "title": "02 · Физический контур и границы этапа", + "type": "text" + }, + { + "id": "current-foundation", + "title": "03 · Существующая архитектурная основа Mission Core", + "type": "text" + }, + { + "id": "ownership", + "title": "04 · Владельцы функций и данных", + "type": "text" + }, + { + "id": "identity-contracts", + "title": "05 · Модель борта, устройств и контрактов", + "type": "text" + }, + { + "id": "runtime-stack", + "title": "06 · Runtime и изоляция процессов", + "type": "text" + }, + { + "id": "installer", + "title": "07 · Установщик и первый запуск", + "type": "text" + }, + { + "id": "lifecycle-storage", + "title": "08 · Headless-работа, права и обновления", + "type": "text" + }, + { + "id": "pairing", + "title": "09 · Сопряжение Node → Mission Core", + "type": "text" + }, + { + "id": "network", + "title": "10 · Частная связь и сменяемый Tailscale", + "type": "text" + }, + { + "id": "control", + "title": "11 · Канал управления и authority", + "type": "text" + }, + { + "id": "streams", + "title": "12 · Видео, метрические данные и WebRTC", + "type": "text" + }, + { + "id": "bandwidth", + "title": "13 · Полнота слоёв, профили и ресурсные ограничения", + "type": "text" + }, + { + "id": "realsense", + "title": "14 · RealSense D455: данные и полный интерфейс управления", + "type": "text" + }, + { + "id": "k1", + "title": "15 · XGRIDS K1: перенос на Linux и сохранение паритета", + "type": "text" + }, + { + "id": "recordings", + "title": "16 · Локальная запись, каталог и передача в Core", + "type": "text" + }, + { + "id": "geometry", + "title": "17 · Время, геометрия и границы вычислений", + "type": "text" + }, + { + "id": "ui", + "title": "18 · Локальный интерфейс и представление в Mission Core", + "type": "text" + }, + { + "id": "diagnostics", + "title": "19 · Диагностика и восстановление", + "type": "text" + }, + { + "id": "os-hardware", + "title": "20 · Ubuntu и конфигурация Mac mini", + "type": "text" + }, + { + "id": "insta", + "title": "21 · Insta360 и расширение оборудования", + "type": "text" + }, + { + "id": "plan", + "title": "22 · План реализации и проверяемые выходы этапов", + "type": "text" + }, + { + "id": "acceptance", + "title": "23 · Критерии приёмки первой версии", + "type": "checker" + }, + { + "id": "verification-gaps", + "title": "24 · Что требует проверки, не меняя базовых требований", + "type": "text" + }, + { + "id": "sources", + "title": "25 · Основания и проверяемые источники", + "type": "text" + } + ], + "comments": [ + { + "id": "249fec09-b255-498a-9806-8ea8f08b424a", + "created_at": "2026-09-05T08:22:34.159519+00:00", + "updated_at": "2026-09-05T08:22:34.159546+00:00" + }, + { + "id": "f54a9e3f-f167-4f30-8119-1ac662ff46c3", + "created_at": "2026-09-05T08:36:38.364501+00:00", + "updated_at": "2026-09-05T08:36:38.364527+00:00" + }, + { + "id": "5409348b-d466-4758-ae0e-dcdaf0f37afb", + "created_at": "2026-09-05T09:48:56.352696+00:00", + "updated_at": "2026-09-05T09:48:56.352728+00:00" + }, + { + "id": "f8c07edb-5518-4208-94b7-4d8d10d68b2f", + "created_at": "2026-09-05T10:22:03.549952+00:00", + "updated_at": "2026-09-05T10:22:03.549977+00:00" + }, + { + "id": "06a5c930-2575-4a96-a260-5e330fa82670", + "created_at": "2026-09-05T11:06:22.520250+00:00", + "updated_at": "2026-09-05T11:06:22.520280+00:00" + }, + { + "id": "d89e2929-e388-4408-b49a-5d9fa5db60c2", + "created_at": "2026-09-05T12:00:19.969869+00:00", + "updated_at": "2026-09-05T12:00:19.969898+00:00" + }, + { + "id": "288f1cf2-368d-4039-90e5-d929e9fa01f7", + "created_at": "2026-09-05T13:14:31.122033+00:00", + "updated_at": "2026-09-05T13:14:31.122059+00:00" + }, + { + "id": "b02aa226-9f6c-48f5-bff5-7af3c45541e5", + "created_at": "2026-09-05T14:12:55.531058+00:00", + "updated_at": "2026-09-05T14:12:55.531086+00:00" + }, + { + "id": "b52fc499-7ea5-4828-ac21-1d26d5d43860", + "created_at": "2026-09-05T14:32:11.926721+00:00", + "updated_at": "2026-09-05T14:33:33.535465+00:00" + }, + { + "id": "fd3ce187-f5e9-4538-a73b-2e0f895cddde", + "created_at": "2026-09-05T16:59:56.378949+00:00", + "updated_at": "2026-09-05T16:59:56.378977+00:00" + }, + { + "id": "d42e5560-f369-4d8c-9a7c-661fbc9f1c94", + "created_at": "2026-09-05T17:16:57.766438+00:00", + "updated_at": "2026-09-05T17:16:57.766464+00:00" + }, + { + "id": "d9fc7e48-49f1-4896-8b7e-6018cc7f3b6f", + "created_at": "2026-09-05T17:33:30.975596+00:00", + "updated_at": "2026-09-05T17:33:30.975627+00:00" + }, + { + "id": "972513e1-ea4f-4016-bcb0-199989ce0e8e", + "created_at": "2026-09-05T18:46:20.456938+00:00", + "updated_at": "2026-09-05T18:46:20.456964+00:00" + }, + { + "id": "c0d35dd3-9c8c-4192-aab7-1706b12d512c", + "created_at": "2026-09-05T22:13:48.724207+00:00", + "updated_at": "2026-09-05T22:13:48.724249+00:00" + }, + { + "id": "096a1731-da4d-4807-9cce-61d7797b44d8", + "created_at": "2026-09-05T22:27:14.000614+00:00", + "updated_at": "2026-09-05T22:27:14.000644+00:00" + }, + { + "id": "3fbb3e79-1200-4504-b8cc-39c899c56ca6", + "created_at": "2026-09-05T22:41:35.784135+00:00", + "updated_at": "2026-09-05T22:41:35.784167+00:00" + } + ], + "comments_complete": true + } + ] +} diff --git a/docs/audits/2026-09-06-k1-live-reference-camera-root.md b/docs/audits/2026-09-06-k1-live-reference-camera-root.md new file mode 100644 index 0000000..fad5052 --- /dev/null +++ b/docs/audits/2026-09-06-k1-live-reference-camera-root.md @@ -0,0 +1,129 @@ +# K1 live reference comparison and missing camera + +Scope: operator-local K1 Bridge data acquisition on canonical Mission Core 8000. +No new physical START, STOP, provisioning or RTSP request was sent during this +investigation. The comparison uses the existing 6 September capture. + +## Authoritative references found in Ops + +MISSIONCOR-3, “Mission Core. Lixel K1 / XGRIDS Integration”, block “Внутренние +эталоны Mission Core” and its comment dated 22 August 2026 identify: + +- Fast reference A: eaad9de, 20260822T105904Z_viewer_live. START to calibration + under 5 seconds; calibration 21 seconds; after calibration cloud 2 seconds, + right camera 4 seconds; STOP to physical onset 1 second. +- Recovery reference B: 1001a31, 20260822T130323Z_viewer_live. START 14 seconds; + calibration 22 seconds; cloud +1 second, camera +8 seconds; STOP onset 7 seconds. + +The linked canonical report is +[Lab 010](../lab/010_K1_MISSION_CORE_INTERNAL_LIVE_BASELINE_20260822.redacted.md). +These are measured internal references for one K1 FW 3.0.2 and Bridge/direct LAN, +not a transferable SLA or a standalone Rerun preset. A used a short physical +ledger; B retained mature recovery history. Source quality was not reduced. + +MISSIONCOR-66, “Mission Core. Канон интеграции Rerun”, and MISSIONCOR-74, +“Additional Core · Переносимая кастомизация Rerun”, define the separate +presentation contracts: + +| Surface | Current profile | Clock and media | +| --- | --- | --- | +| Live acquisition | live-acquisition | stream_time; upstream live Rerun; right camera through the independent RTSP → durable fMP4 → MSE path | +| Saved Sessions / Data | recorded-session | session_time; progressive recorded admission; RecordedFmp4Player follows the shared playback clock | +| Canonical LAB result | laboratory-result | session_time; result-specific settings; merged RRD with native AssetVideo/VideoFrameReference; separate presentation gate | + +The live receiver does not inherit the LAB full-readiness gate. Changing point +size, accumulation or blueprint cannot repair a camera producer that never +started. The current live display settings include accumulation 47 seconds, +point size 1 and height/viridis; no evidence identifies those visual values as +part of the fast reference, so they were not arbitrarily reset. + +### Settings isolation limitation found during the audit + +Distinct profile kinds, clocks and media admission do not prove complete +settings isolation. App.tsx still owns one sceneSettings/displayDraft pair for +live and Data, and useWorkspaceLayoutProfile() loads and saves one +observation.spatial profile containing scene settings. The normal settings +committer suppresses backend writes while recorded replay is presented, but it +still updates the common in-memory settings. The persisted layout restore/apply +path also has no profile-kind namespace. LAB uses its own resultId-scoped draft +and durable view profile. + +Therefore this audit confirms distinct presentation contracts and LAB settings +ownership, not full live/Data settings isolation. No shared settings, layout, +Rerun renderer or replay code was changed for this camera repair. The focused +profile tests below do not cover the remaining live/Data settings coupling. +Separating that storage and state requires its own transition/race and browser +regression checks; it must not be folded into a camera-path fix implicitly. + +## Observed 6 September failure + +Existing session 20260906T184240Z_viewer_live: + +| Metric | Fast A | Recovery B | Recent run | +| --- | ---: | ---: | ---: | +| MQTT callback → publication p50 | 23.839 ms | 83.934 ms | 93.151 ms | +| MQTT callback → publication p95 | 41.541 ms | 223.589 ms | 208.989 ms | +| Preview drops | 0 | 70 | 58 | +| Point decode errors | 0 | 0 | 0 | +| Camera archive | complete | complete | absent | + +Run lengths differ; drop counts are not normalized performance rates. Device +calibration onset and first visible pixels were not independently measured in +the recent run, so the historical operator timings are not falsely compared to +backend timestamps. The recent run published 1,028,061 points in 386 PCL frames. + +At 18:43:15 UTC the browser admitted a Rerun store; this alone is not proof of +visible point pixels. Between 18:43:20 and 18:43:50 the backend logged 22 failed +post-authoritative-PCL camera activations. First-PCL admission took 4–34 ms. +No camera producer activation success or camera media artifact exists in this +session. The private formatter discarded exception details, preventing recovery +of each historical exception stack from those records. + +## Reproduced storage defect and bounded repair + +The running checkout is separate from MISSIONCORE_DATA_DIR. Acquisition uses +resolve_missioncore_evidence_dir(), but XgridsK1CameraGateway previously confined +session paths to repository_root. The actual session is outside the checkout. +An offline call using the real existing session directory deterministically +raised “camera recording root must stay inside the repository” before authority +reservation, FFmpeg preparation or network I/O. Camera remained idle, matching +the observed pre-producer failure. This mismatch necessarily blocks recording +at the configured path even though the original exception stacks were lost. + +The gateway now receives an explicit evidence_root from the existing service +composition. It confines both acquisition-owned and selected-preview recording +to that root after resolving paths. It rejects sibling directories and symlink +escapes. The source checkout remains the FFmpeg-binary lookup root; the fallback +for standalone gateway callers preserves their existing repository confinement. +No RTSP arguments, video quality, stream choice, camera producer lifecycle, +START/STOP, MQTT dialogue, Rerun blueprint or LAB/archive viewer policy changed. + +Private exception diagnostics now retain only the exception class and final +filename/line/function. They omit exception text, locals, source lines and +absolute paths. This makes future failures attributable without leaking data. + +## Validation and remaining physical acceptance + +- Camera gateway suite: 42 passed, including seven new external-root, + composition-wiring and path-confinement cases. Synthetic FFmpeg produced + and archived media in the configured external directory. +- Camera acquisition lifecycle: 37 passed. +- Persistent diagnostics suite: 8 passed, including exception-location redaction. +- Focused frontend profile, environment, LAB view profile and recorded-camera + journal checks: 14 passed. These are contract-level checks, not physical + playback acceptance or proof of complete settings isolation. +- Mypy on camera.py and runtime_diagnostics.py: passed. +- Ruff and git diff --check: passed. +- Protocol, BLE provisioning/AP, physical ledger/coordinator, MQTT, + connection supervisor, runtime and archive remain identical to c041a56. +- The broad frozen-contour guard also includes camera.py, so it now deliberately + detects this narrow camera storage change. Its baseline was not advanced or + weakened. This is not a claim that the full freeze check passes unchanged. + +The canonical idle service was refreshed and its replacement was confirmed +ready on port 8000 at 19:10:22 UTC; no frontend rebuild was required. A new +operator-started physical run is still +needed to verify right-camera appearance, durable media and browser playback. +The existing failed session is preserved and is not retroactively repaired. + +Ops was consulted read-only. This local report was not published to a card. diff --git a/docs/audits/2026-09-06-k1-station-reply-semantics.md b/docs/audits/2026-09-06-k1-station-reply-semantics.md new file mode 100644 index 0000000..b7ad82f --- /dev/null +++ b/docs/audits/2026-09-06-k1-station-reply-semantics.md @@ -0,0 +1,95 @@ +# K1 station replies: misleading ATT error and fresh failures + +Scope: the two operator-started Bridge attempts on Core 8000 following the +operator-reported browser cache clear. No device write, START/STOP, Wi-Fi switch, +subnet scan or firmware execution was performed by this investigation. + +## Fresh evidence + +| Attempt, UTC | K1 first advertisement | Complete scan | Connect operation | Reply | +| --- | ---: | ---: | ---: | --- | +| 19:22:25 | 1.308 s | 6.366 s | 51.399 s | ATT 4 INVALID_PDU | +| 19:23:55 | 2.208 s | 6.023 s | 7.587 s | ATT 4 INVALID_PDU | + +Both attempts reached the validated GATT contract, baseline status read and +one 99-byte write-with-response. Neither write was acknowledged as successful. +The observed command capacity was 253 bytes. Empty passwords are rejected +before this path; presence does not prove correctness. Individual connect and +write durations were not recorded, so total latency is not falsely attributed +entirely to the Wi-Fi operation or entirely to CoreBluetooth. + +Private snapshots, operation identities and the disassembly are retained under +the ignored `.runtime/k1-connect-incident-20260906/fresh-1922/` directory with +0600 files, UTC/monotonic timestamps and a SHA-256 artifact index. + +## Firmware-level explanation + +The reviewed official FW 3.0.2 artifact is the immutable source documented in +[Lab 004](../lab/004_K1_FW302_AP_CREDENTIAL_PROVIDER_20260720.redacted.md). +Its extracted `lixel_nman` executable is 323,464 bytes, SHA-256 +`aead745e4e0073ae99e84e851e5560161f2d560d5804f21e5268116efbb1dc42`, +ELF build ID `205fb546e44667ca0e74159a7672269a81b754f5`. + +Bounded offline AArch64 disassembly established: + +1. At 0x14588 the service registers characteristic 7f01, with write callback + 0x17e48 selected at 0x14598. +2. Its station branch calls `wifi_connect` at 0x18034 → 0x15440. The return + value is retained in w28 at 0x18038 and passed as the write-result error at + 0x17fc4 → 0x291a8. +3. `wifi_connect` returns 4 in its network-not-found branches: 0x16190 and + 0x16a2c. Adjacent diagnostics refer explicitly to the SSID not being found. +4. The branch matching NetworkManager's credentials-required output returns + 6 at 0x1641c. This describes an unsuccessful credential path, not proof + of the exact incorrect character or how the form was populated. + +Thus the device's application return codes collide with generic ATT names: +4 is displayed as INVALID_PDU and 6 as REQUEST_NOT_SUPPORTED. The existing +Mission Core message discarded the reviewed device meaning. Neither changing +the 99-byte layout nor switching write mode follows from these observations. +The callback performs Wi-Fi work before returning, so its response is not merely +an instantaneous transport acknowledgment. + +This is an interpretation under the selected exact firmware profile. It is not +a universal ATT error mapping, proof of the live firmware before DeviceInfo, +or confirmation that the network configuration remained unchanged. The actual +SSID/radio condition still needs a corrected operator-run connection test. +The old screenshot displays a network name whose exact spelling was queried; +this report does not assume that spelling is a typo or assume K1 can see it. + +## Bounded correction + +`wifi_failure.py` owns the pure profile-scoped classification. The service uses +it only for Bridge/Direct, selected FW 3.0.2, an annotated gatt-write failure, +a dispatched/unconfirmed 99-byte write-with-response and BleakGATTProtocolError. +It retains the original exception code and raw ATT facts. Quick Connect, +baseline-read failures, other firmware and unrelated errors retain their +original diagnostics. Retry flags, mutation ledger and ambiguity remain intact. + +The frontend shares the station guidance between the form and recovery panel. +It offers “Указать сеть Wi-Fi заново” through the existing explicit reset and +scan flow. The pending stage explains K1 Wi-Fi connection. It does not populate, +store, inspect or change the password. The GATT helper, packet bytes, MQTT, +physical command control and all Rerun profiles are unchanged in this increment. + +## Validation + +- 19 focused backend cases passed: 16 classification boundary cases, two + service-level reply cases and the existing ambiguous-write recovery case. + They verify one submission, retained raw codes, failed/unconfirmed state and + an unresolved ledger rather than promoting an error to success. +- 24 classification/diagnostic cases passed; mypy and Ruff passed. +- 775 frontend tests passed, including rendered station guidance and the + explicit same-device network setup action. Architecture: 4 passed. +- Production typecheck and build passed. The replacement canonical Core was + confirmed ready at 2026-09-06T19:41:10.665304+00:00. +- Browser check: the real Core 8000 Test Devices page loaded; normal and expanded + sizes worked. The page was left open. No BLE action was clicked. The new error + messages were covered by rendered tests, not represented as a new hardware run. +- Hardware Wi-Fi acceptance remains pending the exact target SSID and an + operator-started attempt. macOS networksetup did not provide the current SSID; + its output was not treated as proof that the host network is down. + +Ops MISSIONCOR-3 was read through the direct MCP. Its iPhone network capture +starts at IP and does not contain Bluetooth HCI; it cannot establish the meaning +of this GATT callback. No Ops card was written. diff --git a/docs/audits/2026-09-06-node-k1-bridge-implementation.md b/docs/audits/2026-09-06-node-k1-bridge-implementation.md new file mode 100644 index 0000000..ea7a346 --- /dev/null +++ b/docs/audits/2026-09-06-node-k1-bridge-implementation.md @@ -0,0 +1,164 @@ +# K1 Bridge on the onboard computer + +Authority: the owner's 2026-09-06 request and two annotated Fleet screenshots. +The earlier Node context is historical evidence, not a new instruction. X4 work +is paused while its battery charges. Hardware acceptance below remains pending +until the owner powers on K1 and supplies the target WLAN in the application. + +## Product surface decision + +The operator occasionally adds a wireless sensor to one selected onboard +computer. The primary entity is that computer's device inventory. Discovery, +network observations, credentials and device control belong to that computer; +the operator's Mission Core is a paired remote console. Node's local console +uses the same form and backend contract. + +Selected composition: a plus beside refresh in the device inventory opens the +canonical modal Window, titled «Подключение устройства к БК». The target board +is visible before discovery or credentials. K1 is selected from an explicit +scan on that board. The operator chooses a board-observed WLAN or enters an +SSID, then submits one Bridge operation. Success requires observed device +connectivity; dispatch alone is never shown as connected. + +An independent Fleet connection workspace was considered: it separates the +action from its board and can imply operator-local radios. A new primary root +was also unnecessary. The owner explicitly selected the inventory plus and +authorized moving the existing operator-local connection workspace into LAB, +with all its current modes and functions, under «Тестовые устройства». +The stable workspace ID is retained for saved navigation. + +This is domain content in admitted list/detail and modal compositions. Reuse +`Window`, `WindowFooterActions`, `TextField` (including password), `Select`, +`Button`, `IconButton`, `ResourceRow`, `SettingsCard`, `StatusBadge`, +`ActivityIndicator`, `ToastStack`; icons `plus`, `refresh`, `network`, `camera`, +`eye`, `settings`, `close`, `play`, `stop`. These exist in the sibling Design +Guideline registries. No new generic visual entity is introduced. + +State grammar: board unavailable; service unavailable; ready to scan; +scanning; no candidates; candidate selected; network list unavailable with +manual entry; ready to connect; connecting; observed connected; failed before +write; outcome unknown after possible write. A stale discovery/runtime or +changed board invalidates the form. Closing the modal clears its password. +Closing after dispatch does not claim cancellation of a physical operation. + +## Execution and security boundaries + +Only Bridge is admitted on Node. Reuse the reviewed firmware 3.0.2 profile and +99-byte 7f01 operation with 7f02 observation and exact endpoint verification. +No Quick Connect, host association, AP enable, subnet scan or firmware action +is exposed by the Node adapter. BLE notifications, where used by the existing +profile, can entail the standard temporary CCCD subscription write. + +Use the existing authenticated Node/Core channel. A device-enrollment command +targets the paired node and a worker runtime/discovery generation, even before +a device session exists. Wi-Fi credentials must not enter the existing durable +sensor-command journal, Fleet database, error text, evidence or argv. Pending +secret payloads are short-lived memory only. A restart, expired command or +uncertain dispatch never retries a provisioning write with a new identity. +The Node worker is the sole hardware owner for both consoles. + +Linux adapters must observe BlueZ and NetworkManager on the board. Host-route +verification uses the kernel route to the exact K1 address; a tunnel/default +route cannot silently qualify as the required local Bridge path. Existing +macOS adapter behavior and the accepted local LAB workflows are retained. + +## Rerun profile boundary + +| Profile | Source / clock | Settings authority | Lifetime | +| --- | --- | --- | --- | +| Live acquisition | Current board camera/LiDAR / stream_time | Live preview settings | Active acquisition and execution binding | +| Recorded session | Immutable admitted recording / session_time | Session replay, trajectory, time and playback settings | Recording identity | +| Laboratory result | Immutable admitted result and recording / session_time | Result-specific scene, evidence layers, diagnostic selections | Result identity | + +The native renderer and recorded data pipeline can be shared, but each profile +has an explicit discriminator. Crossing profiles remounts the renderer so its +refs, subscriptions and pending recovery cannot leak into another profile. +LAB uses its own factory and result identity. Source recording/application IDs +remain unchanged: profile separation does not rewrite evidence lineage. + +The Node publishes native RRD through ordered WebRTC data channels and the +existing camera gateway publishes H.264/fMP4 through a second channel. Signalling +uses paired sensor operations; ICE admits private LAN/Tailscale host candidates +only, with no STUN/TURN. The Node Rerun sink opens no gRPC port. Both hosts inject +the existing isolated native Rerun renderer into the shared sensor UI. + +Two viewers at most are admitted. Decoded preview envelopes use latest-value +queues; encoded RRD bytes are never dropped within an open recording. Slow +consumers are closed. Camera preview leases share the canonical recording +producer. Closing a viewer releases its peer and delivery lease; acquisition +STOP remains a separate explicit operator action. These bounds concern delivery; +sustained native-viewer CPU/GPU/memory acceptance requires the real board test. + +Node live settings (point size, accumulation, color, palette, points, trajectory, +grid) invoke only `viewer.settings.update` behind `profile=live-acquisition`. +Recorded and LAB profiles keep their own controls. LAB result changes remount +the renderer as well as changes between the three profile kinds. + +## Validation and current acceptance + +- Focused backend tests: board binding, expiration, replay prevention, secret + non-persistence, restart/unknown outcomes and Linux route classifications. +- Architecture gate; frontend typecheck, unit tests and build sequentially. +- Node package build and installation provenance; service health after reboot. +- Browser: both plus controls, target board, scan/empty/error states, manual + SSID, password clearing, keyboard Escape, normal/expanded live viewer. +- Real K1: one explicit scan/select/Bridge, actual DeviceInfo verification, + device appears in both inventories; camera and LiDAR acquisition/stop; + reconnect and restart; regression of local LAB and recorded replay. + +No hardware acceptance or successful deployment is claimed by this document. + +## Completed validation + +- Core architecture gate: 4/4; complete frontend suite: 764/764 after updating + navigation expectations. Core and Node typechecks and production builds pass. +- Node Go tests pass, including redacted enrollment journal, one dispatch per + intent, restart outcome unknown, wrong node/expired request/host mutation deny. +- Focused Python Fleet, pairing, Node SDK, existing BLE scanner and Rerun tests + pass (one existing optional case skipped). Empty Node adapter construction, + public state and teardown pass locally, with no hardware discovery/listener. +- Six WebRTC tests pass, including an actual bounded loopback data-channel + roundtrip. Missing camera preserves the RRD channel; rejected offers release + the peer. Native RRD sink emits an RRF2 header without opening gRPC. +- All 51 Linux wheel hashes and archive members were checked. The only admitted + .pth is Rerun's literal package-directory declaration; bootstrap adds that + exact directory without executing path hooks. Ubuntu 24.04/amd64/Python 3.12 + and fixed root-owned runtime paths are enforced. Installation checks idle + acquisition and stops the old K1 worker before replacing its modules. +- Canonical Core checkout was updated and its exact launch agent restarted. + `/api/liveness` and the new enrollment API pass; the paired board remains + online. Exactly one integrated backend remains on 8000, none on 8765. +- In-app browser: Fleet no longer contains the old connection workspace; LAB + opens Test devices with the existing K1 scenario. The vehicle inventory plus + opens the board-scoped modal. Unavailable-service state and Escape were + verified against the real paired board, which still runs the prior Node. + +## Package and remaining work + +Prepared package: `apps/node-agent/build/mission-core-node_0.7.0_amd64.deb`. +Size: 429540264 bytes. SHA-256: +`10b1164b57c31cd3edcced8653faf14cb9dc459db06442f0b417bc7f23bd4e76`. +The adjacent provenance includes base revision, exact source hashes, pinned Go +and Design Guideline identity and the locked wheel manifest. Maintainer scripts +inside the package match the reviewed sources. + +The automatic approval reviewer rejected a proposed source-only Linux smoke +transfer because the board address was considered insufficiently authorized. +No proprietary source/package or application key was sent after that rejection. +Only dependency wheels and their build fetch script were staged in a separate +Downloads directory earlier; no board service/system installation has occurred. +An explicit confirmation of the exact target, package and encrypted credential +migration was requested after the concrete package was ready. + +After that confirmation: install the package with the board administrator's +normal authentication, import the existing application key via protected stdin +and `systemd-creds`, verify Linux worker/BlueZ/NetworkManager and both UIs, then +ask the owner to power K1 for the admitted Bridge and acquisition tests. Wi-Fi +credentials are entered in the application only. Do not claim hardware, reboot, +expanded live-view, camera/LiDAR or sustained-resource acceptance before these +checks. Board source archives are retained by the canonical K1 runtime; Node's +recorded-session browse/export surface is not introduced in this change. + +Direct Ops tools were unavailable in this session. This local audit records +profile distinctions and engineering evidence; no live Ops card update is +claimed. The user's historical attachments were context, not authorization. diff --git a/docs/audits/2026-09-07-k1-lab-acceptance-and-node-continuation.md b/docs/audits/2026-09-07-k1-lab-acceptance-and-node-continuation.md new file mode 100644 index 0000000..db3af32 --- /dev/null +++ b/docs/audits/2026-09-07-k1-lab-acceptance-and-node-continuation.md @@ -0,0 +1,62 @@ +# K1 LAB acceptance and onboard continuation + +## Accepted local baseline + +The owner confirmed a successful local LAB connection after correcting the +network name. Core independently recorded `network_applied` and DeviceInfo / +control ready in 8.298 seconds. In the next operator-run session the owner +reported the usual approximately 20-second calibration, prompt point-cloud +appearance, visible signal loss after disabling Wi-Fi and successful recovery +after enabling it again (approximately 20 seconds of waiting). These recovery +timings are operator observations, not newly instrumented latency measurements. +They qualify that local run, not every outage duration, a Node reboot or Ubuntu. + +The owner requested committing and pushing the working implementation and +resuming K1 Bridge on the paired onboard computer. The backend connection +read-model extraction, plugin-owned sensor UI, BLE discovery fixes, station +error messages, camera evidence-root handling, Node enrollment and separate +viewer-profile discriminators are included in this checkpoint. Earlier audit +documents retain their original time-scoped validation and limitations. + +## Spatial scene placement + +The owner explicitly moved the local test-device live scene from Control to +LAB. Its operator job is inspection of the current local test stream; source +selection, acquisition and live settings retain their existing ownership. +Keeping it under Control would imply the future operational board view; a new +root or duplicate viewer would add an unnecessary product surface. The existing +workspace is therefore registered under LAB with its stable `spatial-scene` ID, +renderer, settings and links intact. Existing sidebar, workspace shell and +`globe` icon are reused; no new Design Guideline primitive is introduced. + +Only obsolete Control quick links to that scene are retired on settings read. +Custom page copy, media and unrelated links remain intact. Default Control +shortcuts become cameras and map. Home and LAB links can still open the same +scene. This is an owner-approved relocation, with no Rerun parameter change. + +## Validation and onboard candidate + +- Environment migration, Node bridge, connection read-model and Fleet + enrollment tests passed: 36 cases. Ruff passed for the environment changes. +- Go package tests passed with the built Node UI embedded and an isolated + build cache. No system toolchain or package installation was needed. +- Core architecture/type checks passed. Full frontend run: 784/786 passed; + the two failures were old LAB workspace-list expectations. Those expectations + were updated and both affected suites passed; no production change followed. +- Node 0.7.1 is the next package version so the earlier 0.7.0 candidate is not + silently replaced. Build provenance now covers the moved K1 frontend sources. + The 51 K1 and 29 RealSense wheel hashes were verified before reuse. + +The current paired board was resolved from authenticated Core Fleet data and +its SSH host key matched the previously trusted Mini key. It runs Node 0.6.11. +Administrative installation requires the owner's normal Ubuntu authentication; +non-interactive sudo is unavailable. No password is requested in chat. + +The exact package, installation result, source commit and final Core UI +delivery are recorded in a subsequent release addendum after the build. +Device preparation and real Bridge acquisition must still be accepted through +both interfaces on that board. Operator and board WLANs remain independent; +the board owns Bluetooth, network observation, K1 commands and raw data. + +Ops direct tools are absent in this session. This local report is prepared for +the K1 and Node cards; no Ops publication is claimed. diff --git a/docs/audits/2026-09-07-k1-plugin-boundary-r1-sources.json b/docs/audits/2026-09-07-k1-plugin-boundary-r1-sources.json new file mode 100644 index 0000000..e623e53 --- /dev/null +++ b/docs/audits/2026-09-07-k1-plugin-boundary-r1-sources.json @@ -0,0 +1,145 @@ +{ + "stage": "k1-plugin-boundary-r1", + "source_files": [ + { + "path": "src/k1link/device_plugins/xgrids_k1/facade.py", + "sha256": "4d5144ffa261b4e436be037ed421a9a48386485ff81128aa2cf95ca6efe9f469", + "lines": 35549 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/connection_attempt.py", + "sha256": "0619c1555c182bb635272c66c998e457d3fb1378d221b440449b5b274fc2a27d", + "lines": 495 + }, + { + "path": "src/k1link/device_plugins/xgrids_k1/node_bridge.py", + "sha256": "11a58900ca1a8553e4301fa4a2cc61717b06a2b2ee18de412decf8c65ab2acdf", + "lines": 319 + }, + { + "path": "tests/test_k1_connection_read_model.py", + "sha256": "f3476dad252c9efb536fb6c1e7e84154e7704bae063fe7f1abd4fe536c4ed585", + "lines": 170 + }, + { + "path": "apps/control-station/src/core/device-plugins/contracts.ts", + "sha256": "c01d9f81d554d7c3155576eee148cf62f3ddbb891ef718c259ef5e4ad0190070", + "lines": 125 + }, + { + "path": "apps/control-station/src/workspaces/fleet/VehicleSensors.tsx", + "sha256": "d0922fe89b0e9162c045cbf6163737b789af0aa1239034b7bd7596fa9a961897", + "lines": 24 + }, + { + "path": "apps/control-station/tsconfig.app.json", + "sha256": "29297ad53cb440fe8bb0d399aacbf8d192d1df2bd87cc264537b6444fb15f3cd", + "lines": 52 + }, + { + "path": "apps/control-station/vite.config.ts", + "sha256": "92dab3df33e06c5e9302d1bacc9f9526b64341fa55a0cd466479b4c9505d4cd3", + "lines": 134 + }, + { + "path": "apps/control-station/test/sensorEnrollment.test.mjs", + "sha256": "b5d8d3ad4a4baaa5e16c40cfd5ec710c7800625bfdd53a3fa6e310d3b3352f8f", + "lines": 102 + }, + { + "path": "apps/node-agent/ui/src/NodeSensors.tsx", + "sha256": "e26493746c8a50e604a1e3f8bc75c3ba7cfdafd1a70b9750a1375f8d5bc14300", + "lines": 7 + }, + { + "path": "apps/node-agent/ui/tsconfig.json", + "sha256": "0bb5f9df862a6ce20e2877a947a0741e7e1c823939ac01dc4f76a449a72a290a", + "lines": 34 + }, + { + "path": "apps/node-agent/ui/vite.config.js", + "sha256": "682f1d5d691a74a7e3e5c15d38bb522455017b8f873fb424b374859e84d42a84", + "lines": 17 + }, + { + "path": "packages/sensor-ui/src/contracts.ts", + "sha256": "da575e7fd018e5bf6802c0ae5a706f05dda436587b7088eef2c009e56a7ade51", + "lines": 44 + }, + { + "path": "packages/sensor-ui/src/enrollment.ts", + "sha256": "2c510e2ae8597953b08ab017f89663f149a95b08b7c416708476f2fe5dc3cfd8", + "lines": 20 + }, + { + "path": "packages/sensor-ui/src/extensions.ts", + "sha256": "db930b121cf828f7e5e6a533f51b2eae40d99e383a6be6b37e16e28ea2271de3", + "lines": 29 + }, + { + "path": "packages/sensor-ui/src/pluginSdk.ts", + "sha256": "349f87b22d49e63a9b7f5ffc8fd1b05be424d9a3b918f232a491b333ae08ce8a", + "lines": 4 + }, + { + "path": "packages/sensor-ui/src/SensorWorkspace.tsx", + "sha256": "30f91a8d313e07c36f7b8577317cadece18467ece9af093abf76b3d59b14fd78", + "lines": 43 + }, + { + "path": "plugins/xgrids-k1/frontend/src/plugin.ts", + "sha256": "bc3a062cd9408622de39eb00bd60b0e9e701095eba1483d2a22874f1630fde07", + "lines": 17 + }, + { + "path": "plugins/xgrids-k1/frontend/src/api.ts", + "sha256": "df61477546658836b3472d423c5366c6c6e0de0cb08a178d7c9e31b93edf1e2e", + "lines": 2078 + }, + { + "path": "plugins/xgrids-k1/frontend/src/connectionAttempt.ts", + "sha256": "ff28453215c6c6f4a23d969b06914fd191e376da87fd72f3a4e44cb8d7a61286", + "lines": 17 + }, + { + "path": "plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx", + "sha256": "27bed30edf1397617bfcf51cea0a2783b6d7cdd43ece6469d092b89559f1e154", + "lines": 231 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/K1Detail.tsx", + "sha256": "b02130ff22bbd81d7a409001dfde21df682b20229a4f08d600d3e8f412a4acde", + "lines": 25 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/enrollment.ts", + "sha256": "ca29b7f3f5b33e6bf582748e86bc85eceef6c634ea877f494e27694177a46191", + "lines": 124 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/K1LiveSettings.tsx", + "sha256": "dc7ce392e92240766b69c3db25cd500d6752917a675acdf57c53a65c6a34f6a4", + "lines": 28 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/plugin.ts", + "sha256": "89bab0bd0955dbc85820b32194ea0d3b3d03fbb0567297fb8527db9543ccf705", + "lines": 7 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/runtime.ts", + "sha256": "f813948f12b9eb86ee7f5c70dbbbe33cc785ce7e4c941d8059a3d1ed82829de5", + "lines": 7 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/K1LiveView.tsx", + "sha256": "0493ff5b09f191fdb03bc14f635ec67289100f9e6927f6dd3d124f0f0656babe", + "lines": 63 + }, + { + "path": "plugins/xgrids-k1/frontend/src/sensors/DeviceEnrollmentWindow.tsx", + "sha256": "59dedf270b7a5b4fad3bb75b47b4ad4cac964a54aa4d73075c38b7f6860a3ff8", + "lines": 62 + } + ] +} diff --git a/docs/audits/2026-09-07-k1-plugin-boundary-r1.md b/docs/audits/2026-09-07-k1-plugin-boundary-r1.md new file mode 100644 index 0000000..3651546 --- /dev/null +++ b/docs/audits/2026-09-07-k1-plugin-boundary-r1.md @@ -0,0 +1,147 @@ +# K1 plugin boundary R1 — implementation + +Scope: the first independently verifiable increment of the approved plugin +refactor. This report does not declare the whole architecture migration or +the physical Bridge incident resolved. + +## Result + +The shared sensor workspace no longer imports K1, selects K1-specific controls, +or owns K1 control-operation deadlines. K1 detail, live settings, live renderer +and Bridge enrollment are under `plugins/xgrids-k1/frontend/src/sensors`. +Control Station receives their optional contributions from the installed +`DeviceUiPlugin` registry; Node composes the same integration explicitly. +Both hosts resolve `@mission-core/sensor-sdk` to the same portable host surface. +Missing and ambiguous renderers fail closed; absence does not substitute the +camera detail controls for an unsupported device. Existing camera behavior is +retained through the host's default camera detail. + +Four functions were mechanically extracted from `facade.py` into +`connection_attempt.py`. Their ASTs match the pre-change working-tree copy +exactly, including runtime/target/parent/lease/host-epoch proof checks. The +facade is now 35,549 lines; this is a first responsibility boundary, not a claim +that the remaining service is small or process-isolated. + +Node now exports the existing connection attempt, snapshot revision, runtime +start time and current permitted enrollment actions. The compact projection +does not forward diagnostics, timeline payloads or exception text. After an +invocation error the driver may read the exact journaled operation once; it +never resends the physical command. Explicit host admission rejection is +distinguished from an unknown post-dispatch outcome. Secrets are removed from +input references even on pre-dispatch rejection; this does not claim secure +erasure of immutable language/runtime copies. + +The enrollment observer distinguishes delivery completion from network and +control completion. It sends one POST, resolves lost responses by the same +operation ID, follows the exact owned bootstrap, ignores older snapshots, +stops observing across a runtime replacement, and stops client observation on +window closure without cancelling/replaying physical commands. Read-only +observation has a bounded budget beyond the existing delivery deadline; it +does not extend command admission. Polling in the open window keeps host +availability and backend authority visible. A changed runtime/discovery/mode +invalidates the selected device and credential draft. + +## Validation + +- All 629 cases in the existing acquisition lifecycle suite passed after the + extraction, including the existing recovery and physical-authority checks. +- The final Python read-model, Node bridge and Fleet enrollment set passed: + 22 cases, covering projection privacy, exact operation correlation, + no-resubmit behavior, secret lifetime and pre-dispatch rejection. +- The full frontend unit suite passed: 784 tests. After the final observer and + presentation changes, 41 focused enrollment/boundary/architecture tests passed, including + two additional cases for current authority and delayed results during an + observed board outage. +- The architecture test passed. Control Station and Node TypeScript checks + and production builds passed. Jobs were run without a second backend or + Docker startup. The Node dependency install used the local npm cache with + scripts disabled. Full frontend tests emitted existing sandbox HMR/listener + warnings; the unit tests do not establish browser or hardware acceptance. +- Ruff and `git diff --check` passed. The four extracted functions retain equal + ASTs after formatting. Existing dirty working-tree changes were preserved. + +The canonical LaunchAgent was restarted only after `/api/state` showed idle +capture/control and no accepted/running operation. The configured persistent +data directory remained outside the checkout. The replacement serves +`127.0.0.1:8000`, reports liveness `alive`, runtime +`snapshot-runtime-fefcc6209249e845738886a72813e13c`, and idle source state. +No Mission Core backend was found listening on 8765. + +## Limits and next acceptance gate + +No physical K1 command or new capture was issued in this increment. Browser +hardware acceptance remains pending the owner's required cache clearing; the +available browser tools do not expose that operation. A new tab or reload was +not counted as a cleared-cache test. The Node UI is built locally, but this +increment was not installed on the Ubuntu mini-PC. + +The BLE packet format, provisioning dispatch/polling behavior, acquisition +command order, camera producer and Rerun profile settings were not modified. +In particular, this work does not prove resolution of the earlier ATT failure +or the separately identified early network-status polling risk. + +Backend optional installation, dependency separation, isolated macOS runtime, +portable media IPC, archive-codec separation and full Node reboot acceptance +remain subsequent stages. Multiple simultaneous device sessions and a +multi-provider enrollment picker are not supplied by this increment. + +Before the next deeper lifecycle extraction, accept one cleared-cache UI +Bridge connection and its exact operation stages on the prepared Mac; keep +network/control unknown states honest and compare recovery against the prior +Ops scenarios. Ubuntu Bridge requires its own physical acceptance before any +claim of platform parity. LAB/recorded/live Rerun profiles remain separate +acceptance dimensions. + +References: [architecture audit](2026-09-06-k1-bridge-architecture-review.md), +[Ops inventory](2026-09-06-k1-bridge-ops-index.json), +[changed source hashes](2026-09-07-k1-plugin-boundary-r1-sources.json). + +The implementation and open hardware checks were added to Ops card #3, +“Mission Core. Lixel K1 / XGRIDS Integration”, as 12 titled R1 blocks. The +17 existing blocks and historical card status were preserved. + +## Owner's fresh Chrome test after R1 + +The owner reported clearing Chrome's cache, scanning, selecting K1 and +submitting the network form. The canonical service journal records discovery +completing in 12.395 seconds and the network attempt running from +2026-09-06 21:16:38.081 UTC to 21:16:49.307 UTC (11.226 seconds). +It failed at `gatt-write`, after one dispatched 99-byte write-with-response, +with `BleakGATTProtocolError`, ATT 4 (`INVALID_PDU`). Advertised characteristic +properties were read/write; the reported command capacity was 253 bytes. +The write was not confirmed. Neither post-write status polling nor MQTT +bootstrap was reached. This failure does not implicate the separate early +status-poll termination risk, camera path or Rerun profiles. + +The selected FW 3.0.2 profile classifies this reply as +`k1-wifi-network-not-found`, based on the previously reviewed firmware callback. +This attempt itself did not obtain live DeviceInfo and cannot establish the +actual firmware or exact cause of network invisibility. The submitted network +name is deliberately not retained in the server journal. The exact SSID was +requested from the owner; no password was requested or retained. No spelling +error or radio incompatibility is assumed. + +Source review confirmed that the form captures the password before clearing +React state, and the station path passes it to the existing frame encoder. +The existing LAB form trims SSID edges; whether that affects this attempt is +unknown. No provisioning behavior was changed on this evidence. The focused +Wi-Fi provisioning and firmware failure suites passed (43 tests); hardware +acceptance remains failed/pending diagnosis. + +Sanitized operation events, UTC and monotonic observation timestamps, owner +test notes and an artifact hash are retained outside Git in the ignored +`.runtime/k1-connect-incident-20260906/fresh-2116/` directory. The agent issued +only local state/liveness reads, with no new device command or service restart. +Core 8000 remained alive. A fresh Ops card read timed out; this addendum has +not yet been copied to Ops. + +## Subsequent successful connection and refusal guidance + +The owner subsequently confirmed a mistyped network name and a successful +connection after correcting it. Core independently reports network applied +and DeviceInfo/control ready in 8.298 seconds on 2026-09-07. The previous +failure is explained for this incident; its historical outcome is not rewritten. +The UI now names Wi-Fi failure prominently and asks to check the network name +and password. See [the bounded correction and evidence](2026-09-07-k1-wifi-refusal-ui.md). +This accepts that connection attempt, not stream, reboot, Ubuntu or fresh-cache +visual acceptance of the subsequent wording change. diff --git a/docs/audits/2026-09-07-k1-wifi-refusal-ui.md b/docs/audits/2026-09-07-k1-wifi-refusal-ui.md new file mode 100644 index 0000000..ca36645 --- /dev/null +++ b/docs/audits/2026-09-07-k1-wifi-refusal-ui.md @@ -0,0 +1,57 @@ +# K1 Wi-Fi refusal: operator diagnosis and UI correction + +The owner confirmed that the previously entered Wi-Fi network name was wrong +and that connection succeeded after correcting it. The canonical Core journal +independently records a successful Bridge attempt on 2026-09-07, from +07:01:10.203 UTC to 07:01:18.501 UTC: `network_applied`, +`device-info-confirmed`, control `ready` (8.298 seconds). + +The earlier ATT 4 response is therefore consistent with the exact reviewed +FW 3.0.2 station callback's network-not-found branch. The response establishes +the device's reported failure, not independently which character the operator +typed incorrectly. ATT 6 remains the separate credentials-required branch; +neither is inferred from a generic timeout or lost response. + +## Implementation + +The existing plugin-owned `networkFailurePresentation.ts` keeps distinct public +codes and specific reasons, while both messages now explicitly ask the operator +to check the network name and password. `K1OperatorError.tsx` gives those exact +station refusals the title “Не удалось подключить K1 к Wi‑Fi”. Existing LAB and +onboard enrollment consumers share the text. Unclassified Bluetooth errors, +timeouts and unknown outcomes keep their separate presentation. + +This increment changes presentation only. No BLE frame, station callback +classification, ledger, retry authorization, acquisition command, connection +recovery or Rerun setting was changed. The historical failed attempt remains +failed/unconfirmed; the later successful attempt supplies its own authority. + +## Validation and delivery + +- Existing rendered recovery, provisioning and onboard enrollment assertions + were updated for the explicit Wi-Fi cause and credential guidance. +- Architecture checks passed (4 cases); the full frontend suite passed + (786 cases), including the existing no-resubmit and unknown-outcome coverage. +- Control Station and Node UI TypeScript checks and production builds passed. + Builds/tests were sequential; no Docker or extra backend was started. +- The Core UI was built in a temporary directory, then published with HTML last + and prior hashed assets retained for already open tabs. HTTP verification of + `/assets/app-B5NeGPU1.js` confirmed both the new title and guidance; the HTML + response has `Cache-Control: no-store`. +- Core 8000 remained alive with the same runtime identity and the K1 control + session ready. The backend was not restarted. No backend listens on 8765. +- The Node UI was built locally; this increment was not installed on Ubuntu. + No new device failure or browser hardware attempt was induced. Browser cache + clearing was not performed by the agent and no fresh-cache visual acceptance + is claimed. The user's successful attempt and rendered tests are distinct + evidence sources. Stream, power-loss and Ubuntu acceptance are not established + by this connection-only success. + +Sanitized success evidence, owner notes, UTC/monotonic observation timestamps +and SHA-256 artifact metadata are retained outside Git in the ignored +`.runtime/k1-connect-incident-20260906/success-20260907/` directory. No Wi-Fi +password or submitted network name was retained. + +The direct Ops tools are absent from this turn's tool inventory, and no tool +discovery endpoint is exposed. This report is prepared for card #3; it has not +been written to Ops. Legacy Ops widgets and raw API workarounds were not used. diff --git a/packages/plugin-sdk/README.md b/packages/plugin-sdk/README.md index 34e4225..36d5f49 100644 --- a/packages/plugin-sdk/README.md +++ b/packages/plugin-sdk/README.md @@ -52,6 +52,11 @@ compatibility profile and adapter. Current host implementation references: +- `packages/sensor-ui/src/pluginSdk.ts` — portable frontend sensor contribution + and transport types exposed as `@mission-core/sensor-sdk` by both hosts; + integration-owned detail/enrollment UI enters through reviewed composition, + not device branches in the shared sensor workspace; + - `apps/control-station/src/core/device-plugins/frontendSdk.ts` — current public TypeScript/React host surface for statically reviewed UI contributions; - `plugins/xgrids-k1/frontend/` — first physically plugin-owned consumer of diff --git a/packages/sensor-ui/src/SensorWorkspace.tsx b/packages/sensor-ui/src/SensorWorkspace.tsx index 5bc0d02..ffa70de 100644 --- a/packages/sensor-ui/src/SensorWorkspace.tsx +++ b/packages/sensor-ui/src/SensorWorkspace.tsx @@ -1,10 +1,13 @@ -import {useCallback,useEffect,useState} from 'react'; +import {useCallback,useEffect,useState,type ComponentType} from 'react'; import {ActivityIndicator,Button,Icon,IconButton,ResourceList,ResourceRow,SettingsCard,StatusBadge,TextField,ToastStack,Window,WindowFooterActions} from '@nodedc/ui-react'; import {perform,type Sensor,type SensorInventory,type SensorTransport} from './contracts'; import {SensorDetail} from './SensorDetail'; import {sensorStatus} from './sensorStatus'; +import {sensorContribution,type SensorUiContribution,type SensorEnrollmentProps} from './extensions'; +import type {RerunHostFactory} from './rerunHost'; import './sensors.css'; -export function SensorWorkspace({transport,enabled=true,onDetailChange}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void}){ +export function SensorWorkspace({transport,enabled=true,onDetailChange,createRerunHost,contributions=[],EnrollmentView}:{transport:SensorTransport;enabled?:boolean;onDetailChange?:(open:boolean)=>void;createRerunHost?:RerunHostFactory;contributions?:readonly SensorUiContribution[];EnrollmentView?:ComponentType}){ + const [adding,setAdding]=useState(false); const [inventory,setInventory]=useState(null);const [selected,setSelected]=useState(null);const [editing,setEditing]=useState(null);const [name,setName]=useState('');const [localBusy,setBusy]=useState(null);const [error,setError]=useState('');const [fresh,setFresh]=useState(false); const failure=useCallback((e:unknown)=>{setError(e===null?'':e instanceof Error?e.message:'Не удалось выполнить действие устройства.');},[]); const refresh=useCallback(async()=>{if(!enabled){setFresh(false);return;}try{const value=await transport.inventory();setInventory(value);setFresh(value.fresh!==false);}catch(e){setFresh(false);failure(e);}},[transport,enabled,failure]); @@ -19,20 +22,22 @@ export function SensorWorkspace({transport,enabled=true,onDetailChange}:{transpo },[transport,enabled,refresh]); async function action(device:Sensor,action:string,parameters:Record={}){if(localBusy)return;setError('');setBusy(device.id);try{await perform(transport,device,action,parameters);await refresh();setEditing(null);}catch(e){failure(e);}finally{setBusy(null);}} const device=inventory?.items.find(v=>v.id===selected); - const connected=inventory?.items.filter(v=>v.online)??[]; + const connected=inventory?.items.filter(v=>v.online||sensorContribution(contributions,v)?.retainOffline)??[]; const editingCurrent=inventory?.items.find(v=>v.id===editing?.id); useEffect(()=>{onDetailChange?.(!!device);},[!!device,onDetailChange]); - return
{device?setSelected(null)} refresh={refresh} failure={failure}/>:<> -
{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}void refresh()}>
- {!inventory?:connected.length===0?:{connected.map(item=>{ + const Detail=device?(sensorContribution(contributions,device)?.Detail??(device.kind?null:SensorDetail)):null; + return
{device?Detail?setSelected(null)} refresh={refresh} failure={failure} createRerunHost={createRerunHost}/>:
:<> +
{!enabled?'БК недоступен':fresh?'Сведения с БК':'Нет свежих сведений'}
{transport.enrollment&&EnrollmentView&&setAdding(true)}>}{void refresh();}}>
+ {!inventory?:connected.length===0?:{connected.map(item=>{ const operation=inventory.operations?.find(v=>v.device_id===item.id&&v.state==='running');const busy=!!operation||localBusy===item.id; const configured=item.configured??item.snapshot.enrollment==='enrolled'; const prep=operation?.action_id==='prepare'&&inventory.preparation&&(inventory.preparation.started_at*1000>=Date.parse(operation.requested_at)-1000)?inventory.preparation:undefined; const status=sensorStatus(item,enabled&&fresh);const label=busy?'Подготовка или команда выполняется':status.label; - return
  • } title={item.name} description={item.model} metadata={USB {item.usb}} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}
    } + return
  • } title={item.name} description={item.model} metadata={{item.connection_label||`USB ${item.usb}`}} statusPlacement={configured?'leading':'trailing'} aria-busy={busy} progress={busy?{label, value:prep?.state==='running'?prep.steps.filter(s=>s.state==='complete').length/(prep.steps.length+1):prep?.state==='complete'?5/6:undefined,valueText:prep?.steps.find(s=>s.state==='running')?.label??'Проверка кадров камеры'}:undefined} status={{configured&&item.online?null:label}} actions={<>{!configured&&sensorContribution(contributions,item)?.supportsPreparation!==false&&void action(item,'prepare')}>}{setEditing(item);setName(item.name);}}>setSelected(item.id)}>}/>
  • ;})}} {(inventory?.operations?.some(v=>v.state==='running'&&v.action_id==='prepare'&&!!inventory.preparation&&inventory.preparation.started_at*1000>=Date.parse(v.requested_at)-1000))&&inventory?.preparation&&{inventory.preparation.steps.map(step=>:step.state==='running'?:{step.state==='error'?'Ошибка':step.state==='blocked'?'Не выполнено':'Ожидает'}}/>) }:Ожидает}/>} } - setEditing(null)} footer={}>
    setName(e.target.value)} disabled={!!localBusy}/>{editing&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить}/>}
    + setEditing(null)} footer={}>
    setName(e.target.value)} disabled={!!localBusy}/>{editing&&sensorContribution(contributions,editing)?.supportsPreparation!==false&&(editing.configured??editing.snapshot.enrollment==='enrolled')&&{const target=editing;setEditing(null);void action(target,'prepare');}}>Обновить}/>}
    + {adding&&transport.enrollment&&EnrollmentView&&setAdding(false)} onChange={()=>{void refresh();}}/>} setError('')}/>
    ; } diff --git a/packages/sensor-ui/src/contracts.ts b/packages/sensor-ui/src/contracts.ts index 2033a45..dbb0d72 100644 --- a/packages/sensor-ui/src/contracts.ts +++ b/packages/sensor-ui/src/contracts.ts @@ -1,6 +1,8 @@ export interface SensorProfile { id: string; sensor: number; stream: string; index: number; format: string; fps: number; width?: number; height?: number } export interface SensorOption { id: string; sensor: string; label: string; value: number; min: number; max: number; step: number; read_only: boolean } export interface Sensor { + kind?:string; connection_label?:string; control?:{generation:number;revision:number;phase:string;can_start:boolean;can_stop?:boolean;acquisition_id:string|null}; + live_settings?: Record; id: string; name: string; model: string; prepared: boolean; configured?: boolean; verified: boolean; online: boolean; usb: string; firmware?: string; playback_id?: string | null; snapshot: {context: {session_id: string; device: {device_id: string}; execution: {node_id: string}}; acquisition: string; enrollment: string; message?: string}; profiles?: SensorProfile[]; defaults?: string[]; options?: SensorOption[]; layers: string[]; @@ -18,19 +20,20 @@ export interface SensorCommand { } export interface SensorOperation {state:string;error?:string;result?:unknown} export interface SensorTransport { + enrollment?: import('./enrollment').EnrollmentTransport; inventory: () => Promise; subscribe?: (receive:(value:SensorInventory)=>void, unavailable:()=>void) => (()=>void); submit: (value:SensorCommand) => Promise; operation: (id:string) => Promise; } -export function command(device:Sensor,action:string,parameters:Record={}):SensorCommand { +export function command(device:Sensor,action:string,parameters:Record={},timeoutMs=60000):SensorCommand { const id='op_'+crypto.randomUUID().replaceAll('-','');const now=Date.now(); return {api_version:'missioncore.nodedc/plugin-sdk/v0alpha2',kind:'OperationRequest',operation_id:id,idempotency_key:id, session:{session_id:device.snapshot.context.session_id,device_id:device.id},action_id:action, - requested_at:new Date(now).toISOString(),deadline_at:new Date(now+(action==='prepare'?350000:60000)).toISOString(),parameters}; + requested_at:new Date(now).toISOString(),deadline_at:new Date(now+(action==='prepare'?350000:timeoutMs)).toISOString(),parameters}; } -export async function perform(transport:SensorTransport,device:Sensor,action:string,parameters:Record={}):Promise{ - const request=command(device,action,parameters);let value=await transport.submit(request); +export async function perform(transport:SensorTransport,device:Sensor,action:string,parameters:Record={},timeoutMs=60000):Promise{ + const request=command(device,action,parameters,timeoutMs);let value=await transport.submit(request); const deadline=Date.parse(request.deadline_at)+10000; while (value.state==='running'||value.state==='queued') { if(Date.now()>deadline) throw new Error('Подтверждение пока не получено. Обновите состояние устройства.'); diff --git a/packages/sensor-ui/src/enrollment.ts b/packages/sensor-ui/src/enrollment.ts new file mode 100644 index 0000000..c4c1df1 --- /dev/null +++ b/packages/sensor-ui/src/enrollment.ts @@ -0,0 +1,20 @@ +export interface EnrollmentState { + available:boolean; fresh?:boolean; node_id:string; name?:string; runtime_id?:string; + mode?:string; model?:string; discovery_generation?:number; mode_revision?:number; + candidates?:{id:string;name:string;rssi?:number}[]; + networks?:{ssid:string;signal:number;security:string}[]; + connected?:boolean; ready_to_start?:boolean; selected_device_id?:string; ip?:string; + snapshot_revision?:number; runtime_started_at?:string; allowed_actions?:string[]; + connection_attempt?:unknown; command_result?:{operation_id:string;status:string;error_code?:string}; + device_session?:{device_session_id:string;device_id:string}; observed_at?:string; +} +export interface EnrollmentCommand { + operation_id:string; node_id:string; runtime_id:string; action:'scan'|'networks'|'connect'|'verify'; + deadline_at:string; parameters:Record; +} +export interface EnrollmentOperation {operation_id?:string;state:string;error?:string;result?:EnrollmentState} +export interface EnrollmentTransport { + state:()=>Promise; + submit:(command:EnrollmentCommand)=>Promise; + operation:(id:string)=>Promise; +} diff --git a/packages/sensor-ui/src/extensions.ts b/packages/sensor-ui/src/extensions.ts new file mode 100644 index 0000000..03cbac4 --- /dev/null +++ b/packages/sensor-ui/src/extensions.ts @@ -0,0 +1,29 @@ +import type {ComponentType} from 'react'; +import type {IconName} from '@nodedc/ui-react'; +import type {Sensor, SensorTransport} from './contracts'; +import type {EnrollmentTransport} from './enrollment'; +import type {RerunHostFactory} from './rerunHost'; + +export interface SensorDetailProps { + device:Sensor; transport:SensorTransport; enabled:boolean; + back:()=>void; refresh:()=>Promise; failure:(error:unknown)=>void; + createRerunHost?:RerunHostFactory; +} +export interface SensorEnrollmentProps { + transport:EnrollmentTransport; onClose:()=>void; onChange:()=>void; +} +export interface SensorUiContribution { + kind:string; + Detail:ComponentType; + icon:IconName; + retainOffline:boolean; + supportsPreparation:boolean; +} + +export function sensorContribution( + contributions:readonly SensorUiContribution[], device:Sensor, +):SensorUiContribution|undefined { + const matches=contributions.filter(value=>value.kind===device.kind); + // An ambiguous renderer must not acquire authority over a device. + return matches.length===1?matches[0]:undefined; +} diff --git a/packages/sensor-ui/src/pluginSdk.ts b/packages/sensor-ui/src/pluginSdk.ts new file mode 100644 index 0000000..1d6f64e --- /dev/null +++ b/packages/sensor-ui/src/pluginSdk.ts @@ -0,0 +1,4 @@ +export * from './contracts'; +export type * from './enrollment'; +export type * from './rerunHost'; +export type * from './extensions'; diff --git a/packages/sensor-ui/src/rerunHost.ts b/packages/sensor-ui/src/rerunHost.ts new file mode 100644 index 0000000..202d9ac --- /dev/null +++ b/packages/sensor-ui/src/rerunHost.ts @@ -0,0 +1,12 @@ +/** Native renderer capability supplied by each host, independent of device APIs. */ +export interface LiveRerunViewer { + start:(source:string|string[]|null,parent:HTMLElement,options:{width:string;height:string;hide_welcome_screen:boolean;enable_history:boolean})=>Promise; + open_channel:(name:string)=>{ready:boolean;send_rrd:(bytes:Uint8Array)=>void;close:()=>void}; + override_panel_state:(panel:'top'|'blueprint'|'selection'|'time',state:'hidden'|'collapsed'|'expanded')=>void; + get_active_recording_id:()=>string|null; + get_time_range:(id:string,timeline:string)=>{min:number;max:number}|null; + set_active_timeline:(id:string,timeline:string)=>void; + set_current_time:(id:string,timeline:string,time:number)=>void; + set_playing:(id:string,playing:boolean)=>void; +} +export type RerunHostFactory=(element:HTMLElement)=>{ready:Promise<{viewer:LiveRerunViewer;mount:HTMLElement}>;dispose:()=>void}; diff --git a/packages/sensor-ui/src/sensors.css b/packages/sensor-ui/src/sensors.css index 462b06c..9dd6976 100644 --- a/packages/sensor-ui/src/sensors.css +++ b/packages/sensor-ui/src/sensors.css @@ -9,3 +9,7 @@ .sensor-viewer-expanded {position:fixed;inset:var(--nodedc-space-4);z-index:var(--nodedc-layer-overlay);background:var(--nodedc-canvas);overflow:auto} .sensor-viewer-expanded .sensor-media {height:calc(100vh - 190px)} .sensor-record {display:flex;flex-wrap:wrap;gap:var(--nodedc-space-4);padding-block:var(--nodedc-space-3);font-size:var(--nodedc-font-size-sm)} +.sensor-live-layout {display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--nodedc-space-4)} +.sensor-live-spatial {min-height:420px;overflow:hidden} +.sensor-live-spatial iframe {display:block;width:100%;height:100%;min-height:420px;border:0} +.sensor-viewer-expanded .sensor-live-spatial,.sensor-viewer-expanded .sensor-live-spatial iframe {min-height:calc(100vh - 230px)} diff --git a/plugins/xgrids-k1/README.md b/plugins/xgrids-k1/README.md index d7df916..9185ce7 100644 --- a/plugins/xgrids-k1/README.md +++ b/plugins/xgrids-k1/README.md @@ -14,6 +14,23 @@ This manifest currently exposes only `xgrids.lixelkity-k1`, and the transitional runtime still owns one active model/session at a time. Concurrent model/session routing remains a later supervisor milestone. +The onboard sensor views and Bridge enrollment controller are also plugin-owned +under `frontend/src/sensors`. The shared sensor workspace consumes optional +`SensorUiContribution` renderers through `@mission-core/sensor-sdk`; the main +frontend registers these on `DeviceUiPlugin.sensorUi`, and the Node frontend +selects its installed contribution explicitly. An absent or ambiguous renderer +does not fall through into another device's detail controls. The current single +enrollment provider is explicit; a multi-provider enrollment picker is not yet +implemented. + +`connection_attempt.py` owns the pure join of network journal, owned control +bootstrap, recovery verification and current control authority. LAB keeps the +full public attempt; Node exports a compact allowlisted projection without +timeline payloads or diagnostic bundles. The Node observer submits once and +follows the same operation across response loss and bootstrap completion. +These are read-model and presentation boundaries, not a new command scheduler +or a claim of independently installable backend/process isolation. + The plugin owns: - the exact firmware/topology compatibility profiles under diff --git a/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx b/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx index 6ca3b87..a475855 100644 --- a/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx +++ b/plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx @@ -275,6 +275,11 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) : sourceRuntimeBusy || preparedAcquisition ? "warning" : "neutral"; + const connectionAttemptFailed = !sourceRuntimeBusy && !preparedAcquisition + && connectionTopology?.status !== "active" + && state?.connection_attempt?.connection_mode === effectiveDesiredConnectionMode + && ["failed", "cancelled", "timed_out", "interrupted", "operator_action_required"] + .includes(state.connection_attempt.status); const connectionControlBootstrapSettling = isConnectionControlBootstrapSettling(state); const connectionPhaseLabel = livePreparationPending @@ -293,6 +298,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) ? savedBridgeNetworkSetupRequired ? "Нужна настройка сети" : "Требуется действие" + : connectionAttemptFailed + ? "Подключение не завершено" : projectedPhase === "error" ? connectionPhaseFallbackLabel(projectedPhase) : connectionTopology?.status === "active" @@ -316,6 +323,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) ? "accent" : physicalRecoveryRequired ? "warning" + : connectionAttemptFailed + ? "warning" : projectedPhase === "error" ? phaseTone(projectedPhase) : connectionTopology?.status === "active" @@ -336,6 +345,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps) : physicalRecoveryRequired ? physicalRecoveryDetail ?? "Безопасное восстановление прежнего K1 сейчас недоступно." + : connectionAttemptFailed + ? "Проверьте состояние K1 или начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "configured-unverified" ? "Начните новое подключение." : !sourceRuntimeBusy && connectionTopology?.status === "active" diff --git a/plugins/xgrids-k1/frontend/src/api.ts b/plugins/xgrids-k1/frontend/src/api.ts index 4bfbe8e..807cabd 100644 --- a/plugins/xgrids-k1/frontend/src/api.ts +++ b/plugins/xgrids-k1/frontend/src/api.ts @@ -1,3 +1,4 @@ +import type {XgridsConnectionAttemptSummary} from './connectionAttempt'; import type { ViewerSettings } from "@mission-core/plugin-sdk"; import { xgridsK1Actions, xgridsK1Manifest } from "./manifest"; @@ -826,28 +827,7 @@ export const XGRIDS_CONNECTION_ATTEMPT_PHASES = [ export type XgridsConnectionAttemptPhase = typeof XGRIDS_CONNECTION_ATTEMPT_PHASES[number]; -export interface XgridsConnectionAttempt { - schema_version: "missioncore.xgrids-k1-connection-attempt/v1"; - attempt_id: string; - connection_mode: XgridsConnectionMode; - status: OperationStatus; - stage: string; - public_error_code: string | null; - side_effect_status: string; - phase: XgridsConnectionAttemptPhase; - control_state: "ready" | "control_not_ready" | "unknown"; - safe_next_action: - | "wait-for-current-attempt" - | "continue-with-control-verification" - | "verify-control-read-only" - | "start-acquisition" - | "stop-local-receiver" - | "retire-unavailable-physical-target" - | "scan-select-connect" - | "manual-recovery-required"; - automatic_retry: false; - accepted_at: string | null; - completed_at: string | null; +export interface XgridsConnectionAttempt extends XgridsConnectionAttemptSummary { timeline: XgridsOperationEvent[]; diagnostic_bundle?: XgridsConnectionDiagnosticBundle; } diff --git a/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx b/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx index 66b5156..acd3ae6 100644 --- a/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx +++ b/plugins/xgrids-k1/frontend/src/components/K1OperatorError.tsx @@ -3,13 +3,14 @@ import { Button } from "@nodedc/ui-react"; import type { XgridsConnectionAttempt } from "../api"; import { hostFailureDiagnosticPresentation } from "../hostDiagnosticPresentation"; +import { STATION_WIFI_FAILURE_MESSAGES } from "../networkFailurePresentation"; const connectionAttemptStageLabels: Record = { accepted: "Запрос принят", "scan-selection-admitted": "Результат выбран", "host-wifi-profile-preflight": "Подготовка профиля Wi‑Fi", "device-ap-activation": "Подготовка локальной сети", - "ble-provisioning-write": "Передаются настройки сети", + "ble-provisioning-write": "Подключение K1 к Wi‑Fi", "ble-write-dispatched": "Настройки переданы", "status-observing": "Ожидание ответа", "device-topology-applied": "Целевая сеть подтверждена", @@ -62,6 +63,8 @@ export function attemptNextActionLabel( return "Проверить управление без изменения сети"; case "start-acquisition": return "Готово к запуску приёма"; + case "stop-acquisition": + return "Остановить текущий приём"; case "stop-local-receiver": return "Завершить только локальный приём"; case "retire-unavailable-physical-target": @@ -74,6 +77,9 @@ export function attemptNextActionLabel( } const publicConnectionErrorLabels: Readonly> = { + ...STATION_WIFI_FAILURE_MESSAGES, + BleakGATTProtocolError: + "K1 ответил ошибкой Bluetooth. Подключение к Wi‑Fi не подтверждено; проверьте состояние устройства или начните новое подключение.", "network-provision-discovery-generation-conflict": "Результат Bluetooth-поиска устарел до отправки. Настройки устройства не изменялись; выполните новый поиск.", "connection-mode-draft-revision-conflict": @@ -121,6 +127,7 @@ export function K1OperatorError({ onClear: () => void; }) { const structured = hostFailureDiagnosticPresentation(diagnostic); + const stationFailure = STATION_WIFI_FAILURE_MESSAGES[attempt?.public_error_code ?? ""]; const [diagnosticCopied, setDiagnosticCopied] = useState(false); const copyDiagnosticBundle = async () => { if (!attempt?.diagnostic_bundle || !navigator.clipboard) return; @@ -137,7 +144,7 @@ export function K1OperatorError({ >