fix(perception): reuse M4.8 for vegetation evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 00:13:58 +03:00
parent 57204b3b0b
commit 67e6ae98e2
12 changed files with 482 additions and 226 deletions
@@ -4,11 +4,25 @@ const RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const CANDIDATES = ["ddrnet", "ppliteseg"] as const;
const ROUTE_MODES = ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"] as const;
const VALIDATION_MODES = ["source", "truth", "ddrnet", "ppliteseg"] as const;
const VALIDATION_ASSETS = [
"source",
"truth",
"ddrnet",
"ppliteseg",
"ddrnet_error",
"ppliteseg_error",
] as const;
export type VegetationCandidateKey = typeof CANDIDATES[number];
export type VegetationRouteMode = typeof ROUTE_MODES[number];
export type VegetationValidationMode = typeof VALIDATION_MODES[number];
export interface VegetationVisualFocus {
className: string;
labelId: number;
truthPixels: number;
truthFraction: number;
stratumRank: number;
}
export interface VegetationCandidateMetrics {
candidate: VegetationCandidateKey;
@@ -33,6 +47,7 @@ export interface VegetationVisualCase {
height: number;
centerCropXyxy: readonly [number, number, number, number];
outsideCropState: "undefined" | "not-applicable";
focus: VegetationVisualFocus | null;
assets: Readonly<Record<string, string>>;
}
@@ -174,7 +189,7 @@ function visualCaseValue(
.join("/")}`;
}
const expectedAssets = expectedKind === "goose"
? VALIDATION_MODES
? VALIDATION_ASSETS
: ROUTE_MODES;
if (expectedAssets.some((key) => !projected[key])) {
throw new VegetationShadowContractError(`vegetation.case.assets: ${expectedKind} набор неполон.`);
@@ -183,6 +198,23 @@ function visualCaseValue(
if (outsideCropState !== "undefined" && outsideCropState !== "not-applicable") {
throw new VegetationShadowContractError("vegetation.case.outside_crop_state: контракт изменён.");
}
let focus: VegetationVisualFocus | null = null;
if (expectedKind === "goose") {
const rawFocus = objectValue(row.focus, "vegetation.case.focus");
const truthFraction = numberValue(rawFocus.truth_fraction, "vegetation.case.focus.truth_fraction");
if (truthFraction <= 0 || truthFraction > 1) {
throw new VegetationShadowContractError("vegetation.case.focus.truth_fraction: диапазон изменён.");
}
focus = {
className: textValue(rawFocus.class_name, "vegetation.case.focus.class_name"),
labelId: integerValue(rawFocus.label_id, "vegetation.case.focus.label_id"),
truthPixels: integerValue(rawFocus.truth_pixels, "vegetation.case.focus.truth_pixels"),
truthFraction,
stratumRank: integerValue(rawFocus.stratum_rank, "vegetation.case.focus.stratum_rank"),
};
} else if (row.focus !== null) {
throw new VegetationShadowContractError("vegetation.case.focus: RAVNOVES focus отсутствует.");
}
return {
caseId,
sourceKind: expectedKind,
@@ -190,6 +222,7 @@ function visualCaseValue(
height: integerValue(row.height, "vegetation.case.height"),
centerCropXyxy: crop as unknown as readonly [number, number, number, number],
outsideCropState,
focus,
assets: projected,
};
}
@@ -233,8 +266,8 @@ function parseResult(value: unknown, resultId: string): VegetationShadowResult {
.map((item) => visualCaseValue(item, resultId, "ravnoves"));
const validationCases = arrayValue(catalogs.goose, "vegetation.catalogs.goose")
.map((item) => visualCaseValue(item, resultId, "goose"));
if (routeCases.length !== 12 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 + 12 случаев.");
if (routeCases.length !== 0 || validationCases.length !== 12) {
throw new VegetationShadowContractError("vegetation.catalogs: ожидалось 12 truth-backed GOOSE случаев без route viewer.");
}
return {
resultId,
@@ -28,6 +28,119 @@ const ATLAS_MODES = [
] as const;
type AtlasMode = typeof ATLAS_MODES[number]["value"];
const MASK_COMPARISON_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "prediction", label: "PREDICTION" },
{ value: "error", label: "ERROR" },
] as const;
const MASK_COMPARISON_CANDIDATES = [
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
type MaskComparisonMode = typeof MASK_COMPARISON_MODES[number]["value"];
type MaskComparisonCandidate = typeof MASK_COMPARISON_CANDIDATES[number]["value"];
export interface M48MaskComparisonCase {
caseId: string;
title: string;
context?: string;
sourceUrl: string;
truthUrl: string;
predictions: Readonly<Record<MaskComparisonCandidate, string>>;
errors: Readonly<Record<MaskComparisonCandidate, string>>;
}
function M48MaskComparisonScene({
item,
mode,
candidate,
}: {
item: M48MaskComparisonCase;
mode: MaskComparisonMode;
candidate: MaskComparisonCandidate;
}) {
const overlay = mode === "truth"
? item.truthUrl
: mode === "prediction"
? item.predictions[candidate]
: mode === "error"
? item.errors[candidate]
: null;
return (
<div className="recorded-evidence-image-scene">
<img src={item.sourceUrl} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function M48MaskComparisonVisual({
cases,
initialCandidate,
}: {
cases: readonly M48MaskComparisonCase[];
initialCandidate: MaskComparisonCandidate;
}) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<MaskComparisonMode>("error");
const [candidate, setCandidate] = useState<MaskComparisonCandidate>(initialCandidate);
const [expanded, setExpanded] = useState(false);
const item = cases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="M4.8 vegetation truth comparison"
className="m48-atlas-visual"
mode={mode}
modes={MASK_COMPARISON_MODES}
secondaryMode={{
value: candidate,
modes: MASK_COMPARISON_CANDIDATES,
label: "Сравниваемая модель",
onChange: setCandidate,
}}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
chromeLayout="stacked"
actions={(
<>
<IconButton
label="Предыдущий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current - 1 + cases.length) % cases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation hard case"
disabled={!cases.length}
onClick={() => setIndex((current) => (current + 1) % cases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE TRUTH · {index + 1}/{cases.length}</StatusBadge>
<strong>{item.title}</strong>
{item.context ? <small>{item.context}</small> : null}
</div>
) : null}
>
{item ? (
<M48MaskComparisonScene item={item} mode={mode} candidate={candidate} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
Vegetation hard-case каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
function message(error: unknown): string {
return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно.";
}
@@ -6,14 +6,45 @@ import {
} from "../../components/laboratory/LaboratoryPresentation";
import type { VegetationShadowResult } from "../../core/laboratory/vegetationShadow";
import {
VegetationRouteVisual,
VegetationValidationVisual,
} from "./VegetationShadowVisual";
M48MaskComparisonVisual,
type M48MaskComparisonCase,
} from "./M48FailureAtlasVisual";
function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
const VEGETATION_LABELS: Readonly<Record<string, string>> = {
high_grass: "Высокая трава",
low_grass: "Низкая трава",
bush: "Куст",
tree_trunk: "Ствол дерева",
tree_crown: "Крона дерева",
hedge: "Живая изгородь",
forest: "Лесная растительность",
crops: "Посевы",
};
function comparisonCases(result: VegetationShadowResult): readonly M48MaskComparisonCase[] {
return result.validationCases.map((item) => {
const focus = item.focus!;
return {
caseId: item.caseId,
title: `${VEGETATION_LABELS[focus.className] ?? focus.className} · truth ${decimal(focus.truthFraction * 100, 1)}% кадра`,
sourceUrl: item.assets.source,
truthUrl: item.assets.truth,
predictions: {
ddrnet: item.assets.ddrnet,
ppliteseg: item.assets.ppliteseg,
},
errors: {
ddrnet: item.assets.ddrnet_error,
ppliteseg: item.assets.ppliteseg_error,
},
};
});
}
export function VegetationShadowResultView({
rigLabel,
result,
@@ -31,21 +62,21 @@ export function VegetationShadowResultView({
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB V1 · растительность и mission-policy пресеты"
description="Две готовые fine-64 модели GOOSE проверены на полном validation split и перенесены в автономный визуальный shadow по RAVNOVES00. Интерфейс читает sealed-кадры локально и не зависит от доступности Worker 006."
status="Визуальный shadow готов · navigation authority OFF"
title="LAB V1 · готовые модели растительности"
description="Штатный M4.8-инструмент сравнивает две готовые fine-64 модели на полном GOOSE validation split и на 12 truth-backed hard cases, выбранных только по наличию нужной растительности. Sealed evidence открывается локально без Worker 006."
status="Truth-backed model comparison · route transfer не принят"
statusTone="warning"
facts={[
{ label: "Источник", value: `${rigLabel} RIGHT · 12 raw KB4 кадров + GOOSE validation 962` },
{ label: "Источник", value: "GOOSE validation · 962 размеченных кадра · 12 vegetation hard cases" },
{ label: "Сравнение", value: "DDRNet-39 vs PPLiteSeg · official fine-64 weights" },
{ label: "Policy", value: "Urban / rural / off-road · mission-configurable" },
{ label: "Authority", value: "SHADOW ONLY · commands OFF · geometry stays authoritative" },
{ label: "Кейсы", value: "трава · куст · ствол · крона · изгородь · лес · посевы" },
{ label: "Authority", value: `${rigLabel} · MODEL QUALIFICATION ONLY · commands OFF` },
]}
brief={{
question: "Можно ли взять готовую сегментацию растительности, увидеть её на нашем маршруте и сразу проверить разные правила миссии?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime: сначала 962 размеченных GOOSE-кадра, затем 12 детерминированных кадров RAVNOVES00. Для каждого кадра запечатаны source, обе семантики и три policy-проекции.",
principalResult: `${selected.loadedModelName} выбран по vegetation IoU ${decimal(selected.vegetationMeanIouPercent, 2)}% при shadow p95 ${decimal(selected.shadowLatencyP95Ms, 2)} ms. Визуальный результат доступен локально без Worker.`,
limitation: "RAVNOVES00 не размечен по fine-64, поэтому это перенос и визуальная проверка, а не доказательство точности или безопасности. Камерная семантика не может очищать жёсткую LiDAR/TGS occupancy.",
question: "Какие готовые веса лучше различают проезжаемую траву, кусты и стволы на размеченных off-road кадрах?",
approach: "Обе модели последовательно прогнаны в одном изолированном CUDA-runtime на 962 кадрах. 12 визуальных кейсов выбраны детерминированно по truth-поддержке восьми растительных классов; один M4.8 viewer показывает source, truth, prediction и material-error для выбранной модели.",
principalResult: `${selected.loadedModelName} лидирует по vegetation IoU: ${decimal(selected.vegetationMeanIouPercent, 2)}% против ${decimal(alternative.vegetationMeanIouPercent, 2)}%. Ошибки по каждому типу теперь проверяются в одном штатном инструменте.`,
limitation: "Это внешний GOOSE-домен, а не наш fisheye/off-road маршрут. Папоротник отдельным классом отсутствует; RAVNOVES00 не содержит truth-backed vegetation island и не используется как главное визуальное доказательство.",
}}
method={{
completeness: "complete",
@@ -62,29 +93,22 @@ export function VegetationShadowResultView({
/>
)}
evidence={(
<>
<LaboratoryEvidence
eyebrow="RAVNOVES00 · AUTONOMOUS VISUAL SHADOW"
title="Источник, обе модели и три правила миссии на одинаковых кадрах"
kind="diagnostic-model"
resizable
>
<VegetationRouteVisual result={result} />
</LaboratoryEvidence>
<LaboratoryEvidence
eyebrow="GOOSE · EXTERNAL VALIDATION EVIDENCE"
title="Независимая разметка: truth против DDRNet и PPLiteSeg"
kind="diagnostic-model"
resizable
>
<VegetationValidationVisual result={result} />
</LaboratoryEvidence>
</>
<LaboratoryEvidence
eyebrow="M4.8 · GOOSE VEGETATION HARD CASES"
title="ERROR: красный — пропуск · жёлтый — лишнее · фиолетовый — перепутан тип · зелёный — совпадение"
kind="diagnostic-model"
resizable
>
<M48MaskComparisonVisual
cases={comparisonCases(result)}
initialCandidate={result.selectedCandidate}
/>
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Готовые веса дают рабочую точку старта, но ещё не право ехать"
status={`${selected.loadedModelName} выбран для shadow`}
title="DDRNet — стартовые веса; перенос на ровер ещё не доказан"
status={`${selected.loadedModelName} выбран только как vegetation candidate`}
statusTone="warning"
metrics={[
{
@@ -98,7 +122,7 @@ export function VegetationShadowResultView({
hint: "агрегация классов grass/vegetation/bush/tree и родственных fine-64 labels",
},
{
label: "RAVNOVES p95",
label: "Worker shadow p95",
value: `${decimal(selected.shadowLatencyP95Ms, 2)} / ${decimal(alternative.shadowLatencyP95Ms, 2)} ms`,
hint: "чистый inference · одна тяжёлая модель за раз",
},
@@ -108,7 +132,7 @@ export function VegetationShadowResultView({
hint: "один явный inference до допуска кадров; исключён из steady-state p95",
},
{
label: "RAVNOVES throughput",
label: "Worker throughput",
value: `${decimal(selected.shadowThroughputFps, 1)} / ${decimal(alternative.shadowThroughputFps, 1)} FPS`,
hint: "изолированный Worker 006 · не realtime graph целиком",
},
@@ -118,15 +142,15 @@ export function VegetationShadowResultView({
hint: `${selected.candidate} / ${alternative.candidate} · RTX 4090`,
},
{
label: "Autonomous evidence",
value: "12 route + 12 validation",
hint: "только текущий кадр загружается в viewer; Worker не требуется",
label: "Hard-case evidence",
value: "12 truth-backed cases",
hint: "8 vegetation strata · Worker для открытия не требуется",
},
]}
conclusion={{
proved: "Обе официальные fine-64 модели запускаются на Worker 006, проходят полный GOOSE validation и дают воспроизводимые растительные маски на 12 фиксированных RAVNOVES00 кадрах. Urban/rural/off-road policy-проекции формируются без повторного inference.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, различение тонкой травы от толстого ствола во всех условиях, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить выбранную модель как shadow provider. Следующий критический блок — разметить небольшой hard-case island нашего офф-роуда: трава, папоротник, куст с толстыми стволами и дерево; затем калибровать policy без ослабления LiDAR/TGS fail-closed геометрии.",
proved: "Обе официальные fine-64 модели воспроизводимо запускаются на Worker 006; DDRNet лучше по aggregate vegetation IoU. Truth-backed hard cases прямо показывают траву, кусты и стволы, а не случайные автомобили и здания.",
notProved: "Не доказаны accuracy на нашем fisheye-домене, папоротник как отдельный материал, temporal stability, collision safety и physical-live поведение ровера.",
decision: "Сохранить DDRNet как стартовый vegetation candidate. Mission-policy и автоматическое переключение пресетов подключать только после truth-backed island нашего офф-роуда; LiDAR/TGS fail-closed геометрию не ослаблять.",
}}
/>
)}
@@ -1,157 +0,0 @@
import { useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
VegetationRouteMode,
VegetationShadowResult,
VegetationValidationMode,
VegetationVisualCase,
} from "../../core/laboratory/vegetationShadow";
const ROUTE_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
{ value: "urban", label: "URBAN" },
{ value: "rural", label: "RURAL" },
{ value: "offroad", label: "OFF-ROAD" },
] as const;
const VALIDATION_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "ddrnet", label: "DDRNET" },
{ value: "ppliteseg", label: "PPLITE" },
] as const;
function VegetationScene({
item,
mode,
}: {
item: VegetationVisualCase;
mode: string;
}) {
const overlay = mode === "source" ? null : item.assets[mode];
return (
<div className="recorded-evidence-image-scene">
<img src={item.assets.source} alt="" draggable={false} />
{overlay ? <img src={overlay} alt="" draggable={false} /> : null}
</div>
);
}
export function VegetationRouteVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationRouteMode>("offroad");
const [expanded, setExpanded] = useState(false);
const item = result.routeCases[index] ?? null;
const selected = result.candidates.find(
(candidate) => candidate.candidate === result.selectedCandidate,
);
return (
<LaboratoryEvidenceViewer
label="RAVNOVES00 vegetation policy shadow"
className="m48-atlas-visual"
mode={mode}
modes={ROUTE_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.routeCases.length
) % result.routeCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий vegetation shadow кадр"
disabled={!result.routeCases.length}
onClick={() => setIndex((current) => (current + 1) % result.routeCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="warning">SHADOW ONLY</StatusBadge>
<strong>RAVNOVES00 · {item.caseId} · {mode.toUpperCase()}</strong>
<small>
{selected?.loadedModelName ?? result.selectedCandidate} policy provider
{" · "}center crop 600×600
{" · "}outside crop UNKNOWN
</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
RAVNOVES vegetation shadow каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
export function VegetationValidationVisual({ result }: { result: VegetationShadowResult }) {
const [index, setIndex] = useState(0);
const [mode, setMode] = useState<VegetationValidationMode>("truth");
const [expanded, setExpanded] = useState(false);
const item = result.validationCases[index] ?? null;
return (
<LaboratoryEvidenceViewer
label="GOOSE validation vegetation comparison"
className="m48-atlas-visual"
mode={mode}
modes={VALIDATION_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton
label="Предыдущий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (
current - 1 + result.validationCases.length
) % result.validationCases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий GOOSE validation кадр"
disabled={!result.validationCases.length}
onClick={() => setIndex((current) => (current + 1) % result.validationCases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone="accent">GOOSE VALIDATION</StatusBadge>
<strong>{item.caseId} · {mode.toUpperCase()}</strong>
<small>official fine-64 labels · fixed 512×512 preprocessing · visual sample</small>
</div>
) : null}
>
{item ? (
<VegetationScene item={item} mode={mode} />
) : (
<div className="m48-atlas-visual__state" role="alert">
<Icon name="alert" size={18} />
GOOSE validation каталог пуст.
</div>
)}
</LaboratoryEvidenceViewer>
);
}
@@ -65,10 +65,10 @@ const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"lab-v1-vegetation-shadow": {
profileId: "rig-ravnoves-perception-gate-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 vegetation shadow`,
profileName: (rigLabel) => `${rig(rigLabel)} · GOOSE vegetation qualification`,
experimentId: "lab-v1-vegetation-mission-policy",
experimentName: "GOOSE ready weights → RAVNOVES vegetation policy",
variantName: "LAB V1 · DDRNet vs PPLiteSeg · urban/rural/off-road presets",
experimentName: "DDRNet vs PPLiteSeg · truth-backed vegetation hard cases",
variantName: "LAB V1 · готовые vegetation weights · GOOSE truth",
},
"m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { access, readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
@@ -51,7 +52,7 @@ function candidate(candidateKey, vegetationIou) {
function visualCase(sourceKind, index) {
const caseId = `case-${index}`;
const keys = sourceKind === "goose"
? ["source", "truth", "ddrnet", "ppliteseg"]
? ["source", "truth", "ddrnet", "ppliteseg", "ddrnet_error", "ppliteseg_error"]
: ["source", "ddrnet", "ppliteseg", "urban", "rural", "offroad"];
return {
case_id: caseId,
@@ -60,6 +61,13 @@ function visualCase(sourceKind, index) {
height: sourceKind === "goose" ? 512 : 600,
center_crop_xyxy: sourceKind === "goose" ? [0, 0, 512, 512] : [100, 0, 700, 600],
outside_crop_state: sourceKind === "goose" ? "not-applicable" : "undefined",
focus: sourceKind === "goose" ? {
class_name: "high_grass",
label_id: 51,
truth_pixels: 16384,
truth_fraction: 0.0625,
stratum_rank: index + 1,
} : null,
assets: Object.fromEntries(keys.map((key) => [key, {
path: `visual/${sourceKind}/${caseId}/${key}.png`,
sha256: "d".repeat(64),
@@ -101,7 +109,7 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
},
catalogs: {
goose: Array.from({ length: 12 }, (_, index) => visualCase("goose", index)),
ravnoves: Array.from({ length: 12 }, (_, index) => visualCase("ravnoves", index)),
ravnoves: [],
},
access: "read-only",
}), { status: 200, headers: { "Content-Type": "application/json" } });
@@ -113,9 +121,10 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
);
assert.equal(result.selectedCandidate, "ddrnet");
assert.equal(result.candidates[0].vegetationMeanIouPercent, 64);
assert.equal(result.routeCases.length, 12);
assert.equal(result.routeCases.length, 0);
assert.equal(result.validationCases.length, 12);
assert.match(result.routeCases[0].assets.offroad, /\/assets\/visual\/ravnoves\//);
assert.equal(result.validationCases[0].focus.className, "high_grass");
assert.match(result.validationCases[0].assets.ddrnet_error, /\/assets\/visual\/goose\//);
assert.deepEqual(result.authority, {
commandsEnabled: false,
navigationOrSafetyAccepted: false,
@@ -123,3 +132,17 @@ test("vegetation LAB keeps autonomous assets and fail-closed authority", async (
cameraSemanticsCanClearRigidGeometry: false,
});
});
test("vegetation LAB reuses the admitted M4.8 instrument", async () => {
const resultSource = await readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
);
assert.match(resultSource, /M48MaskComparisonVisual/);
assert.equal(resultSource.match(/<LaboratoryEvidence\b/g)?.length, 1);
assert.doesNotMatch(resultSource, /VegetationRouteVisual|urban\/rural\/off-road presets/);
await assert.rejects(
access(new URL("../src/workspaces/laboratory/VegetationShadowVisual.tsx", import.meta.url)),
{ code: "ENOENT" },
);
});