Spaces:
Paused
Paused
Deploy Sensorflow Studio (part 3)
Browse files- src/App.tsx +13 -8
- src/components/help/HelpMenu.tsx +3 -1
- src/components/rotr/RotrAttributionHeatmap.tsx +140 -0
- src/components/rotr/RotrBevReplay.tsx +225 -0
- src/components/rotr/RotrQueryBuilder.tsx +128 -0
- src/content/glossary.ts +95 -57
- src/content/pageHelp.ts +48 -36
- src/context/LabelEvalContext.tsx +3 -1
- src/pages/rotr/RotrControlCenterPage.tsx +843 -0
- src/services/rotr.ts +134 -0
- src/types/rotr.ts +292 -0
src/App.tsx
CHANGED
|
@@ -41,14 +41,15 @@ import {
|
|
| 41 |
Sparkles,
|
| 42 |
LineChart,
|
| 43 |
Layers,
|
| 44 |
-
RefreshCcwDot,
|
| 45 |
-
ClipboardCheck,
|
| 46 |
-
BadgeCheck,
|
| 47 |
Shapes,
|
| 48 |
Network,
|
| 49 |
LayoutGrid,
|
| 50 |
Undo2,
|
|
|
|
| 51 |
Scale,
|
|
|
|
|
|
|
|
|
|
| 52 |
} from 'lucide-react';
|
| 53 |
import { LabelEvalContext, type PageId, ALL_PAGE_IDS } from './context/LabelEvalContext';
|
| 54 |
import { getOverview, useStream } from './services/labeleval';
|
|
@@ -85,11 +86,12 @@ import BevFusionPage from './pages/engines/BevFusionPage';
|
|
| 85 |
import ScenarioComposerPage from './pages/studio/ScenarioComposerPage';
|
| 86 |
import PipelineBuilderPage from './pages/studio/PipelineBuilderPage';
|
| 87 |
import MyDashboardPage from './pages/studio/MyDashboardPage';
|
|
|
|
| 88 |
import ClosedLoopLabPage from './pages/nextgen/ClosedLoopLabPage';
|
|
|
|
| 89 |
import Studio2GovernancePage from './pages/studio2/Studio2GovernancePage';
|
| 90 |
import ProductionReadinessPage from './pages/hardening/ProductionReadinessPage';
|
| 91 |
-
import
|
| 92 |
-
import LaunchReadinessPage from './pages/agentic/LaunchReadinessPage';
|
| 93 |
import PageIntro from './components/help/PageIntro';
|
| 94 |
import HelpMenu from './components/help/HelpMenu';
|
| 95 |
|
|
@@ -144,9 +146,10 @@ const SAFETY_NAV: NavItem[] = [
|
|
| 144 |
{ id: 'safety-discrepancy', label: 'Discrepancy Mining', icon: <GitCompareArrows size={18} /> },
|
| 145 |
{ id: 'safety-scenarios', label: 'Scenario DB', icon: <Library size={18} /> },
|
| 146 |
{ id: 'safety-search', label: 'Semantic Search', icon: <Sparkles size={18} /> },
|
| 147 |
-
{ id: 'studio2', label: 'Studio 2.0 Governance', icon: <ClipboardCheck size={18} /> },
|
| 148 |
{ id: 'retro', label: 'Retrospective Analyzer', icon: <Undo2 size={18} /> },
|
| 149 |
{ id: 'launch-readiness', label: 'Launch Readiness', icon: <Scale size={18} /> },
|
|
|
|
|
|
|
| 150 |
];
|
| 151 |
|
| 152 |
const LEGACY_NAV: NavItem[] = [
|
|
@@ -191,8 +194,9 @@ const PAGE_TITLES: Record<PageId, string> = {
|
|
| 191 |
'closed-loop-lab': 'Closed-Loop Lab',
|
| 192 |
'launch-readiness': 'Launch Readiness (Agentic Triage)',
|
| 193 |
studio2: 'Studio 2.0 Governance (Control Plane & Release)',
|
| 194 |
-
'production-readiness': 'Production Readiness (Hardening Audit)',
|
| 195 |
legacy: 'Legacy Studio',
|
|
|
|
|
|
|
| 196 |
};
|
| 197 |
|
| 198 |
function parseHash(): { page: PageId; entityId: string | null } {
|
|
@@ -443,10 +447,11 @@ export default function App() {
|
|
| 443 |
{page === 'pipeline-builder' ? <PipelineBuilderPage /> : null}
|
| 444 |
{page === 'my-dashboard' ? <MyDashboardPage /> : null}
|
| 445 |
{page === 'retro' ? <RetroAnalyzerPage /> : null}
|
| 446 |
-
{page === 'launch-readiness' ? <LaunchReadinessPage /> : null}
|
| 447 |
{page === 'closed-loop-lab' ? <ClosedLoopLabPage /> : null}
|
|
|
|
| 448 |
{page === 'studio2' ? <Studio2GovernancePage /> : null}
|
| 449 |
{page === 'production-readiness' ? <ProductionReadinessPage /> : null}
|
|
|
|
| 450 |
</Box>
|
| 451 |
)}
|
| 452 |
</Box>
|
|
|
|
| 41 |
Sparkles,
|
| 42 |
LineChart,
|
| 43 |
Layers,
|
|
|
|
|
|
|
|
|
|
| 44 |
Shapes,
|
| 45 |
Network,
|
| 46 |
LayoutGrid,
|
| 47 |
Undo2,
|
| 48 |
+
RefreshCcwDot,
|
| 49 |
Scale,
|
| 50 |
+
ClipboardCheck,
|
| 51 |
+
BadgeCheck,
|
| 52 |
+
TrafficCone,
|
| 53 |
} from 'lucide-react';
|
| 54 |
import { LabelEvalContext, type PageId, ALL_PAGE_IDS } from './context/LabelEvalContext';
|
| 55 |
import { getOverview, useStream } from './services/labeleval';
|
|
|
|
| 86 |
import ScenarioComposerPage from './pages/studio/ScenarioComposerPage';
|
| 87 |
import PipelineBuilderPage from './pages/studio/PipelineBuilderPage';
|
| 88 |
import MyDashboardPage from './pages/studio/MyDashboardPage';
|
| 89 |
+
import RetroAnalyzerPage from './pages/retro/RetroAnalyzerPage';
|
| 90 |
import ClosedLoopLabPage from './pages/nextgen/ClosedLoopLabPage';
|
| 91 |
+
import LaunchReadinessPage from './pages/agentic/LaunchReadinessPage';
|
| 92 |
import Studio2GovernancePage from './pages/studio2/Studio2GovernancePage';
|
| 93 |
import ProductionReadinessPage from './pages/hardening/ProductionReadinessPage';
|
| 94 |
+
import RotrControlCenterPage from './pages/rotr/RotrControlCenterPage';
|
|
|
|
| 95 |
import PageIntro from './components/help/PageIntro';
|
| 96 |
import HelpMenu from './components/help/HelpMenu';
|
| 97 |
|
|
|
|
| 146 |
{ id: 'safety-discrepancy', label: 'Discrepancy Mining', icon: <GitCompareArrows size={18} /> },
|
| 147 |
{ id: 'safety-scenarios', label: 'Scenario DB', icon: <Library size={18} /> },
|
| 148 |
{ id: 'safety-search', label: 'Semantic Search', icon: <Sparkles size={18} /> },
|
|
|
|
| 149 |
{ id: 'retro', label: 'Retrospective Analyzer', icon: <Undo2 size={18} /> },
|
| 150 |
{ id: 'launch-readiness', label: 'Launch Readiness', icon: <Scale size={18} /> },
|
| 151 |
+
{ id: 'studio2', label: 'Studio 2.0 Governance', icon: <ClipboardCheck size={18} /> },
|
| 152 |
+
{ id: 'rotr', label: 'ROTR Control Center', icon: <TrafficCone size={18} /> },
|
| 153 |
];
|
| 154 |
|
| 155 |
const LEGACY_NAV: NavItem[] = [
|
|
|
|
| 194 |
'closed-loop-lab': 'Closed-Loop Lab',
|
| 195 |
'launch-readiness': 'Launch Readiness (Agentic Triage)',
|
| 196 |
studio2: 'Studio 2.0 Governance (Control Plane & Release)',
|
|
|
|
| 197 |
legacy: 'Legacy Studio',
|
| 198 |
+
'production-readiness': 'Production Readiness (Hardening Audit)',
|
| 199 |
+
rotr: 'ROTR Control Center (Right-of-the-Road)',
|
| 200 |
};
|
| 201 |
|
| 202 |
function parseHash(): { page: PageId; entityId: string | null } {
|
|
|
|
| 447 |
{page === 'pipeline-builder' ? <PipelineBuilderPage /> : null}
|
| 448 |
{page === 'my-dashboard' ? <MyDashboardPage /> : null}
|
| 449 |
{page === 'retro' ? <RetroAnalyzerPage /> : null}
|
|
|
|
| 450 |
{page === 'closed-loop-lab' ? <ClosedLoopLabPage /> : null}
|
| 451 |
+
{page === 'launch-readiness' ? <LaunchReadinessPage /> : null}
|
| 452 |
{page === 'studio2' ? <Studio2GovernancePage /> : null}
|
| 453 |
{page === 'production-readiness' ? <ProductionReadinessPage /> : null}
|
| 454 |
+
{page === 'rotr' ? <RotrControlCenterPage /> : null}
|
| 455 |
</Box>
|
| 456 |
)}
|
| 457 |
</Box>
|
src/components/help/HelpMenu.tsx
CHANGED
|
@@ -201,11 +201,12 @@ const PAGE_ORDER: PageId[] = [
|
|
| 201 |
'safety-scenarios',
|
| 202 |
'safety-search',
|
| 203 |
'retro',
|
| 204 |
-
'closed-loop-lab',
|
| 205 |
'launch-readiness',
|
|
|
|
| 206 |
'studio2',
|
| 207 |
'legacy',
|
| 208 |
'production-readiness',
|
|
|
|
| 209 |
];
|
| 210 |
|
| 211 |
const PAGE_NAMES: Record<PageId, string> = {
|
|
@@ -247,6 +248,7 @@ const PAGE_NAMES: Record<PageId, string> = {
|
|
| 247 |
studio2: 'Studio 2.0 Governance',
|
| 248 |
legacy: 'Legacy Studio',
|
| 249 |
'production-readiness': 'Production Readiness',
|
|
|
|
| 250 |
};
|
| 251 |
|
| 252 |
function PageIndex({ goTo }: { goTo: (p: PageId) => void }) {
|
|
|
|
| 201 |
'safety-scenarios',
|
| 202 |
'safety-search',
|
| 203 |
'retro',
|
|
|
|
| 204 |
'launch-readiness',
|
| 205 |
+
'closed-loop-lab',
|
| 206 |
'studio2',
|
| 207 |
'legacy',
|
| 208 |
'production-readiness',
|
| 209 |
+
'rotr',
|
| 210 |
];
|
| 211 |
|
| 212 |
const PAGE_NAMES: Record<PageId, string> = {
|
|
|
|
| 248 |
studio2: 'Studio 2.0 Governance',
|
| 249 |
legacy: 'Legacy Studio',
|
| 250 |
'production-readiness': 'Production Readiness',
|
| 251 |
+
rotr: 'ROTR Control Center',
|
| 252 |
};
|
| 253 |
|
| 254 |
function PageIndex({ goTo }: { goTo: (p: PageId) => void }) {
|
src/components/rotr/RotrAttributionHeatmap.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Attribution-matrix heatmap: one row per violation, one column per causal
|
| 3 |
+
* layer. Cell color encodes tri-state evidence (SUPPORTED / RULED_OUT /
|
| 4 |
+
* UNKNOWN); the primary layer carries a ring. Clicking a row selects the
|
| 5 |
+
* violation for the consequence replay view.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import Box from '@mui/material/Box';
|
| 9 |
+
import Chip from '@mui/material/Chip';
|
| 10 |
+
import Tooltip from '@mui/material/Tooltip';
|
| 11 |
+
import Typography from '@mui/material/Typography';
|
| 12 |
+
import type { RotrAttributionMatrix } from '../../types/rotr';
|
| 13 |
+
|
| 14 |
+
const LAYERS = [
|
| 15 |
+
'perception',
|
| 16 |
+
'prediction',
|
| 17 |
+
'planning',
|
| 18 |
+
'localization',
|
| 19 |
+
'map',
|
| 20 |
+
'control',
|
| 21 |
+
'policy_rule',
|
| 22 |
+
'data_label',
|
| 23 |
+
];
|
| 24 |
+
|
| 25 |
+
const STATUS_COLOR: Record<string, string> = {
|
| 26 |
+
SUPPORTED: '#c62828',
|
| 27 |
+
RULED_OUT: '#1b5e20',
|
| 28 |
+
UNKNOWN: '#616161',
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
interface Props {
|
| 32 |
+
matrix: RotrAttributionMatrix;
|
| 33 |
+
selectedViolationId: string | null;
|
| 34 |
+
onSelect: (violationId: string) => void;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export default function RotrAttributionHeatmap({ matrix, selectedViolationId, onSelect }: Props) {
|
| 38 |
+
return (
|
| 39 |
+
<Box sx={{ overflowX: 'auto' }}>
|
| 40 |
+
<Box sx={{ display: 'flex', gap: 1, mb: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
| 41 |
+
{Object.entries(STATUS_COLOR).map(([status, color]) => (
|
| 42 |
+
<Chip
|
| 43 |
+
key={status}
|
| 44 |
+
size="small"
|
| 45 |
+
label={status}
|
| 46 |
+
sx={{ bgcolor: color, color: '#fff', fontSize: 10, height: 18 }}
|
| 47 |
+
/>
|
| 48 |
+
))}
|
| 49 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 50 |
+
ring = primary layer (highest-confidence positive evidence)
|
| 51 |
+
</Typography>
|
| 52 |
+
</Box>
|
| 53 |
+
<table style={{ borderCollapse: 'separate', borderSpacing: 3 }}>
|
| 54 |
+
<thead>
|
| 55 |
+
<tr>
|
| 56 |
+
<th style={{ textAlign: 'left', fontSize: 11, color: '#8a949e', paddingRight: 8 }}>
|
| 57 |
+
violation
|
| 58 |
+
</th>
|
| 59 |
+
{LAYERS.map((l) => (
|
| 60 |
+
<th
|
| 61 |
+
key={l}
|
| 62 |
+
style={{
|
| 63 |
+
fontSize: 10,
|
| 64 |
+
color: '#8a949e',
|
| 65 |
+
writingMode: 'vertical-rl',
|
| 66 |
+
transform: 'rotate(180deg)',
|
| 67 |
+
padding: '2px 0',
|
| 68 |
+
}}
|
| 69 |
+
>
|
| 70 |
+
{l}
|
| 71 |
+
</th>
|
| 72 |
+
))}
|
| 73 |
+
</tr>
|
| 74 |
+
</thead>
|
| 75 |
+
<tbody>
|
| 76 |
+
{matrix.rows.map((row) => {
|
| 77 |
+
const selected = row.violation_id === selectedViolationId;
|
| 78 |
+
return (
|
| 79 |
+
<tr
|
| 80 |
+
key={row.violation_id}
|
| 81 |
+
onClick={() => onSelect(row.violation_id)}
|
| 82 |
+
style={{ cursor: 'pointer' }}
|
| 83 |
+
>
|
| 84 |
+
<td
|
| 85 |
+
style={{
|
| 86 |
+
fontSize: 11,
|
| 87 |
+
fontFamily: 'monospace',
|
| 88 |
+
color: selected ? '#4fc3f7' : '#c9d1d9',
|
| 89 |
+
paddingRight: 8,
|
| 90 |
+
whiteSpace: 'nowrap',
|
| 91 |
+
borderLeft: selected ? '3px solid #4fc3f7' : '3px solid transparent',
|
| 92 |
+
paddingLeft: 5,
|
| 93 |
+
}}
|
| 94 |
+
>
|
| 95 |
+
{row.violation_id.replace(/^bank-[0-9a-f]+-/, '')}
|
| 96 |
+
</td>
|
| 97 |
+
{LAYERS.map((layer) => {
|
| 98 |
+
const cell = row.layers[layer];
|
| 99 |
+
const status = cell?.status ?? 'UNKNOWN';
|
| 100 |
+
const isPrimary = row.primary_layer === layer;
|
| 101 |
+
return (
|
| 102 |
+
<Tooltip
|
| 103 |
+
key={layer}
|
| 104 |
+
title={
|
| 105 |
+
<Box sx={{ maxWidth: 340 }}>
|
| 106 |
+
<b>
|
| 107 |
+
{layer}: {status}
|
| 108 |
+
</b>{' '}
|
| 109 |
+
(conf {cell ? cell.confidence.toFixed(2) : '—'})
|
| 110 |
+
<br />
|
| 111 |
+
{cell?.evidence ?? 'no evidence recorded'}
|
| 112 |
+
</Box>
|
| 113 |
+
}
|
| 114 |
+
arrow
|
| 115 |
+
>
|
| 116 |
+
<td
|
| 117 |
+
style={{
|
| 118 |
+
width: 26,
|
| 119 |
+
height: 20,
|
| 120 |
+
background: STATUS_COLOR[status],
|
| 121 |
+
opacity: status === 'SUPPORTED' ? (cell ? 0.45 + 0.55 * cell.confidence : 1) : 0.85,
|
| 122 |
+
borderRadius: 3,
|
| 123 |
+
outline: isPrimary ? '2px solid #ffb300' : 'none',
|
| 124 |
+
outlineOffset: -2,
|
| 125 |
+
}}
|
| 126 |
+
/>
|
| 127 |
+
</Tooltip>
|
| 128 |
+
);
|
| 129 |
+
})}
|
| 130 |
+
</tr>
|
| 131 |
+
);
|
| 132 |
+
})}
|
| 133 |
+
</tbody>
|
| 134 |
+
</table>
|
| 135 |
+
<Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
|
| 136 |
+
{matrix.invariant}
|
| 137 |
+
</Typography>
|
| 138 |
+
</Box>
|
| 139 |
+
);
|
| 140 |
+
}
|
src/components/rotr/RotrBevReplay.tsx
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* BEV schematic replay of one violation: top-down road geometry (lanes,
|
| 3 |
+
* stop line, crosswalk, intersection box), actor positions at the scrubbed
|
| 4 |
+
* time, and the OBSERVED vs CORRECTED ego trajectories from the
|
| 5 |
+
* counterfactual replay.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { useMemo, useState } from 'react';
|
| 9 |
+
import Box from '@mui/material/Box';
|
| 10 |
+
import Chip from '@mui/material/Chip';
|
| 11 |
+
import Slider from '@mui/material/Slider';
|
| 12 |
+
import Typography from '@mui/material/Typography';
|
| 13 |
+
import type { RotrConsequenceDetail, RotrTrajectoryPoint } from '../../types/rotr';
|
| 14 |
+
|
| 15 |
+
const W = 860;
|
| 16 |
+
const H = 340;
|
| 17 |
+
const PAD = 24;
|
| 18 |
+
|
| 19 |
+
const ACTOR_COLOR: Record<string, string> = {
|
| 20 |
+
pedestrian: '#ffb300',
|
| 21 |
+
cyclist: '#ff7043',
|
| 22 |
+
vehicle: '#ab47bc',
|
| 23 |
+
bus: '#7e57c2',
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
interface Props {
|
| 27 |
+
detail: RotrConsequenceDetail;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function nearest(traj: RotrTrajectoryPoint[], t: number): RotrTrajectoryPoint | null {
|
| 31 |
+
if (!traj.length) return null;
|
| 32 |
+
let best = traj[0];
|
| 33 |
+
for (const p of traj) if (Math.abs(p.t - t) < Math.abs(best.t - t)) best = p;
|
| 34 |
+
return best;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export default function RotrBevReplay({ detail }: Props) {
|
| 38 |
+
const obs = detail.planner_evaluation.observed_trajectory;
|
| 39 |
+
const corr = detail.planner_evaluation.corrected_trajectory;
|
| 40 |
+
const geom = detail.scenario_geometry;
|
| 41 |
+
const tMax = obs.length ? obs[obs.length - 1].t : 0;
|
| 42 |
+
const [t, setT] = useState(tMax / 2);
|
| 43 |
+
|
| 44 |
+
const bounds = useMemo(() => {
|
| 45 |
+
let xMin = -12;
|
| 46 |
+
let xMax = 62;
|
| 47 |
+
let yMin = -9;
|
| 48 |
+
let yMax = 9;
|
| 49 |
+
for (const p of [...obs, ...corr]) {
|
| 50 |
+
xMin = Math.min(xMin, p.x - 3);
|
| 51 |
+
xMax = Math.max(xMax, p.x + 3);
|
| 52 |
+
yMin = Math.min(yMin, p.y - 3);
|
| 53 |
+
yMax = Math.max(yMax, p.y + 3);
|
| 54 |
+
}
|
| 55 |
+
for (const a of geom?.actors ?? []) {
|
| 56 |
+
for (const s of a.states) {
|
| 57 |
+
if (s.y > -12 && s.y < 24) {
|
| 58 |
+
yMin = Math.min(yMin, s.y - 2);
|
| 59 |
+
yMax = Math.max(yMax, s.y + 2);
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
return { xMin, xMax, yMin, yMax };
|
| 64 |
+
}, [obs, corr, geom]);
|
| 65 |
+
|
| 66 |
+
const sx = (x: number) =>
|
| 67 |
+
PAD + ((x - bounds.xMin) / (bounds.xMax - bounds.xMin)) * (W - 2 * PAD);
|
| 68 |
+
const sy = (y: number) =>
|
| 69 |
+
PAD + ((bounds.yMax - y) / (bounds.yMax - bounds.yMin)) * (H - 2 * PAD);
|
| 70 |
+
|
| 71 |
+
const path = (traj: RotrTrajectoryPoint[]) =>
|
| 72 |
+
traj.map((p, i) => `${i === 0 ? 'M' : 'L'}${sx(p.x).toFixed(1)},${sy(p.y).toFixed(1)}`).join(' ');
|
| 73 |
+
|
| 74 |
+
const obsNow = nearest(obs, t);
|
| 75 |
+
const corrNow = nearest(corr, t);
|
| 76 |
+
const ctx = geom?.actual_context;
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<Box>
|
| 80 |
+
<Box sx={{ display: 'flex', gap: 1, mb: 1, flexWrap: 'wrap', alignItems: 'center' }}>
|
| 81 |
+
<Chip size="small" label="observed ego" sx={{ bgcolor: '#c62828', color: '#fff', height: 20 }} />
|
| 82 |
+
<Chip size="small" label="corrected ego (counterfactual)" sx={{ bgcolor: '#2e7d32', color: '#fff', height: 20 }} />
|
| 83 |
+
<Chip
|
| 84 |
+
size="small"
|
| 85 |
+
label={`engine: ${detail.planner_evaluation.engine}`}
|
| 86 |
+
sx={{ bgcolor: '#232a31', height: 20, fontFamily: 'monospace', fontSize: 10 }}
|
| 87 |
+
/>
|
| 88 |
+
<Chip
|
| 89 |
+
size="small"
|
| 90 |
+
label={detail.consequence_class}
|
| 91 |
+
sx={{
|
| 92 |
+
bgcolor: detail.consequence_class === 'SAFETY_CRITICAL' ? '#b71c1c' : '#37474f',
|
| 93 |
+
color: '#fff',
|
| 94 |
+
height: 20,
|
| 95 |
+
fontWeight: 700,
|
| 96 |
+
}}
|
| 97 |
+
/>
|
| 98 |
+
</Box>
|
| 99 |
+
<svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ background: '#0d1117', borderRadius: 8 }}>
|
| 100 |
+
{/* lanes */}
|
| 101 |
+
{ctx?.lanes.map((lane) => (
|
| 102 |
+
<g key={lane.lane_id}>
|
| 103 |
+
<rect
|
| 104 |
+
x={sx(bounds.xMin)}
|
| 105 |
+
y={sy(lane.center_y + 1.75)}
|
| 106 |
+
width={sx(bounds.xMax) - sx(bounds.xMin)}
|
| 107 |
+
height={sy(lane.center_y - 1.75) - sy(lane.center_y + 1.75)}
|
| 108 |
+
fill={lane.restricted_to ? 'rgba(255,112,67,0.10)' : 'rgba(255,255,255,0.045)'}
|
| 109 |
+
stroke="#2c333b"
|
| 110 |
+
strokeDasharray="6 5"
|
| 111 |
+
/>
|
| 112 |
+
<text x={sx(bounds.xMin) + 4} y={sy(lane.center_y) + 4} fontSize={10} fill="#5c6670">
|
| 113 |
+
{lane.lane_id}
|
| 114 |
+
{lane.restricted_to ? ` (${lane.restricted_to}-only)` : ''}
|
| 115 |
+
</text>
|
| 116 |
+
</g>
|
| 117 |
+
))}
|
| 118 |
+
{/* intersection box */}
|
| 119 |
+
{ctx?.intersection_x_min != null && ctx?.intersection_x_max != null ? (
|
| 120 |
+
<rect
|
| 121 |
+
x={sx(ctx.intersection_x_min)}
|
| 122 |
+
y={PAD}
|
| 123 |
+
width={sx(ctx.intersection_x_max) - sx(ctx.intersection_x_min)}
|
| 124 |
+
height={H - 2 * PAD}
|
| 125 |
+
fill="rgba(79,195,247,0.05)"
|
| 126 |
+
stroke="#31424e"
|
| 127 |
+
/>
|
| 128 |
+
) : null}
|
| 129 |
+
{/* crosswalks */}
|
| 130 |
+
{ctx?.crosswalks.map((cw) => (
|
| 131 |
+
<g key={cw.crosswalk_id}>
|
| 132 |
+
{Array.from({ length: 7 }).map((_, i) => (
|
| 133 |
+
<rect
|
| 134 |
+
key={i}
|
| 135 |
+
x={sx(cw.x_min) + (i * (sx(cw.x_max) - sx(cw.x_min))) / 7 + 2}
|
| 136 |
+
y={sy(5)}
|
| 137 |
+
width={(sx(cw.x_max) - sx(cw.x_min)) / 7 - 4}
|
| 138 |
+
height={sy(-5) - sy(5)}
|
| 139 |
+
fill="rgba(255,255,255,0.10)"
|
| 140 |
+
/>
|
| 141 |
+
))}
|
| 142 |
+
</g>
|
| 143 |
+
))}
|
| 144 |
+
{/* stop line */}
|
| 145 |
+
{ctx?.stop_line_x != null ? (
|
| 146 |
+
<line
|
| 147 |
+
x1={sx(ctx.stop_line_x)}
|
| 148 |
+
y1={PAD}
|
| 149 |
+
x2={sx(ctx.stop_line_x)}
|
| 150 |
+
y2={H - PAD}
|
| 151 |
+
stroke="#e53935"
|
| 152 |
+
strokeWidth={2}
|
| 153 |
+
strokeDasharray="8 4"
|
| 154 |
+
/>
|
| 155 |
+
) : null}
|
| 156 |
+
{/* trajectories */}
|
| 157 |
+
<path d={path(obs)} fill="none" stroke="#c62828" strokeWidth={2.2} />
|
| 158 |
+
<path d={path(corr)} fill="none" stroke="#2e7d32" strokeWidth={2.2} strokeDasharray="7 4" />
|
| 159 |
+
{/* actors at t */}
|
| 160 |
+
{geom?.actors.map((a) => {
|
| 161 |
+
const s = a.states.length
|
| 162 |
+
? a.states.reduce((b, c) => (Math.abs(c.t - t) < Math.abs(b.t - t) ? c : b))
|
| 163 |
+
: null;
|
| 164 |
+
if (!s) return null;
|
| 165 |
+
const color = ACTOR_COLOR[a.class_name] ?? '#90a4ae';
|
| 166 |
+
const isPed = a.class_name === 'pedestrian' || a.class_name === 'cyclist';
|
| 167 |
+
return (
|
| 168 |
+
<g key={a.actor_id}>
|
| 169 |
+
{isPed ? (
|
| 170 |
+
<circle cx={sx(s.x)} cy={sy(s.y)} r={6} fill={color} />
|
| 171 |
+
) : (
|
| 172 |
+
<rect
|
| 173 |
+
x={sx(s.x) - 11}
|
| 174 |
+
y={sy(s.y) - 6}
|
| 175 |
+
width={22}
|
| 176 |
+
height={12}
|
| 177 |
+
fill={color}
|
| 178 |
+
rx={2}
|
| 179 |
+
transform={`rotate(${(-s.yaw * 180) / Math.PI} ${sx(s.x)} ${sy(s.y)})`}
|
| 180 |
+
/>
|
| 181 |
+
)}
|
| 182 |
+
<text x={sx(s.x) + 9} y={sy(s.y) - 8} fontSize={10} fill={color}>
|
| 183 |
+
{a.actor_id}
|
| 184 |
+
</text>
|
| 185 |
+
</g>
|
| 186 |
+
);
|
| 187 |
+
})}
|
| 188 |
+
{/* ego markers at t */}
|
| 189 |
+
{obsNow ? (
|
| 190 |
+
<rect x={sx(obsNow.x) - 13} y={sy(obsNow.y) - 7} width={26} height={14} fill="#c62828" rx={3} stroke="#fff" strokeWidth={0.8} />
|
| 191 |
+
) : null}
|
| 192 |
+
{corrNow ? (
|
| 193 |
+
<rect x={sx(corrNow.x) - 13} y={sy(corrNow.y) - 7} width={26} height={14} fill="#2e7d32" rx={3} stroke="#fff" strokeWidth={0.8} opacity={0.9} />
|
| 194 |
+
) : null}
|
| 195 |
+
</svg>
|
| 196 |
+
<Box sx={{ px: 1, display: 'flex', alignItems: 'center', gap: 2 }}>
|
| 197 |
+
<Typography variant="caption" sx={{ color: '#8a949e', minWidth: 70 }}>
|
| 198 |
+
t = {t.toFixed(1)} s
|
| 199 |
+
</Typography>
|
| 200 |
+
<Slider size="small" min={0} max={tMax} step={0.1} value={t} onChange={(_, v) => setT(v as number)} />
|
| 201 |
+
</Box>
|
| 202 |
+
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap', mt: 0.5 }}>
|
| 203 |
+
{(
|
| 204 |
+
[
|
| 205 |
+
['observed', detail.observed_safety, obsNow],
|
| 206 |
+
['corrected', detail.corrected_safety, corrNow],
|
| 207 |
+
] as const
|
| 208 |
+
).map(([label, sa, now]) => (
|
| 209 |
+
<Typography key={label} variant="caption" sx={{ fontFamily: 'monospace', color: label === 'observed' ? '#ef9a9a' : '#a5d6a7' }}>
|
| 210 |
+
{label}: v={now ? now.v.toFixed(1) : '—'} m/s · minTTC=
|
| 211 |
+
{sa.min_ttc_s != null ? `${sa.min_ttc_s.toFixed(2)}s` : '—'} · clearance=
|
| 212 |
+
{sa.min_clearance_m != null ? `${sa.min_clearance_m.toFixed(2)}m` : '—'} · PET=
|
| 213 |
+
{sa.pet_s != null ? `${sa.pet_s.toFixed(2)}s` : '—'} · brake=
|
| 214 |
+
{sa.max_braking_mps2.toFixed(1)} m/s² {sa.collision ? '· COLLISION' : ''}
|
| 215 |
+
</Typography>
|
| 216 |
+
))}
|
| 217 |
+
</Box>
|
| 218 |
+
<Typography variant="caption" sx={{ color: '#6b7681', display: 'block', mt: 0.5 }}>
|
| 219 |
+
divergence: {detail.planner_evaluation.max_position_divergence_m.toFixed(2)} m position ·{' '}
|
| 220 |
+
{detail.planner_evaluation.max_speed_divergence_mps.toFixed(2)} m/s speed · corrected layers:{' '}
|
| 221 |
+
{detail.corrected_layers.join(', ')} — {detail.observed_safety.surrogate_caveat}
|
| 222 |
+
</Typography>
|
| 223 |
+
</Box>
|
| 224 |
+
);
|
| 225 |
+
}
|
src/components/rotr/RotrQueryBuilder.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Structured-query builder for taxonomy mining: one dropdown chip per axis
|
| 3 |
+
* plus a free-text field that the backend parses DETERMINISTICALLY (keyword
|
| 4 |
+
* map, no LLM) into the same filterable query object.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { useState } from 'react';
|
| 8 |
+
import Box from '@mui/material/Box';
|
| 9 |
+
import Button from '@mui/material/Button';
|
| 10 |
+
import Chip from '@mui/material/Chip';
|
| 11 |
+
import FormControl from '@mui/material/FormControl';
|
| 12 |
+
import InputLabel from '@mui/material/InputLabel';
|
| 13 |
+
import MenuItem from '@mui/material/MenuItem';
|
| 14 |
+
import Select from '@mui/material/Select';
|
| 15 |
+
import TextField from '@mui/material/TextField';
|
| 16 |
+
import Typography from '@mui/material/Typography';
|
| 17 |
+
|
| 18 |
+
const AXIS_OPTIONS: Record<string, string[]> = {
|
| 19 |
+
actor: ['pedestrian', 'cyclist', 'vehicle', 'none'],
|
| 20 |
+
vulnerability: ['VRU', 'NON_VRU'],
|
| 21 |
+
legality: ['YIELD', 'RESTRICTED_PATH', 'LANE_ASSOCIATION', 'SIGNAL', 'MERGE', 'STOP'],
|
| 22 |
+
interaction: ['CROSSING', 'MERGING', 'FOLLOWING', 'OTHER', 'NONE'],
|
| 23 |
+
behavior: [
|
| 24 |
+
'proceed_without_yield',
|
| 25 |
+
'enter_restricted_path',
|
| 26 |
+
'unpermitted_maneuver',
|
| 27 |
+
'run_red_signal',
|
| 28 |
+
'insufficient_gap_merge',
|
| 29 |
+
'rolling_stop',
|
| 30 |
+
],
|
| 31 |
+
road_geometry: ['uncontrolled', 'controlled', 'none'],
|
| 32 |
+
traffic_control: ['none', 'signal', 'stop_sign'],
|
| 33 |
+
visibility: ['clear', 'low'],
|
| 34 |
+
lighting: ['day', 'dusk', 'night'],
|
| 35 |
+
weather: ['clear', 'rain'],
|
| 36 |
+
consequence_class: [
|
| 37 |
+
'NO_MATERIAL_CONSEQUENCE',
|
| 38 |
+
'DEGRADED_COMFORT',
|
| 39 |
+
'PLANNER_INTERVENTION',
|
| 40 |
+
'SAFETY_CRITICAL',
|
| 41 |
+
],
|
| 42 |
+
primary_layer: [
|
| 43 |
+
'perception',
|
| 44 |
+
'prediction',
|
| 45 |
+
'planning',
|
| 46 |
+
'localization',
|
| 47 |
+
'map',
|
| 48 |
+
'control',
|
| 49 |
+
'policy_rule',
|
| 50 |
+
'data_label',
|
| 51 |
+
],
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
interface Props {
|
| 55 |
+
onRun: (text: string | undefined, filters: Record<string, string>) => void;
|
| 56 |
+
busy?: boolean;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export default function RotrQueryBuilder({ onRun, busy }: Props) {
|
| 60 |
+
const [text, setText] = useState(
|
| 61 |
+
'failed to yield to pedestrian at uncontrolled intersection during low visibility'
|
| 62 |
+
);
|
| 63 |
+
const [filters, setFilters] = useState<Record<string, string>>({});
|
| 64 |
+
|
| 65 |
+
const setAxis = (axis: string, value: string) => {
|
| 66 |
+
setFilters((f) => {
|
| 67 |
+
const next = { ...f };
|
| 68 |
+
if (value) next[axis] = value;
|
| 69 |
+
else delete next[axis];
|
| 70 |
+
return next;
|
| 71 |
+
});
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
return (
|
| 75 |
+
<Box>
|
| 76 |
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
| 77 |
+
<TextField
|
| 78 |
+
size="small"
|
| 79 |
+
fullWidth
|
| 80 |
+
label="natural-language query (parsed by a deterministic keyword map — no LLM)"
|
| 81 |
+
value={text}
|
| 82 |
+
onChange={(e) => setText(e.target.value)}
|
| 83 |
+
/>
|
| 84 |
+
<Button variant="contained" disabled={busy} onClick={() => onRun(text || undefined, filters)}>
|
| 85 |
+
Run query
|
| 86 |
+
</Button>
|
| 87 |
+
</Box>
|
| 88 |
+
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
| 89 |
+
{Object.entries(AXIS_OPTIONS).map(([axis, options]) => (
|
| 90 |
+
<FormControl key={axis} size="small" sx={{ minWidth: 148 }}>
|
| 91 |
+
<InputLabel sx={{ fontSize: 12 }}>{axis}</InputLabel>
|
| 92 |
+
<Select
|
| 93 |
+
label={axis}
|
| 94 |
+
value={filters[axis] ?? ''}
|
| 95 |
+
onChange={(e) => setAxis(axis, e.target.value)}
|
| 96 |
+
sx={{ fontSize: 12 }}
|
| 97 |
+
>
|
| 98 |
+
<MenuItem value="">
|
| 99 |
+
<em>any</em>
|
| 100 |
+
</MenuItem>
|
| 101 |
+
{options.map((o) => (
|
| 102 |
+
<MenuItem key={o} value={o} sx={{ fontSize: 12 }}>
|
| 103 |
+
{o}
|
| 104 |
+
</MenuItem>
|
| 105 |
+
))}
|
| 106 |
+
</Select>
|
| 107 |
+
</FormControl>
|
| 108 |
+
))}
|
| 109 |
+
</Box>
|
| 110 |
+
{Object.keys(filters).length ? (
|
| 111 |
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 1, alignItems: 'center' }}>
|
| 112 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 113 |
+
active chips:
|
| 114 |
+
</Typography>
|
| 115 |
+
{Object.entries(filters).map(([axis, value]) => (
|
| 116 |
+
<Chip
|
| 117 |
+
key={axis}
|
| 118 |
+
size="small"
|
| 119 |
+
label={`${axis}=${value}`}
|
| 120 |
+
onDelete={() => setAxis(axis, '')}
|
| 121 |
+
sx={{ height: 20, fontSize: 10, fontFamily: 'monospace' }}
|
| 122 |
+
/>
|
| 123 |
+
))}
|
| 124 |
+
</Box>
|
| 125 |
+
) : null}
|
| 126 |
+
</Box>
|
| 127 |
+
);
|
| 128 |
+
}
|
src/content/glossary.ts
CHANGED
|
@@ -40,7 +40,8 @@ export type GlossaryCategory =
|
|
| 40 |
| 'Closed-loop evaluation'
|
| 41 |
| 'Launch readiness'
|
| 42 |
| 'Release governance'
|
| 43 |
-
| 'Production hardening'
|
|
|
|
| 44 |
|
| 45 |
export const GLOSSARY: Record<string, GlossaryEntry> = {
|
| 46 |
// ------------------------------------------------------------ detection metrics
|
|
@@ -964,59 +965,6 @@ export const GLOSSARY: Record<string, GlossaryEntry> = {
|
|
| 964 |
short: 'Every agent tool call — including denials — is appended to a per-analysis audit log with args, result hash, and timestamp.',
|
| 965 |
detail: 'Tools are read-only by default; the single write tool (CreateEvaluationCase) requires an explicit policy authorization flag. The log reader is path-allowlisted to runs/ and the fixtures directory. The audit trail makes each scorecard reproducible and reviewable.',
|
| 966 |
},
|
| 967 |
-
|
| 968 |
-
// ------------------------------------------------------------ launch readiness (agentic triage)
|
| 969 |
-
policy_outcome: {
|
| 970 |
-
term: 'Policy outcome (four-way)',
|
| 971 |
-
category: 'Launch readiness',
|
| 972 |
-
short: 'AUTOMATIC_STOP_SHIP / LAUNCH_REVIEW_REQUIRED / CONTINUE_INVESTIGATION / NO_LAUNCH_IMPACT — decided by the deterministic policy engine, never by an agent.',
|
| 973 |
-
detail: 'The versioned stop-ship policy evaluates explicit conditions over severity × exposure × frequency × confidence × novelty × downstream consequence. Automatic stop-ship fires only on pre-authorized conditions; anything with missing ground truth, incomplete lineage, failed fusion verification or conflicting agents resolves to INDETERMINATE (a fail-safe escalation), never a pass.',
|
| 974 |
-
caveat: 'All numeric thresholds in the shipped policy are example placeholders (flagged placeholder_values: true); a real deployment must set organization-approved values through the versioned policy API.',
|
| 975 |
-
},
|
| 976 |
-
severity_taxonomy: {
|
| 977 |
-
term: 'Severity taxonomy (S0–S5)',
|
| 978 |
-
category: 'Launch readiness',
|
| 979 |
-
short: 'S0 cosmetic → S5 collision-in-replay, assigned by deterministic criteria over class criticality, containment and observed behavioral impact.',
|
| 980 |
-
detail: 'S4 requires OBSERVED unsafe downstream behavior in replay evidence — a hypothesized consequence cannot reach S4. S3 is a safety-critical misclassification whose downstream effect is not shown to be contained.',
|
| 981 |
-
},
|
| 982 |
-
evidence_status_tag: {
|
| 983 |
-
term: 'Evidence status (OBSERVED / DERIVED / HYPOTHESIS / UNAVAILABLE)',
|
| 984 |
-
category: 'Launch readiness',
|
| 985 |
-
short: 'Every node in the failure evidence graph carries an explicit epistemic status; missing evidence is shown as UNAVAILABLE rather than guessed.',
|
| 986 |
-
detail: 'OBSERVED = measured from campaign data or replay; DERIVED = computed deterministically from observed data; HYPOTHESIS = produced by an advisory agent and requires confirmation; UNAVAILABLE = the evidence does not exist yet. Agents cannot upgrade their own hypotheses.',
|
| 987 |
-
},
|
| 988 |
-
option_c_reduced_odd: {
|
| 989 |
-
term: 'Option C — reduced ODD',
|
| 990 |
-
category: 'Launch readiness',
|
| 991 |
-
short: 'Ship while excluding the concentrated failure region from the operational design domain — allowed only when the region is reliably detectable at runtime.',
|
| 992 |
-
detail: 'Feasibility checks use the ODD taxonomy: the concentrating dimension must be an instrumented ODD dimension, the runtime detector recall must meet MIN_REQUIRED_SAFETY_RECALL, the post-exclusion residual rate must meet MAX_ACCEPTABLE_FAILURE_PROBABILITY, evidence confidence must reach the configured minimum, and the exclusion must not exceed MAX_ALLOWED_ODD_EXCLUSION.',
|
| 993 |
-
caveat: 'A geographic bucket is not an instrumented ODD dimension, so geo-concentration alone can never justify Option C.',
|
| 994 |
-
},
|
| 995 |
-
hard_safety_constraint: {
|
| 996 |
-
term: 'Safety as a hard constraint',
|
| 997 |
-
category: 'Launch readiness',
|
| 998 |
-
short: 'Expected loss is computed for every option, but options whose residual failure rate violates the policy limit are excluded from selection no matter how cheap they are.',
|
| 999 |
-
detail: 'Business optimization happens only inside the safe feasible region: the option matrix skips a matched row whose option is safety-infeasible, and if no feasible row remains the engine fails safe to human safety review. Tested explicitly: a cheaper unsafe option is never selected.',
|
| 1000 |
-
},
|
| 1001 |
-
contamination_guard: {
|
| 1002 |
-
term: 'Contamination guard',
|
| 1003 |
-
category: 'Launch readiness',
|
| 1004 |
-
short: 'Members of flywheel evaluation suites are never training-eligible without an explicit recorded override (who + why).',
|
| 1005 |
-
detail: 'Prevents the learning flywheel from leaking its own evaluation examples into training data, which would silently invalidate future evaluations. Promotion without an actor and reason raises a leakage error (reusing the raremine lineage guard); overrides are recorded on the suite and in the audit trail.',
|
| 1006 |
-
},
|
| 1007 |
-
small_sample_instability: {
|
| 1008 |
-
term: 'Small-sample instability flag',
|
| 1009 |
-
category: 'Launch readiness',
|
| 1010 |
-
short: 'Raised when event counts are too small for the rate estimate to be stable (e.g. 25 events on a 240k denominator), capping evidence confidence.',
|
| 1011 |
-
detail: 'A ~0.01% rate estimated from a handful of events has a wide Wilson interval and an unstable relative risk. The statistical agent flags this, the evidence-quality level drops to LIKELY or below, and automatic stop-ship (which requires CONFIRMED) cannot fire on it — the case routes to human review instead.',
|
| 1012 |
-
},
|
| 1013 |
-
audit_hash_chain: {
|
| 1014 |
-
term: 'Hash-chained audit log',
|
| 1015 |
-
category: 'Launch readiness',
|
| 1016 |
-
short: 'Every stage transition, agent output, policy evaluation and human decision is appended to a per-failure JSONL log where each record hashes its predecessor.',
|
| 1017 |
-
detail: 'Records carry a monotonically increasing sequence number and a prev_hash → hash chain; any edit, deletion or reordering breaks recomputation and is reported as a broken chain. The log is append-only by construction.',
|
| 1018 |
-
},
|
| 1019 |
-
|
| 1020 |
data_label: {
|
| 1021 |
term: 'Data provenance label',
|
| 1022 |
category: 'Closed-loop evaluation',
|
|
@@ -1070,7 +1018,59 @@ export const GLOSSARY: Record<string, GlossaryEntry> = {
|
|
| 1070 |
detail: 'Cache keys hash (scenario, sensor_version, preprocessing_version, backbone_version, feature_schema_version); any version bump changes the key, so stale cross-version reuse is structurally impossible. Savings are measured, not estimated.',
|
| 1071 |
},
|
| 1072 |
|
| 1073 |
-
// ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
release_decision: {
|
| 1075 |
term: 'Release decision (GO / REVIEW / NO-GO)',
|
| 1076 |
category: 'Release governance',
|
|
@@ -1108,8 +1108,6 @@ export const GLOSSARY: Record<string, GlossaryEntry> = {
|
|
| 1108 |
short: 'The separate recorded action (approver + rationale) that authorizes deployment after a GO decision.',
|
| 1109 |
detail: 'Only GO decisions can be approved; REVIEW and NO-GO must be re-evaluated after their conditions are resolved. The approval is a first-class registry entity and lands in the append-only audit trail — the gate recommends, humans authorize.',
|
| 1110 |
},
|
| 1111 |
-
|
| 1112 |
-
// ------------------------------------------------------------ Production hardening
|
| 1113 |
hardening_finding: {
|
| 1114 |
term: 'Hardening finding',
|
| 1115 |
category: 'Production hardening',
|
|
@@ -1129,6 +1127,45 @@ export const GLOSSARY: Record<string, GlossaryEntry> = {
|
|
| 1129 |
detail: 'Defined in sensorflow/hardening/contracts.py and mandatory on every hardening contract. Nothing lower on the ladder may silently substitute for a level above it — e.g. a quality gate must refuse AUTO_LABEL "ground truth". Adapters map existing records (labeleval GT types, pipeline proposals) onto the ladder.',
|
| 1130 |
caveat: 'Existing packages are not yet rewritten to these contracts; adapters bridge until their next breaking release.',
|
| 1131 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1132 |
};
|
| 1133 |
|
| 1134 |
export type GlossaryKey = keyof typeof GLOSSARY;
|
|
@@ -1211,4 +1248,5 @@ export const GLOSSARY_CATEGORIES: GlossaryCategory[] = [
|
|
| 1211 |
'Launch readiness',
|
| 1212 |
'Release governance',
|
| 1213 |
'Production hardening',
|
|
|
|
| 1214 |
];
|
|
|
|
| 40 |
| 'Closed-loop evaluation'
|
| 41 |
| 'Launch readiness'
|
| 42 |
| 'Release governance'
|
| 43 |
+
| 'Production hardening'
|
| 44 |
+
| 'ROTR (right-of-the-road)';
|
| 45 |
|
| 46 |
export const GLOSSARY: Record<string, GlossaryEntry> = {
|
| 47 |
// ------------------------------------------------------------ detection metrics
|
|
|
|
| 965 |
short: 'Every agent tool call — including denials — is appended to a per-analysis audit log with args, result hash, and timestamp.',
|
| 966 |
detail: 'Tools are read-only by default; the single write tool (CreateEvaluationCase) requires an explicit policy authorization flag. The log reader is path-allowlisted to runs/ and the fixtures directory. The audit trail makes each scorecard reproducible and reviewable.',
|
| 967 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
data_label: {
|
| 969 |
term: 'Data provenance label',
|
| 970 |
category: 'Closed-loop evaluation',
|
|
|
|
| 1018 |
detail: 'Cache keys hash (scenario, sensor_version, preprocessing_version, backbone_version, feature_schema_version); any version bump changes the key, so stale cross-version reuse is structurally impossible. Savings are measured, not estimated.',
|
| 1019 |
},
|
| 1020 |
|
| 1021 |
+
// ------------------------------------------------------------ launch readiness (agentic triage)
|
| 1022 |
+
policy_outcome: {
|
| 1023 |
+
term: 'Policy outcome (four-way)',
|
| 1024 |
+
category: 'Launch readiness',
|
| 1025 |
+
short: 'AUTOMATIC_STOP_SHIP / LAUNCH_REVIEW_REQUIRED / CONTINUE_INVESTIGATION / NO_LAUNCH_IMPACT — decided by the deterministic policy engine, never by an agent.',
|
| 1026 |
+
detail: 'The versioned stop-ship policy evaluates explicit conditions over severity × exposure × frequency × confidence × novelty × downstream consequence. Automatic stop-ship fires only on pre-authorized conditions; anything with missing ground truth, incomplete lineage, failed fusion verification or conflicting agents resolves to INDETERMINATE (a fail-safe escalation), never a pass.',
|
| 1027 |
+
caveat: 'All numeric thresholds in the shipped policy are example placeholders (flagged placeholder_values: true); a real deployment must set organization-approved values through the versioned policy API.',
|
| 1028 |
+
},
|
| 1029 |
+
severity_taxonomy: {
|
| 1030 |
+
term: 'Severity taxonomy (S0–S5)',
|
| 1031 |
+
category: 'Launch readiness',
|
| 1032 |
+
short: 'S0 cosmetic → S5 collision-in-replay, assigned by deterministic criteria over class criticality, containment and observed behavioral impact.',
|
| 1033 |
+
detail: 'S4 requires OBSERVED unsafe downstream behavior in replay evidence — a hypothesized consequence cannot reach S4. S3 is a safety-critical misclassification whose downstream effect is not shown to be contained.',
|
| 1034 |
+
},
|
| 1035 |
+
evidence_status_tag: {
|
| 1036 |
+
term: 'Evidence status (OBSERVED / DERIVED / HYPOTHESIS / UNAVAILABLE)',
|
| 1037 |
+
category: 'Launch readiness',
|
| 1038 |
+
short: 'Every node in the failure evidence graph carries an explicit epistemic status; missing evidence is shown as UNAVAILABLE rather than guessed.',
|
| 1039 |
+
detail: 'OBSERVED = measured from campaign data or replay; DERIVED = computed deterministically from observed data; HYPOTHESIS = produced by an advisory agent and requires confirmation; UNAVAILABLE = the evidence does not exist yet. Agents cannot upgrade their own hypotheses.',
|
| 1040 |
+
},
|
| 1041 |
+
option_c_reduced_odd: {
|
| 1042 |
+
term: 'Option C — reduced ODD',
|
| 1043 |
+
category: 'Launch readiness',
|
| 1044 |
+
short: 'Ship while excluding the concentrated failure region from the operational design domain — allowed only when the region is reliably detectable at runtime.',
|
| 1045 |
+
detail: 'Feasibility checks use the ODD taxonomy: the concentrating dimension must be an instrumented ODD dimension, the runtime detector recall must meet MIN_REQUIRED_SAFETY_RECALL, the post-exclusion residual rate must meet MAX_ACCEPTABLE_FAILURE_PROBABILITY, evidence confidence must reach the configured minimum, and the exclusion must not exceed MAX_ALLOWED_ODD_EXCLUSION.',
|
| 1046 |
+
caveat: 'A geographic bucket is not an instrumented ODD dimension, so geo-concentration alone can never justify Option C.',
|
| 1047 |
+
},
|
| 1048 |
+
hard_safety_constraint: {
|
| 1049 |
+
term: 'Safety as a hard constraint',
|
| 1050 |
+
category: 'Launch readiness',
|
| 1051 |
+
short: 'Expected loss is computed for every option, but options whose residual failure rate violates the policy limit are excluded from selection no matter how cheap they are.',
|
| 1052 |
+
detail: 'Business optimization happens only inside the safe feasible region: the option matrix skips a matched row whose option is safety-infeasible, and if no feasible row remains the engine fails safe to human safety review. Tested explicitly: a cheaper unsafe option is never selected.',
|
| 1053 |
+
},
|
| 1054 |
+
contamination_guard: {
|
| 1055 |
+
term: 'Contamination guard',
|
| 1056 |
+
category: 'Launch readiness',
|
| 1057 |
+
short: 'Members of flywheel evaluation suites are never training-eligible without an explicit recorded override (who + why).',
|
| 1058 |
+
detail: 'Prevents the learning flywheel from leaking its own evaluation examples into training data, which would silently invalidate future evaluations. Promotion without an actor and reason raises a leakage error (reusing the raremine lineage guard); overrides are recorded on the suite and in the audit trail.',
|
| 1059 |
+
},
|
| 1060 |
+
small_sample_instability: {
|
| 1061 |
+
term: 'Small-sample instability flag',
|
| 1062 |
+
category: 'Launch readiness',
|
| 1063 |
+
short: 'Raised when event counts are too small for the rate estimate to be stable (e.g. 25 events on a 240k denominator), capping evidence confidence.',
|
| 1064 |
+
detail: 'A ~0.01% rate estimated from a handful of events has a wide Wilson interval and an unstable relative risk. The statistical agent flags this, the evidence-quality level drops to LIKELY or below, and automatic stop-ship (which requires CONFIRMED) cannot fire on it — the case routes to human review instead.',
|
| 1065 |
+
},
|
| 1066 |
+
audit_hash_chain: {
|
| 1067 |
+
term: 'Hash-chained audit log',
|
| 1068 |
+
category: 'Launch readiness',
|
| 1069 |
+
short: 'Every stage transition, agent output, policy evaluation and human decision is appended to a per-failure JSONL log where each record hashes its predecessor.',
|
| 1070 |
+
detail: 'Records carry a monotonically increasing sequence number and a prev_hash → hash chain; any edit, deletion or reordering breaks recomputation and is reported as a broken chain. The log is append-only by construction.',
|
| 1071 |
+
},
|
| 1072 |
+
|
| 1073 |
+
// ------------------------------------------------------------ release governance (studio2)
|
| 1074 |
release_decision: {
|
| 1075 |
term: 'Release decision (GO / REVIEW / NO-GO)',
|
| 1076 |
category: 'Release governance',
|
|
|
|
| 1108 |
short: 'The separate recorded action (approver + rationale) that authorizes deployment after a GO decision.',
|
| 1109 |
detail: 'Only GO decisions can be approved; REVIEW and NO-GO must be re-evaluated after their conditions are resolved. The approval is a first-class registry entity and lands in the append-only audit trail — the gate recommends, humans authorize.',
|
| 1110 |
},
|
|
|
|
|
|
|
| 1111 |
hardening_finding: {
|
| 1112 |
term: 'Hardening finding',
|
| 1113 |
category: 'Production hardening',
|
|
|
|
| 1127 |
detail: 'Defined in sensorflow/hardening/contracts.py and mandatory on every hardening contract. Nothing lower on the ladder may silently substitute for a level above it — e.g. a quality gate must refuse AUTO_LABEL "ground truth". Adapters map existing records (labeleval GT types, pipeline proposals) onto the ladder.',
|
| 1128 |
caveat: 'Existing packages are not yet rewritten to these contracts; adapters bridge until their next breaking release.',
|
| 1129 |
},
|
| 1130 |
+
rotr_violation: {
|
| 1131 |
+
term: 'ROTR violation',
|
| 1132 |
+
category: 'ROTR (right-of-the-road)',
|
| 1133 |
+
short: 'A deterministic rule-engine verdict that the executed trajectory broke the expected-behavior envelope (yielding, legal path, right-of-way, stopping).',
|
| 1134 |
+
detail: 'Computed as a function of five evidence structures: ego state, road context, actor states, observed behavior and expected behavior. Each record carries a versioned rule id, structured evidence fields and provenance. Detection runs against the ACTUAL (ground-truth) world; plan compliance runs separately against the stack\u2019s believed world.',
|
| 1135 |
+
caveat: 'A violation verdict says nothing about cause or consequence — those are separate analyses (attribution, counterfactual replay).',
|
| 1136 |
+
},
|
| 1137 |
+
rotr_attribution: {
|
| 1138 |
+
term: 'Causal-layer attribution',
|
| 1139 |
+
category: 'ROTR (right-of-the-road)',
|
| 1140 |
+
short: 'Per-violation, per-layer evidence tests: each of perception / prediction / planning / localization / map / control / data-label / policy-rule is independently SUPPORTED, RULED_OUT or UNKNOWN.',
|
| 1141 |
+
detail: 'The load-bearing invariant: a violation is NEVER auto-attributed to perception. Each layer needs its own positive evidence (e.g. GT-vs-detected diff for perception, correct-perception-but-noncompliant-plan for planning). The primary layer is the highest-confidence SUPPORTED entry; with none, the row stays unattributed and goes to HITL.',
|
| 1142 |
+
},
|
| 1143 |
+
rotr_consequence: {
|
| 1144 |
+
term: 'Behavioral consequence class',
|
| 1145 |
+
category: 'ROTR (right-of-the-road)',
|
| 1146 |
+
short: 'What the violation actually cost, measured by counterfactual replay: NO_MATERIAL_CONSEQUENCE, DEGRADED_COMFORT, PLANNER_INTERVENTION or SAFETY_CRITICAL.',
|
| 1147 |
+
detail: 'The attributed layer\u2019s inputs are corrected and the scenario is re-planned (nextgen closed-loop when importable, else a self-contained IDM-style planner). Observed vs corrected trajectories are compared on TTC, PET, clearance, stopping distance, braking intensity and lateral deviation.',
|
| 1148 |
+
caveat: 'Surrogate-safety measures (TTC/PET/clearance) indicate exposure, not collision certainty — they alone do not establish collision risk.',
|
| 1149 |
+
},
|
| 1150 |
+
sc_rotr_recall: {
|
| 1151 |
+
term: 'SC-ROTR recall',
|
| 1152 |
+
category: 'ROTR (right-of-the-road)',
|
| 1153 |
+
short: 'Safety-critical-weighted violation recall: misses on dangerous violations cost more than misses on cosmetic ones.',
|
| 1154 |
+
detail: 'Weights are exposure-derived per stratum — the fraction of that stratum\u2019s violations whose counterfactual consequence was safety-critical, normalized so weights average 1 — with the calibration method and version recorded next to the number. Not arbitrary class multipliers.',
|
| 1155 |
+
caveat: 'On synthetic banks the weights are calibrated on the bank itself; a production system would calibrate on fleet exposure data.',
|
| 1156 |
+
},
|
| 1157 |
+
rotr_stopship: {
|
| 1158 |
+
term: 'ROTR stop-ship trigger',
|
| 1159 |
+
category: 'ROTR (right-of-the-road)',
|
| 1160 |
+
short: 'Deterministic NO_GO on the strict conjunction: VRU involved AND perception missed the actor AND counterfactual consequence is safety-critical.',
|
| 1161 |
+
detail: 'Policy-versioned and explicitly not LLM-driven. Partial matches never fire. NO_GO events are forwarded to the agentic policy engine as advisory input when importable; the gate outcome is recorded locally either way.',
|
| 1162 |
+
},
|
| 1163 |
+
rotr_six_axis: {
|
| 1164 |
+
term: 'Six-axis ROTR taxonomy',
|
| 1165 |
+
category: 'ROTR (right-of-the-road)',
|
| 1166 |
+
short: 'Actor / Vulnerability / Legality / Environment / Interaction / Behavior — plus road-geometry, traffic-control and visibility extensions — for structured violation mining.',
|
| 1167 |
+
detail: 'Text like "failed to yield to pedestrian at uncontrolled intersection during low visibility" parses through a deterministic keyword map into the same filterable query object as the dropdown chips. Violations cluster into recurring patterns by structured signature.',
|
| 1168 |
+
},
|
| 1169 |
};
|
| 1170 |
|
| 1171 |
export type GlossaryKey = keyof typeof GLOSSARY;
|
|
|
|
| 1248 |
'Launch readiness',
|
| 1249 |
'Release governance',
|
| 1250 |
'Production hardening',
|
| 1251 |
+
'ROTR (right-of-the-road)',
|
| 1252 |
];
|
src/content/pageHelp.ts
CHANGED
|
@@ -227,42 +227,6 @@ export const PAGE_HELP: Record<PageId, PageHelpEntry> = {
|
|
| 227 |
dataFlow:
|
| 228 |
'Scenes and engines come read-only from the bevfusion package; sequential verdicts use seqeval\u2019s anytime-valid tests when importable (local paired-t fallback otherwise). Runs persist under runs/vitis/. Generated variants are evaluation-set supplements tagged evaluation-only (never training-eligible) and are offered to the raremine candidate flow when that package is available.',
|
| 229 |
},
|
| 230 |
-
'closed-loop-lab': {
|
| 231 |
-
subtitle:
|
| 232 |
-
'Next-generation evaluation: counterfactual scenario generation with a validity gate, closed-loop behavioral metrics with causal replay, safety-informed metrics, and a priority-scheduled launch gauntlet with compute deduplication.',
|
| 233 |
-
purpose:
|
| 234 |
-
'Open-loop metrics (mAP/recall) cannot tell you whether a perception error would have changed vehicle behavior, and aggregate recall can improve while safety-critical recall degrades. This lab generates labeled counterfactual scenarios, runs the full perception→planner→vehicle loop, and answers the causal question directly: replay the same scenario with corrected perception and diff the outcomes.',
|
| 235 |
-
reading:
|
| 236 |
-
'Every datum carries a REAL / REPLAYED / SIMULATED / GENERATED / COUNTERFACTUAL provenance label. Counterfactuals show fidelity / validity / realism scores from the gate; low-fidelity scenarios are weight-capped so they cannot dominate launch decisions. The causal replay banner is the key verdict: METRIC_ONLY (visible in metrics, no behavioral change) vs BEHAVIORALLY_CONSEQUENTIAL (the error changes the safety outcome). The Gauntlet tab shows priority strata (safety-critical first), anytime-valid early-stop events, and measured compute savings.',
|
| 237 |
-
actions:
|
| 238 |
-
'Build transformation recipes and generate counterfactuals; run the validity gate; run closed-loop replays and causal comparisons with planted perception faults; run the safety-metric divergence demo; launch a 100k-unit gauntlet against a candidate profile and inspect the launch recommendation; read the architecture decision docs.',
|
| 239 |
-
dataFlow:
|
| 240 |
-
'Counterfactuals are transformations of bevfusion scenes; safety math reuses the SSAM extensions; sequential early stopping delegates to seqeval; distribution similarity delegates to the mega-eval statistics. Artifacts persist under runs/nextgen/. A run with incomplete lineage is marked INVALID for launch purposes.',
|
| 241 |
-
},
|
| 242 |
-
studio2: {
|
| 243 |
-
subtitle:
|
| 244 |
-
'The Studio 2.0 control plane: versioned entities for the whole system, one deterministic release decision composed from the real engines (safety gates + seqeval + megaeval shift + agentic/nextgen when available), hardware-stratified gating, and the unified observability funnel.',
|
| 245 |
-
purpose:
|
| 246 |
-
'The engines exist; what was missing is the layer that composes them. This page answers "can this candidate ship?" with a GO / REVIEW / NO-GO decision backed by a full evidence tuple, keeps humans (not gates) as the deployment authority, and makes cross-package entities (models, datasets with roles, policies, runs) first-class and browsable.',
|
| 247 |
-
reading:
|
| 248 |
-
'Release Board: the decision banner shows status, confidence and evidence completeness; blocking conditions, named degraded inputs and unresolved questions below it; the evidence tuple lists exactly which subsystem artifacts the decision was computed from. A GO decision still shows "deployment not authorized" until the separate human approval is recorded. Control Plane: role badges (REGRESSION/LAUNCH/TEST are PROTECTED by the contamination guard) and REPRODUCIBLE / NON-REPRODUCIBLE badges (a run missing any tuple component is non-reproducible, with the missing parts listed). Hardware Gates: each row is a region × platform × sensor-generation combination; a failing CRITICAL row blocks even when the global aggregate passes; amber rows lack sufficient evidence and never count as passing. Funnel: stage bars are log-scaled; absent sources show UNAVAILABLE with the reason — numbers are never fabricated.',
|
| 249 |
-
actions:
|
| 250 |
-
'Evaluate a candidate from live sources, run the end-to-end closed-loop demo (bevfusion scenario → seqeval verdict → nextgen replay → safety gates → release decision → flywheel registration), browse and auto-ingest registry entities, transition dataset roles (guarded transitions demand an audited override), recompute the hardware matrix, and record human deployment approvals.',
|
| 251 |
-
dataFlow:
|
| 252 |
-
'Reads the persisted stores of megaeval, seqeval, safety, agentic, nextgen, bevfusion and vitis (best-effort, availability-flagged). Writes only under runs/studio2/: registry entities, release decisions, approvals and an append-only audit trail. Failures found by the demo feed back as protected REGRESSION-role dataset entries.',
|
| 253 |
-
},
|
| 254 |
-
'production-readiness': {
|
| 255 |
-
subtitle:
|
| 256 |
-
'The hardening audit as a living page: every finding with file:line references, a readiness scorecard computed from the audit (never hand-written), the live data funnel, and the fix-now vs follow-up remediation board.',
|
| 257 |
-
purpose:
|
| 258 |
-
'An honest answer to "is this platform production ready?". The scorecard is derived from docs/hardening/audit.json and is hard-wired to report NOT PRODUCTION READY while any Critical finding is open — no one can flip the status without closing the findings.',
|
| 259 |
-
reading:
|
| 260 |
-
'The banner shows the overall verdict, finding counts by severity, and fixed-now vs deferred totals. Scorecard cards map each category to its prototype state, production requirement, and open gap IDs. The funnel bars are live counts from the labeleval store (source labeled). The findings table filters by area / severity / disposition; refs are FILE:LINE into this repository. The kanban splits work already done in this pass from documented follow-ups with effort badges (S/M/L).',
|
| 261 |
-
actions:
|
| 262 |
-
'Filter and read findings; use the refs to jump into the code. No mutations happen from this page — it is a read-only audit browser.',
|
| 263 |
-
dataFlow:
|
| 264 |
-
'All content is served by /api/hardening from docs/hardening/audit.{md,json} plus read-only funnel counts from runs/labeleval. Fixes land in code; the audit document is updated with dispositions; this page re-derives everything from it.',
|
| 265 |
-
},
|
| 266 |
ssam: {
|
| 267 |
subtitle:
|
| 268 |
'Surrogate-safety analysis of real intersections: conflicts ranked by TTC / PET / severity on an interactive map — the safety context the label platform feeds.',
|
|
@@ -442,6 +406,18 @@ export const PAGE_HELP: Record<PageId, PageHelpEntry> = {
|
|
| 442 |
dataFlow:
|
| 443 |
'Fixtures live in sensorflow/retro/fixtures; analyses and audit trails persist under runs/retro/. Metric math reuses the SSAM safety extensions; distribution stats delegate to the mega-eval engine; the safety-case corpus is indexed at startup (chromadb or deterministic fallback).',
|
| 444 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
'launch-readiness': {
|
| 446 |
subtitle:
|
| 447 |
'Agentic launch readiness & misclassification triage: a five-layer pipeline (detection → evidence → analysis → decision → flywheel) where AI agents only recommend and a deterministic, versioned policy engine plus recorded human review own every launch decision.',
|
|
@@ -454,6 +430,18 @@ export const PAGE_HELP: Record<PageId, PageHelpEntry> = {
|
|
| 454 |
dataFlow:
|
| 455 |
'Detection scans a seeded synthetic campaign (bevfusion scenes + a paired rate population). Statistics delegate to seqeval; safety replays reuse the SSAM extensions; Option C checks use the ODD taxonomy. Validated failures feed evaluation suites (contamination-guarded) under runs/agentic/, and every action lands in a hash-chained append-only audit log.',
|
| 456 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
legacy: {
|
| 458 |
subtitle:
|
| 459 |
'The original vanilla-JS studio, embedded unchanged — kept for workflows not yet migrated to the React platform.',
|
|
@@ -462,6 +450,30 @@ export const PAGE_HELP: Record<PageId, PageHelpEntry> = {
|
|
| 462 |
actions: 'Use legacy workflows; open in a new tab for full-window work.',
|
| 463 |
dataFlow: 'Talks to the same backend and datasets as the new platform.',
|
| 464 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 465 |
};
|
| 466 |
|
| 467 |
/** Short one-liner per page for the global help menu index. */
|
|
|
|
| 227 |
dataFlow:
|
| 228 |
'Scenes and engines come read-only from the bevfusion package; sequential verdicts use seqeval\u2019s anytime-valid tests when importable (local paired-t fallback otherwise). Runs persist under runs/vitis/. Generated variants are evaluation-set supplements tagged evaluation-only (never training-eligible) and are offered to the raremine candidate flow when that package is available.',
|
| 229 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
ssam: {
|
| 231 |
subtitle:
|
| 232 |
'Surrogate-safety analysis of real intersections: conflicts ranked by TTC / PET / severity on an interactive map — the safety context the label platform feeds.',
|
|
|
|
| 406 |
dataFlow:
|
| 407 |
'Fixtures live in sensorflow/retro/fixtures; analyses and audit trails persist under runs/retro/. Metric math reuses the SSAM safety extensions; distribution stats delegate to the mega-eval engine; the safety-case corpus is indexed at startup (chromadb or deterministic fallback).',
|
| 408 |
},
|
| 409 |
+
'closed-loop-lab': {
|
| 410 |
+
subtitle:
|
| 411 |
+
'Next-generation evaluation: counterfactual scenario generation with a validity gate, closed-loop behavioral metrics with causal replay, safety-informed metrics, and a priority-scheduled launch gauntlet with compute deduplication.',
|
| 412 |
+
purpose:
|
| 413 |
+
'Open-loop metrics (mAP/recall) cannot tell you whether a perception error would have changed vehicle behavior, and aggregate recall can improve while safety-critical recall degrades. This lab generates labeled counterfactual scenarios, runs the full perception→planner→vehicle loop, and answers the causal question directly: replay the same scenario with corrected perception and diff the outcomes.',
|
| 414 |
+
reading:
|
| 415 |
+
'Every datum carries a REAL / REPLAYED / SIMULATED / GENERATED / COUNTERFACTUAL provenance label. Counterfactuals show fidelity / validity / realism scores from the gate; low-fidelity scenarios are weight-capped so they cannot dominate launch decisions. The causal replay banner is the key verdict: METRIC_ONLY (visible in metrics, no behavioral change) vs BEHAVIORALLY_CONSEQUENTIAL (the error changes the safety outcome). The Gauntlet tab shows priority strata (safety-critical first), anytime-valid early-stop events, and measured compute savings.',
|
| 416 |
+
actions:
|
| 417 |
+
'Build transformation recipes and generate counterfactuals; run the validity gate; run closed-loop replays and causal comparisons with planted perception faults; run the safety-metric divergence demo; launch a 100k-unit gauntlet against a candidate profile and inspect the launch recommendation; read the architecture decision docs.',
|
| 418 |
+
dataFlow:
|
| 419 |
+
'Counterfactuals are transformations of bevfusion scenes; safety math reuses the SSAM extensions; sequential early stopping delegates to seqeval; distribution similarity delegates to the mega-eval statistics. Artifacts persist under runs/nextgen/. A run with incomplete lineage is marked INVALID for launch purposes.',
|
| 420 |
+
},
|
| 421 |
'launch-readiness': {
|
| 422 |
subtitle:
|
| 423 |
'Agentic launch readiness & misclassification triage: a five-layer pipeline (detection → evidence → analysis → decision → flywheel) where AI agents only recommend and a deterministic, versioned policy engine plus recorded human review own every launch decision.',
|
|
|
|
| 430 |
dataFlow:
|
| 431 |
'Detection scans a seeded synthetic campaign (bevfusion scenes + a paired rate population). Statistics delegate to seqeval; safety replays reuse the SSAM extensions; Option C checks use the ODD taxonomy. Validated failures feed evaluation suites (contamination-guarded) under runs/agentic/, and every action lands in a hash-chained append-only audit log.',
|
| 432 |
},
|
| 433 |
+
studio2: {
|
| 434 |
+
subtitle:
|
| 435 |
+
'The Studio 2.0 control plane: versioned entities for the whole system, one deterministic release decision composed from the real engines (safety gates + seqeval + megaeval shift + agentic/nextgen when available), hardware-stratified gating, and the unified observability funnel.',
|
| 436 |
+
purpose:
|
| 437 |
+
'The engines exist; what was missing is the layer that composes them. This page answers "can this candidate ship?" with a GO / REVIEW / NO-GO decision backed by a full evidence tuple, keeps humans (not gates) as the deployment authority, and makes cross-package entities (models, datasets with roles, policies, runs) first-class and browsable.',
|
| 438 |
+
reading:
|
| 439 |
+
'Release Board: the decision banner shows status, confidence and evidence completeness; blocking conditions, named degraded inputs and unresolved questions below it; the evidence tuple lists exactly which subsystem artifacts the decision was computed from. A GO decision still shows "deployment not authorized" until the separate human approval is recorded. Control Plane: role badges (REGRESSION/LAUNCH/TEST are PROTECTED by the contamination guard) and REPRODUCIBLE / NON-REPRODUCIBLE badges (a run missing any tuple component is non-reproducible, with the missing parts listed). Hardware Gates: each row is a region × platform × sensor-generation combination; a failing CRITICAL row blocks even when the global aggregate passes; amber rows lack sufficient evidence and never count as passing. Funnel: stage bars are log-scaled; absent sources show UNAVAILABLE with the reason — numbers are never fabricated.',
|
| 440 |
+
actions:
|
| 441 |
+
'Evaluate a candidate from live sources, run the end-to-end closed-loop demo (bevfusion scenario → seqeval verdict → nextgen replay → safety gates → release decision → flywheel registration), browse and auto-ingest registry entities, transition dataset roles (guarded transitions demand an audited override), recompute the hardware matrix, and record human deployment approvals.',
|
| 442 |
+
dataFlow:
|
| 443 |
+
'Reads the persisted stores of megaeval, seqeval, safety, agentic, nextgen, bevfusion and vitis (best-effort, availability-flagged). Writes only under runs/studio2/: registry entities, release decisions, approvals and an append-only audit trail. Failures found by the demo feed back as protected REGRESSION-role dataset entries.',
|
| 444 |
+
},
|
| 445 |
legacy: {
|
| 446 |
subtitle:
|
| 447 |
'The original vanilla-JS studio, embedded unchanged — kept for workflows not yet migrated to the React platform.',
|
|
|
|
| 450 |
actions: 'Use legacy workflows; open in a new tab for full-window work.',
|
| 451 |
dataFlow: 'Talks to the same backend and datasets as the new platform.',
|
| 452 |
},
|
| 453 |
+
'production-readiness': {
|
| 454 |
+
subtitle:
|
| 455 |
+
'The hardening audit as a living page: every finding with file:line references, a readiness scorecard computed from the audit (never hand-written), the live data funnel, and the fix-now vs follow-up remediation board.',
|
| 456 |
+
purpose:
|
| 457 |
+
'An honest answer to "is this platform production ready?". The scorecard is derived from docs/hardening/audit.json and is hard-wired to report NOT PRODUCTION READY while any Critical finding is open — no one can flip the status without closing the findings.',
|
| 458 |
+
reading:
|
| 459 |
+
'The banner shows the overall verdict, finding counts by severity, and fixed-now vs deferred totals. Scorecard cards map each category to its prototype state, production requirement, and open gap IDs. The funnel bars are live counts from the labeleval store (source labeled). The findings table filters by area / severity / disposition; refs are FILE:LINE into this repository. The kanban splits work already done in this pass from documented follow-ups with effort badges (S/M/L).',
|
| 460 |
+
actions:
|
| 461 |
+
'Filter and read findings; use the refs to jump into the code. No mutations happen from this page — it is a read-only audit browser.',
|
| 462 |
+
dataFlow:
|
| 463 |
+
'All content is served by /api/hardening from docs/hardening/audit.{md,json} plus read-only funnel counts from runs/labeleval. Fixes land in code; the audit document is updated with dispositions; this page re-derives everything from it.',
|
| 464 |
+
},
|
| 465 |
+
rotr: {
|
| 466 |
+
subtitle:
|
| 467 |
+
'Right-of-the-road violation detection, triage, evaluation & training: deterministic rule verdicts, causal-layer attribution (never auto-blamed on perception), counterfactual consequence replay, seqeval-gated regression, and a contamination-guarded flywheel.',
|
| 468 |
+
purpose:
|
| 469 |
+
'A ROTR violation ("failed to yield to a crossing pedestrian") is only actionable once three separate questions are answered: did it happen (rule engine vs ground truth), why (per-layer evidence: perception / prediction / planning / localization / map / control), and did it matter (counterfactual replay of the corrected stack). This page runs that pipeline on deterministic planted-violation scenario banks and closes the loop into a protected regression suite.',
|
| 470 |
+
reading:
|
| 471 |
+
'The gate banner is the deterministic stop-ship verdict (VRU + missed detection + safety-critical exposure ⇒ NO_GO; policy-versioned, not LLM-driven). Executive: recall / SC-recall / BCR / CFR tiles and the six-outcome regression status. Engineering: the attribution heatmap colors each violation × layer cell SUPPORTED / RULED_OUT / UNKNOWN with a ring on the primary layer — click a row to open the BEV replay of observed (red) vs corrected (green dashed) trajectories. Data: coverage of the planted bank, the HITL queue, and training candidates whose REGRESSION-role members show a PROTECTED guard chip. Infrastructure: measured wall time and seqeval sample counts; unmeasured figures say NOT MEASURED.',
|
| 472 |
+
actions:
|
| 473 |
+
'Generate a scenario bank and run detection for one of three stack profiles; compare two runs through seqeval; mine violations with the six-axis structured query (text parses through a deterministic keyword map); validate or reject HITL items (validation creates a protected regression artifact); attempt training promotion — the contamination guard blocks protected members unless a reasoned governance override is recorded.',
|
| 474 |
+
dataFlow:
|
| 475 |
+
'Scenario banks and all run artifacts persist under runs/rotr/. Regression statistics delegate to seqeval over a megaeval population; validated artifacts mirror to the studio2 registry when available; NO_GO events forward to the agentic policy engine as advisory input. Everything is reproducible from bank id + seed + version pins.',
|
| 476 |
+
},
|
| 477 |
};
|
| 478 |
|
| 479 |
/** Short one-liner per page for the global help menu index. */
|
src/context/LabelEvalContext.tsx
CHANGED
|
@@ -39,7 +39,8 @@ export type PageId =
|
|
| 39 |
| 'launch-readiness'
|
| 40 |
| 'studio2'
|
| 41 |
| 'legacy'
|
| 42 |
-
| 'production-readiness'
|
|
|
|
| 43 |
|
| 44 |
export const ALL_PAGE_IDS: PageId[] = [
|
| 45 |
'command',
|
|
@@ -80,6 +81,7 @@ export const ALL_PAGE_IDS: PageId[] = [
|
|
| 80 |
'studio2',
|
| 81 |
'legacy',
|
| 82 |
'production-readiness',
|
|
|
|
| 83 |
];
|
| 84 |
|
| 85 |
/** Maps backend evidence_link.page strings (unknown vocabulary) onto our page ids. */
|
|
|
|
| 39 |
| 'launch-readiness'
|
| 40 |
| 'studio2'
|
| 41 |
| 'legacy'
|
| 42 |
+
| 'production-readiness'
|
| 43 |
+
| 'rotr';
|
| 44 |
|
| 45 |
export const ALL_PAGE_IDS: PageId[] = [
|
| 46 |
'command',
|
|
|
|
| 81 |
'studio2',
|
| 82 |
'legacy',
|
| 83 |
'production-readiness',
|
| 84 |
+
'rotr',
|
| 85 |
];
|
| 86 |
|
| 87 |
/** Maps backend evidence_link.page strings (unknown vocabulary) onto our page ids. */
|
src/pages/rotr/RotrControlCenterPage.tsx
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ROTR Control Center — right-of-the-road violation detection, triage,
|
| 3 |
+
* evaluation & training. Four audiences, four views:
|
| 4 |
+
* Executive — headline metrics, gate state, regression status
|
| 5 |
+
* Engineering — taxonomy distributions, attribution heatmap, clusters,
|
| 6 |
+
* model comparison, consequence replay (BEV schematic)
|
| 7 |
+
* Data — coverage, rare patterns, HITL queue, training
|
| 8 |
+
* candidates with role badges + contamination guard,
|
| 9 |
+
* regression suite
|
| 10 |
+
* Infrastructure — measured throughput + delegation stats; unmeasured
|
| 11 |
+
* figures are flagged honestly, never invented
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
| 15 |
+
import type { ReactNode } from 'react';
|
| 16 |
+
import Alert from '@mui/material/Alert';
|
| 17 |
+
import Box from '@mui/material/Box';
|
| 18 |
+
import Button from '@mui/material/Button';
|
| 19 |
+
import Chip from '@mui/material/Chip';
|
| 20 |
+
import CircularProgress from '@mui/material/CircularProgress';
|
| 21 |
+
import FormControl from '@mui/material/FormControl';
|
| 22 |
+
import InputLabel from '@mui/material/InputLabel';
|
| 23 |
+
import MenuItem from '@mui/material/MenuItem';
|
| 24 |
+
import Paper from '@mui/material/Paper';
|
| 25 |
+
import Select from '@mui/material/Select';
|
| 26 |
+
import Tab from '@mui/material/Tab';
|
| 27 |
+
import Table from '@mui/material/Table';
|
| 28 |
+
import TableBody from '@mui/material/TableBody';
|
| 29 |
+
import TableCell from '@mui/material/TableCell';
|
| 30 |
+
import TableHead from '@mui/material/TableHead';
|
| 31 |
+
import TableRow from '@mui/material/TableRow';
|
| 32 |
+
import Tabs from '@mui/material/Tabs';
|
| 33 |
+
import TextField from '@mui/material/TextField';
|
| 34 |
+
import Typography from '@mui/material/Typography';
|
| 35 |
+
import RotrAttributionHeatmap from '../../components/rotr/RotrAttributionHeatmap';
|
| 36 |
+
import RotrBevReplay from '../../components/rotr/RotrBevReplay';
|
| 37 |
+
import RotrQueryBuilder from '../../components/rotr/RotrQueryBuilder';
|
| 38 |
+
import * as api from '../../services/rotr';
|
| 39 |
+
import type {
|
| 40 |
+
RotrAttributionMatrix,
|
| 41 |
+
RotrConsequenceDetail,
|
| 42 |
+
RotrHITLReview,
|
| 43 |
+
RotrQueryResponse,
|
| 44 |
+
RotrRegressionResult,
|
| 45 |
+
RotrRunListItem,
|
| 46 |
+
RotrRunSummary,
|
| 47 |
+
RotrStopshipPolicy,
|
| 48 |
+
RotrSuite,
|
| 49 |
+
RotrTrainingCandidate,
|
| 50 |
+
} from '../../types/rotr';
|
| 51 |
+
|
| 52 |
+
const MODEL_VERSIONS = ['stack-v1', 'stack-v2-improved', 'stack-v3-planning-regression'];
|
| 53 |
+
|
| 54 |
+
const CONSEQUENCE_COLOR: Record<string, string> = {
|
| 55 |
+
SAFETY_CRITICAL: '#b71c1c',
|
| 56 |
+
PLANNER_INTERVENTION: '#e65100',
|
| 57 |
+
DEGRADED_COMFORT: '#f9a825',
|
| 58 |
+
NO_MATERIAL_CONSEQUENCE: '#37474f',
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
const ROLE_COLOR: Record<string, string> = {
|
| 62 |
+
REGRESSION: '#6a1b9a',
|
| 63 |
+
LAUNCH: '#4527a0',
|
| 64 |
+
TRAIN: '#1565c0',
|
| 65 |
+
VALIDATION: '#00695c',
|
| 66 |
+
TEST: '#5d4037',
|
| 67 |
+
MONITORING: '#37474f',
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
function pct(v: number | null | undefined): string {
|
| 71 |
+
return v == null ? '—' : `${(v * 100).toFixed(1)}%`;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function SectionCard({ title, children, subtitle }: { title: string; subtitle?: string; children: ReactNode }) {
|
| 75 |
+
return (
|
| 76 |
+
<Paper sx={{ p: 2, bgcolor: '#161b22', border: '1px solid #232a31', borderRadius: 2 }}>
|
| 77 |
+
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: subtitle ? 0 : 1.25 }}>
|
| 78 |
+
{title}
|
| 79 |
+
</Typography>
|
| 80 |
+
{subtitle ? (
|
| 81 |
+
<Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mb: 1.25 }}>
|
| 82 |
+
{subtitle}
|
| 83 |
+
</Typography>
|
| 84 |
+
) : null}
|
| 85 |
+
{children}
|
| 86 |
+
</Paper>
|
| 87 |
+
);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function Tile({ label, value, hint, color }: { label: string; value: string; hint?: string; color?: string }) {
|
| 91 |
+
return (
|
| 92 |
+
<Paper sx={{ p: 1.5, bgcolor: '#161b22', border: '1px solid #232a31', minWidth: 148, flex: 1 }}>
|
| 93 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 94 |
+
{label}
|
| 95 |
+
</Typography>
|
| 96 |
+
<Typography variant="h5" sx={{ fontWeight: 800, color: color ?? '#e6edf3' }}>
|
| 97 |
+
{value}
|
| 98 |
+
</Typography>
|
| 99 |
+
{hint ? (
|
| 100 |
+
<Typography variant="caption" sx={{ color: '#6b7681' }}>
|
| 101 |
+
{hint}
|
| 102 |
+
</Typography>
|
| 103 |
+
) : null}
|
| 104 |
+
</Paper>
|
| 105 |
+
);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function DistBar({ counts, total }: { counts: Record<string, number>; total: number }) {
|
| 109 |
+
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
| 110 |
+
return (
|
| 111 |
+
<Box>
|
| 112 |
+
{entries.map(([k, n]) => (
|
| 113 |
+
<Box key={k} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.4 }}>
|
| 114 |
+
<Typography variant="caption" sx={{ width: 190, color: '#c9d1d9', fontFamily: 'monospace', fontSize: 11 }}>
|
| 115 |
+
{k}
|
| 116 |
+
</Typography>
|
| 117 |
+
<Box sx={{ flex: 1, bgcolor: '#0d1117', borderRadius: 1, height: 12 }}>
|
| 118 |
+
<Box sx={{ width: `${(n / Math.max(total, 1)) * 100}%`, bgcolor: '#4fc3f7', height: 12, borderRadius: 1 }} />
|
| 119 |
+
</Box>
|
| 120 |
+
<Typography variant="caption" sx={{ width: 26, textAlign: 'right', color: '#8a949e' }}>
|
| 121 |
+
{n}
|
| 122 |
+
</Typography>
|
| 123 |
+
</Box>
|
| 124 |
+
))}
|
| 125 |
+
</Box>
|
| 126 |
+
);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
export default function RotrControlCenterPage() {
|
| 130 |
+
const [tab, setTab] = useState(0);
|
| 131 |
+
const [runs, setRuns] = useState<RotrRunListItem[]>([]);
|
| 132 |
+
const [runId, setRunId] = useState<string>('');
|
| 133 |
+
const [run, setRun] = useState<RotrRunSummary | null>(null);
|
| 134 |
+
const [matrix, setMatrix] = useState<RotrAttributionMatrix | null>(null);
|
| 135 |
+
const [allViolations, setAllViolations] = useState<RotrQueryResponse | null>(null);
|
| 136 |
+
const [selectedVid, setSelectedVid] = useState<string | null>(null);
|
| 137 |
+
const [consequence, setConsequence] = useState<RotrConsequenceDetail | null>(null);
|
| 138 |
+
const [queryResult, setQueryResult] = useState<RotrQueryResponse | null>(null);
|
| 139 |
+
const [regressions, setRegressions] = useState<RotrRegressionResult[]>([]);
|
| 140 |
+
const [baselineId, setBaselineId] = useState<string>('');
|
| 141 |
+
const [hitl, setHitl] = useState<RotrHITLReview[]>([]);
|
| 142 |
+
const [candidates, setCandidates] = useState<RotrTrainingCandidate[]>([]);
|
| 143 |
+
const [suite, setSuite] = useState<RotrSuite | null>(null);
|
| 144 |
+
const [policy, setPolicy] = useState<RotrStopshipPolicy | null>(null);
|
| 145 |
+
const [overrideReason, setOverrideReason] = useState('');
|
| 146 |
+
const [nScenarios, setNScenarios] = useState(28);
|
| 147 |
+
const [seed, setSeed] = useState(7);
|
| 148 |
+
const [modelVersion, setModelVersion] = useState('stack-v1');
|
| 149 |
+
const [busy, setBusy] = useState(false);
|
| 150 |
+
const [error, setError] = useState<string | null>(null);
|
| 151 |
+
const [notice, setNotice] = useState<string | null>(null);
|
| 152 |
+
const [lastRunWallMs, setLastRunWallMs] = useState<number | null>(null);
|
| 153 |
+
|
| 154 |
+
const loadRun = useCallback(async (id: string) => {
|
| 155 |
+
const [summary, m, all, queue] = await Promise.all([
|
| 156 |
+
api.getRun(id),
|
| 157 |
+
api.getAttributionMatrix(id),
|
| 158 |
+
api.structuredQuery({ run_id: id }),
|
| 159 |
+
api.getHitlQueue(id),
|
| 160 |
+
]);
|
| 161 |
+
setRun(summary);
|
| 162 |
+
setMatrix(m);
|
| 163 |
+
setAllViolations(all);
|
| 164 |
+
setHitl(queue);
|
| 165 |
+
setSelectedVid(null);
|
| 166 |
+
setConsequence(null);
|
| 167 |
+
setQueryResult(null);
|
| 168 |
+
}, []);
|
| 169 |
+
|
| 170 |
+
const refreshGlobal = useCallback(async () => {
|
| 171 |
+
const [rs, regs, cands, s, pol] = await Promise.all([
|
| 172 |
+
api.listRuns(),
|
| 173 |
+
api.listRegressions(),
|
| 174 |
+
api.listCandidates(),
|
| 175 |
+
api.getSuite(),
|
| 176 |
+
api.getStopshipPolicy(),
|
| 177 |
+
]);
|
| 178 |
+
setRuns(rs);
|
| 179 |
+
setRegressions(regs);
|
| 180 |
+
setCandidates(cands);
|
| 181 |
+
setSuite(s);
|
| 182 |
+
setPolicy(pol);
|
| 183 |
+
return rs;
|
| 184 |
+
}, []);
|
| 185 |
+
|
| 186 |
+
useEffect(() => {
|
| 187 |
+
refreshGlobal()
|
| 188 |
+
.then((rs) => {
|
| 189 |
+
if (rs.length && !runId) {
|
| 190 |
+
setRunId(rs[rs.length - 1].run_id);
|
| 191 |
+
return loadRun(rs[rs.length - 1].run_id);
|
| 192 |
+
}
|
| 193 |
+
return undefined;
|
| 194 |
+
})
|
| 195 |
+
.catch((e) => setError(String(e)));
|
| 196 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 197 |
+
}, []);
|
| 198 |
+
|
| 199 |
+
const generateAndRun = async () => {
|
| 200 |
+
setBusy(true);
|
| 201 |
+
setError(null);
|
| 202 |
+
try {
|
| 203 |
+
const start = performance.now();
|
| 204 |
+
const summary = await api.createRun({ n_scenarios: nScenarios, seed, model_version: modelVersion });
|
| 205 |
+
setLastRunWallMs(performance.now() - start);
|
| 206 |
+
await refreshGlobal();
|
| 207 |
+
setRunId(summary.run_id);
|
| 208 |
+
await loadRun(summary.run_id);
|
| 209 |
+
setNotice(`run ${summary.run_id} complete: ${summary.n_violations} violations on ${summary.n_scenarios} scenarios`);
|
| 210 |
+
} catch (e) {
|
| 211 |
+
setError(String(e));
|
| 212 |
+
} finally {
|
| 213 |
+
setBusy(false);
|
| 214 |
+
}
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
const selectRun = async (id: string) => {
|
| 218 |
+
setRunId(id);
|
| 219 |
+
setBusy(true);
|
| 220 |
+
try {
|
| 221 |
+
await loadRun(id);
|
| 222 |
+
} catch (e) {
|
| 223 |
+
setError(String(e));
|
| 224 |
+
} finally {
|
| 225 |
+
setBusy(false);
|
| 226 |
+
}
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
const selectViolation = async (vid: string) => {
|
| 230 |
+
setSelectedVid(vid);
|
| 231 |
+
try {
|
| 232 |
+
setConsequence(await api.getConsequence(runId, vid));
|
| 233 |
+
} catch (e) {
|
| 234 |
+
setError(String(e));
|
| 235 |
+
}
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
const runQuery = async (text: string | undefined, filters: Record<string, string>) => {
|
| 239 |
+
setBusy(true);
|
| 240 |
+
try {
|
| 241 |
+
setQueryResult(await api.structuredQuery({ run_id: runId, text, filters }));
|
| 242 |
+
} catch (e) {
|
| 243 |
+
setError(String(e));
|
| 244 |
+
} finally {
|
| 245 |
+
setBusy(false);
|
| 246 |
+
}
|
| 247 |
+
};
|
| 248 |
+
|
| 249 |
+
const doRegression = async () => {
|
| 250 |
+
if (!baselineId || !runId) return;
|
| 251 |
+
setBusy(true);
|
| 252 |
+
try {
|
| 253 |
+
await api.runRegression({ baseline_run_id: baselineId, candidate_run_id: runId });
|
| 254 |
+
setRegressions(await api.listRegressions());
|
| 255 |
+
setNotice('regression comparison complete (statistics delegated to seqeval)');
|
| 256 |
+
} catch (e) {
|
| 257 |
+
setError(String(e));
|
| 258 |
+
} finally {
|
| 259 |
+
setBusy(false);
|
| 260 |
+
}
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
const act = async (review: RotrHITLReview, action: 'VALIDATE' | 'REJECT') => {
|
| 264 |
+
try {
|
| 265 |
+
await api.hitlAction({
|
| 266 |
+
run_id: runId,
|
| 267 |
+
review_id: review.review_id,
|
| 268 |
+
action,
|
| 269 |
+
actor: 'studio-reviewer',
|
| 270 |
+
notes: action === 'VALIDATE' ? 'confirmed via control center' : 'rejected via control center',
|
| 271 |
+
});
|
| 272 |
+
setHitl(await api.getHitlQueue(runId));
|
| 273 |
+
setCandidates(await api.listCandidates());
|
| 274 |
+
setSuite(await api.getSuite());
|
| 275 |
+
setNotice(action === 'VALIDATE' ? 'validated → regression artifact created (role REGRESSION, protected)' : 'review rejected');
|
| 276 |
+
} catch (e) {
|
| 277 |
+
setError(String(e));
|
| 278 |
+
}
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
const promote = async (c: RotrTrainingCandidate) => {
|
| 282 |
+
try {
|
| 283 |
+
await api.promoteCandidate({ candidate_id: c.candidate_id, actor: 'studio-reviewer' });
|
| 284 |
+
setCandidates(await api.listCandidates());
|
| 285 |
+
setNotice(`candidate ${c.candidate_id} promoted to training`);
|
| 286 |
+
} catch (e) {
|
| 287 |
+
setError(`contamination guard: ${String(e)}`);
|
| 288 |
+
}
|
| 289 |
+
};
|
| 290 |
+
|
| 291 |
+
const override = async (c: RotrTrainingCandidate) => {
|
| 292 |
+
try {
|
| 293 |
+
await api.overrideCandidate({ candidate_id: c.candidate_id, actor: 'safety-lead', reason: overrideReason });
|
| 294 |
+
setCandidates(await api.listCandidates());
|
| 295 |
+
setNotice('governance override recorded (audited)');
|
| 296 |
+
} catch (e) {
|
| 297 |
+
setError(String(e));
|
| 298 |
+
}
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
const distributions = useMemo(() => {
|
| 302 |
+
const axes = ['legality', 'vulnerability', 'visibility', 'behavior', 'traffic_control'] as const;
|
| 303 |
+
const out: Record<string, Record<string, number>> = {};
|
| 304 |
+
for (const axis of axes) out[axis] = {};
|
| 305 |
+
const layerCounts: Record<string, number> = {};
|
| 306 |
+
const consCounts: Record<string, number> = {};
|
| 307 |
+
for (const v of allViolations?.results ?? []) {
|
| 308 |
+
for (const axis of axes) {
|
| 309 |
+
const val = v.taxonomy[axis] ?? '?';
|
| 310 |
+
out[axis][val] = (out[axis][val] ?? 0) + 1;
|
| 311 |
+
}
|
| 312 |
+
const l = v.primary_layer ?? 'unattributed (HITL)';
|
| 313 |
+
layerCounts[l] = (layerCounts[l] ?? 0) + 1;
|
| 314 |
+
const c = v.consequence_class ?? 'UNCLASSIFIED';
|
| 315 |
+
consCounts[c] = (consCounts[c] ?? 0) + 1;
|
| 316 |
+
}
|
| 317 |
+
return { axes: out, layers: layerCounts, consequences: consCounts };
|
| 318 |
+
}, [allViolations]);
|
| 319 |
+
|
| 320 |
+
const scCount = distributions.consequences['SAFETY_CRITICAL'] ?? 0;
|
| 321 |
+
const latestReg = regressions.length ? regressions[regressions.length - 1] : null;
|
| 322 |
+
const metrics = run?.metrics;
|
| 323 |
+
|
| 324 |
+
return (
|
| 325 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 326 |
+
{/* controls */}
|
| 327 |
+
<Paper sx={{ p: 1.5, bgcolor: '#161b22', border: '1px solid #232a31', display: 'flex', gap: 1.5, alignItems: 'center', flexWrap: 'wrap' }}>
|
| 328 |
+
<FormControl size="small" sx={{ minWidth: 230 }}>
|
| 329 |
+
<InputLabel>stack under evaluation</InputLabel>
|
| 330 |
+
<Select label="stack under evaluation" value={modelVersion} onChange={(e) => setModelVersion(e.target.value)}>
|
| 331 |
+
{MODEL_VERSIONS.map((m) => (
|
| 332 |
+
<MenuItem key={m} value={m}>
|
| 333 |
+
{m}
|
| 334 |
+
</MenuItem>
|
| 335 |
+
))}
|
| 336 |
+
</Select>
|
| 337 |
+
</FormControl>
|
| 338 |
+
<TextField size="small" type="number" label="scenarios" value={nScenarios} onChange={(e) => setNScenarios(Number(e.target.value))} sx={{ width: 100 }} />
|
| 339 |
+
<TextField size="small" type="number" label="seed" value={seed} onChange={(e) => setSeed(Number(e.target.value))} sx={{ width: 90 }} />
|
| 340 |
+
<Button variant="contained" onClick={generateAndRun} disabled={busy}>
|
| 341 |
+
Generate bank & run detection
|
| 342 |
+
</Button>
|
| 343 |
+
{busy ? <CircularProgress size={20} /> : null}
|
| 344 |
+
<Box sx={{ flex: 1 }} />
|
| 345 |
+
<FormControl size="small" sx={{ minWidth: 260 }}>
|
| 346 |
+
<InputLabel>inspect run</InputLabel>
|
| 347 |
+
<Select label="inspect run" value={runId} onChange={(e) => selectRun(e.target.value)}>
|
| 348 |
+
{runs.map((r) => (
|
| 349 |
+
<MenuItem key={r.run_id} value={r.run_id}>
|
| 350 |
+
{r.run_id} · {r.model_version} · {r.n_violations} violations
|
| 351 |
+
</MenuItem>
|
| 352 |
+
))}
|
| 353 |
+
</Select>
|
| 354 |
+
</FormControl>
|
| 355 |
+
</Paper>
|
| 356 |
+
|
| 357 |
+
{error ? (
|
| 358 |
+
<Alert severity="error" onClose={() => setError(null)}>
|
| 359 |
+
{error}
|
| 360 |
+
</Alert>
|
| 361 |
+
) : null}
|
| 362 |
+
{notice ? (
|
| 363 |
+
<Alert severity="info" onClose={() => setNotice(null)}>
|
| 364 |
+
{notice}
|
| 365 |
+
</Alert>
|
| 366 |
+
) : null}
|
| 367 |
+
|
| 368 |
+
{!run ? (
|
| 369 |
+
<Alert severity="info">No runs yet — generate a scenario bank and run detection to populate the control center.</Alert>
|
| 370 |
+
) : (
|
| 371 |
+
<>
|
| 372 |
+
{/* gate banner */}
|
| 373 |
+
<Alert
|
| 374 |
+
severity={run.gate.outcome === 'NO_GO' ? 'error' : 'success'}
|
| 375 |
+
sx={{ fontWeight: 700, alignItems: 'center' }}
|
| 376 |
+
action={
|
| 377 |
+
<Chip size="small" label={`stop-ship policy ${run.gate.policy_version}`} sx={{ fontFamily: 'monospace', fontSize: 10 }} />
|
| 378 |
+
}
|
| 379 |
+
>
|
| 380 |
+
RELEASE GATE: {run.gate.outcome}
|
| 381 |
+
{run.gate.outcome === 'NO_GO'
|
| 382 |
+
? ` — ${run.gate.events.filter((e) => e.fired).length} catastrophic conjunction(s) fired (VRU + missed detection + safety-critical exposure). Deterministic policy, not LLM-driven.`
|
| 383 |
+
: ' — no catastrophic conjunction observed on this run.'}
|
| 384 |
+
</Alert>
|
| 385 |
+
|
| 386 |
+
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ borderBottom: '1px solid #232a31' }}>
|
| 387 |
+
<Tab label="Executive" />
|
| 388 |
+
<Tab label="Engineering" />
|
| 389 |
+
<Tab label="Data" />
|
| 390 |
+
<Tab label="Infrastructure" />
|
| 391 |
+
</Tabs>
|
| 392 |
+
|
| 393 |
+
{/* ------------------------------------------------ EXECUTIVE */}
|
| 394 |
+
{tab === 0 && metrics ? (
|
| 395 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 396 |
+
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
| 397 |
+
<Tile label="violations detected" value={String(run.n_violations)} hint={`${metrics.n_committed_violations} planted · recall ${pct(metrics.rotr_recall)}`} />
|
| 398 |
+
<Tile label="safety-critical" value={String(scCount)} color="#ef5350" hint="counterfactual consequence class" />
|
| 399 |
+
<Tile label="SC-ROTR recall" value={pct(metrics.sc_rotr_recall)} hint={`exposure-derived weights ${metrics.weight_calibration.version}`} />
|
| 400 |
+
<Tile label="false accusations" value={pct(metrics.false_accusation_rate)} hint={`${metrics.n_planted_non_violations} planted non-violations`} />
|
| 401 |
+
<Tile label="BCR" value={pct(metrics.bcr)} hint="behavioral consequence rate" />
|
| 402 |
+
<Tile label="CFR" value={pct(metrics.cfr)} hint={`95% CI [${pct(metrics.cfr_wilson_95[0])}, ${pct(metrics.cfr_wilson_95[1])}]`} />
|
| 403 |
+
</Box>
|
| 404 |
+
<SectionCard title="Regression status" subtitle="claims are delegated to seqeval's sequential engine; every comparison lands in the six-outcome distinction">
|
| 405 |
+
{latestReg ? (
|
| 406 |
+
<Box>
|
| 407 |
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap', mb: 1 }}>
|
| 408 |
+
<Chip
|
| 409 |
+
label={latestReg.primary_outcome}
|
| 410 |
+
sx={{
|
| 411 |
+
fontWeight: 800,
|
| 412 |
+
bgcolor: latestReg.primary_outcome.includes('REGRESSION') ? '#b71c1c' : '#1b5e20',
|
| 413 |
+
color: '#fff',
|
| 414 |
+
}}
|
| 415 |
+
/>
|
| 416 |
+
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: '#8a949e' }}>
|
| 417 |
+
{latestReg.baseline_model} → {latestReg.candidate_model}
|
| 418 |
+
</Typography>
|
| 419 |
+
</Box>
|
| 420 |
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
| 421 |
+
{Object.entries(latestReg.six_outcomes).map(([k, v]) => (
|
| 422 |
+
<Chip key={k} size="small" label={k.replace(/_/g, ' ')} sx={{ height: 20, fontSize: 10, bgcolor: v ? '#37474f' : '#161b22', color: v ? '#fff' : '#5c6670', border: '1px solid #232a31' }} />
|
| 423 |
+
))}
|
| 424 |
+
</Box>
|
| 425 |
+
<Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
|
| 426 |
+
{latestReg.distribution_note}
|
| 427 |
+
</Typography>
|
| 428 |
+
</Box>
|
| 429 |
+
) : (
|
| 430 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 431 |
+
No comparisons yet — pick a baseline run under Engineering → Model comparison.
|
| 432 |
+
</Typography>
|
| 433 |
+
)}
|
| 434 |
+
</SectionCard>
|
| 435 |
+
<SectionCard title="Runs">
|
| 436 |
+
<Table size="small">
|
| 437 |
+
<TableHead>
|
| 438 |
+
<TableRow>
|
| 439 |
+
<TableCell>run</TableCell>
|
| 440 |
+
<TableCell>stack</TableCell>
|
| 441 |
+
<TableCell align="right">scenarios</TableCell>
|
| 442 |
+
<TableCell align="right">violations</TableCell>
|
| 443 |
+
<TableCell align="right">ROTR recall</TableCell>
|
| 444 |
+
<TableCell>gate</TableCell>
|
| 445 |
+
</TableRow>
|
| 446 |
+
</TableHead>
|
| 447 |
+
<TableBody>
|
| 448 |
+
{runs.map((r) => (
|
| 449 |
+
<TableRow key={r.run_id} hover selected={r.run_id === runId} onClick={() => selectRun(r.run_id)} sx={{ cursor: 'pointer' }}>
|
| 450 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{r.run_id}</TableCell>
|
| 451 |
+
<TableCell>{r.model_version}</TableCell>
|
| 452 |
+
<TableCell align="right">{r.n_scenarios}</TableCell>
|
| 453 |
+
<TableCell align="right">{r.n_violations}</TableCell>
|
| 454 |
+
<TableCell align="right">{pct(r.rotr_recall)}</TableCell>
|
| 455 |
+
<TableCell>
|
| 456 |
+
<Chip size="small" label={r.gate_outcome} sx={{ height: 18, fontSize: 10, fontWeight: 700, bgcolor: r.gate_outcome === 'NO_GO' ? '#b71c1c' : '#1b5e20', color: '#fff' }} />
|
| 457 |
+
</TableCell>
|
| 458 |
+
</TableRow>
|
| 459 |
+
))}
|
| 460 |
+
</TableBody>
|
| 461 |
+
</Table>
|
| 462 |
+
</SectionCard>
|
| 463 |
+
</Box>
|
| 464 |
+
) : null}
|
| 465 |
+
|
| 466 |
+
{/* ------------------------------------------------ ENGINEERING */}
|
| 467 |
+
{tab === 1 ? (
|
| 468 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 469 |
+
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr 1fr' }, gap: 2 }}>
|
| 470 |
+
<SectionCard title="Violations by legality axis">
|
| 471 |
+
<DistBar counts={distributions.axes.legality ?? {}} total={run.n_violations} />
|
| 472 |
+
</SectionCard>
|
| 473 |
+
<SectionCard title="By attributed causal layer">
|
| 474 |
+
<DistBar counts={distributions.layers} total={run.n_violations} />
|
| 475 |
+
</SectionCard>
|
| 476 |
+
<SectionCard title="Consequence breakdown">
|
| 477 |
+
{Object.entries(distributions.consequences).map(([k, n]) => (
|
| 478 |
+
<Box key={k} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
| 479 |
+
<Chip size="small" label={k} sx={{ bgcolor: CONSEQUENCE_COLOR[k] ?? '#37474f', color: '#fff', height: 20, fontSize: 10, minWidth: 200 }} />
|
| 480 |
+
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
| 481 |
+
{n}
|
| 482 |
+
</Typography>
|
| 483 |
+
</Box>
|
| 484 |
+
))}
|
| 485 |
+
</SectionCard>
|
| 486 |
+
</Box>
|
| 487 |
+
|
| 488 |
+
<SectionCard title="Attribution matrix" subtitle="a violation is NEVER auto-attributed to perception — every layer needs its own positive evidence; click a row to open its consequence replay">
|
| 489 |
+
{matrix ? <RotrAttributionHeatmap matrix={matrix} selectedViolationId={selectedVid} onSelect={selectViolation} /> : <CircularProgress size={20} />}
|
| 490 |
+
</SectionCard>
|
| 491 |
+
|
| 492 |
+
{consequence ? (
|
| 493 |
+
<SectionCard title={`Consequence replay — ${consequence.violation_id}`} subtitle="observed trajectory vs corrected-input counterfactual; scrub time with the slider">
|
| 494 |
+
<RotrBevReplay detail={consequence} />
|
| 495 |
+
</SectionCard>
|
| 496 |
+
) : (
|
| 497 |
+
<Alert severity="info">Select a violation in the attribution matrix to open its BEV consequence replay.</Alert>
|
| 498 |
+
)}
|
| 499 |
+
|
| 500 |
+
<SectionCard title="Cluster browser" subtitle="recurring patterns grouped by structured signature (legality · actor · control · behavior · layer)">
|
| 501 |
+
<Table size="small">
|
| 502 |
+
<TableHead>
|
| 503 |
+
<TableRow>
|
| 504 |
+
<TableCell>cluster</TableCell>
|
| 505 |
+
<TableCell>signature key</TableCell>
|
| 506 |
+
<TableCell align="right">members</TableCell>
|
| 507 |
+
<TableCell>environments</TableCell>
|
| 508 |
+
<TableCell>consequences</TableCell>
|
| 509 |
+
<TableCell />
|
| 510 |
+
</TableRow>
|
| 511 |
+
</TableHead>
|
| 512 |
+
<TableBody>
|
| 513 |
+
{run.clusters.map((c) => (
|
| 514 |
+
<TableRow key={c.cluster_id} hover>
|
| 515 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{c.cluster_id}</TableCell>
|
| 516 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{c.key}</TableCell>
|
| 517 |
+
<TableCell align="right">{c.count}</TableCell>
|
| 518 |
+
<TableCell sx={{ fontSize: 11 }}>{c.environment_spread.join(', ')}</TableCell>
|
| 519 |
+
<TableCell sx={{ fontSize: 11 }}>
|
| 520 |
+
{Object.entries(c.consequence_distribution)
|
| 521 |
+
.map(([k, n]) => `${k}:${n}`)
|
| 522 |
+
.join(' ')}
|
| 523 |
+
</TableCell>
|
| 524 |
+
<TableCell>
|
| 525 |
+
<Button size="small" onClick={() => selectViolation(c.exemplar_violation_id)}>
|
| 526 |
+
open exemplar
|
| 527 |
+
</Button>
|
| 528 |
+
</TableCell>
|
| 529 |
+
</TableRow>
|
| 530 |
+
))}
|
| 531 |
+
</TableBody>
|
| 532 |
+
</Table>
|
| 533 |
+
</SectionCard>
|
| 534 |
+
|
| 535 |
+
<SectionCard title="Model comparison" subtitle="baseline vs the currently inspected run; statistics delegated to seqeval (rare-event aware sequential testing)">
|
| 536 |
+
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', mb: 1.5, flexWrap: 'wrap' }}>
|
| 537 |
+
<FormControl size="small" sx={{ minWidth: 260 }}>
|
| 538 |
+
<InputLabel>baseline run</InputLabel>
|
| 539 |
+
<Select label="baseline run" value={baselineId} onChange={(e) => setBaselineId(e.target.value)}>
|
| 540 |
+
{runs
|
| 541 |
+
.filter((r) => r.run_id !== runId)
|
| 542 |
+
.map((r) => (
|
| 543 |
+
<MenuItem key={r.run_id} value={r.run_id}>
|
| 544 |
+
{r.run_id} · {r.model_version}
|
| 545 |
+
</MenuItem>
|
| 546 |
+
))}
|
| 547 |
+
</Select>
|
| 548 |
+
</FormControl>
|
| 549 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 550 |
+
candidate = {runId} ({run.model_version})
|
| 551 |
+
</Typography>
|
| 552 |
+
<Button variant="outlined" disabled={!baselineId || busy} onClick={doRegression}>
|
| 553 |
+
Run seqeval-gated comparison
|
| 554 |
+
</Button>
|
| 555 |
+
</Box>
|
| 556 |
+
{latestReg ? (
|
| 557 |
+
<Table size="small">
|
| 558 |
+
<TableHead>
|
| 559 |
+
<TableRow>
|
| 560 |
+
<TableCell>metric</TableCell>
|
| 561 |
+
<TableCell align="right">delta (candidate − baseline)</TableCell>
|
| 562 |
+
</TableRow>
|
| 563 |
+
</TableHead>
|
| 564 |
+
<TableBody>
|
| 565 |
+
{Object.entries(latestReg.metric_deltas).map(([k, v]) => (
|
| 566 |
+
<TableRow key={k}>
|
| 567 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{k}</TableCell>
|
| 568 |
+
<TableCell align="right" sx={{ color: v > 0 && k.startsWith('violation_rate') ? '#ef5350' : v < 0 && k.startsWith('violation_rate') ? '#66bb6a' : '#c9d1d9', fontFamily: 'monospace' }}>
|
| 569 |
+
{v > 0 ? '+' : ''}
|
| 570 |
+
{v.toFixed(4)}
|
| 571 |
+
</TableCell>
|
| 572 |
+
</TableRow>
|
| 573 |
+
))}
|
| 574 |
+
</TableBody>
|
| 575 |
+
</Table>
|
| 576 |
+
) : null}
|
| 577 |
+
</SectionCard>
|
| 578 |
+
|
| 579 |
+
<SectionCard title="Taxonomy mining — structured query" subtitle="six-axis taxonomy (+ road geometry / traffic control / visibility extensions); text parses through a deterministic keyword map">
|
| 580 |
+
<RotrQueryBuilder onRun={runQuery} busy={busy} />
|
| 581 |
+
{queryResult ? (
|
| 582 |
+
<Box sx={{ mt: 1.5 }}>
|
| 583 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 584 |
+
parsed query:{' '}
|
| 585 |
+
{Object.entries(queryResult.query)
|
| 586 |
+
.filter(([k, v]) => v != null && k !== 'text')
|
| 587 |
+
.map(([k, v]) => `${k}=${v}`)
|
| 588 |
+
.join(' · ') || '(no filters)'}{' '}
|
| 589 |
+
→ {queryResult.n_results} result(s)
|
| 590 |
+
</Typography>
|
| 591 |
+
<Table size="small">
|
| 592 |
+
<TableHead>
|
| 593 |
+
<TableRow>
|
| 594 |
+
<TableCell>violation</TableCell>
|
| 595 |
+
<TableCell>rule</TableCell>
|
| 596 |
+
<TableCell>layer</TableCell>
|
| 597 |
+
<TableCell>consequence</TableCell>
|
| 598 |
+
<TableCell>environment</TableCell>
|
| 599 |
+
<TableCell>cluster</TableCell>
|
| 600 |
+
<TableCell />
|
| 601 |
+
</TableRow>
|
| 602 |
+
</TableHead>
|
| 603 |
+
<TableBody>
|
| 604 |
+
{queryResult.results.map((r) => (
|
| 605 |
+
<TableRow key={r.violation_id} hover>
|
| 606 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{r.violation_id.replace(/^bank-[0-9a-f]+-/, '')}</TableCell>
|
| 607 |
+
<TableCell sx={{ fontSize: 11 }}>{r.rule_id}</TableCell>
|
| 608 |
+
<TableCell sx={{ fontSize: 11 }}>{r.primary_layer ?? '—'}</TableCell>
|
| 609 |
+
<TableCell>
|
| 610 |
+
<Chip size="small" label={r.consequence_class ?? '—'} sx={{ height: 18, fontSize: 9, bgcolor: CONSEQUENCE_COLOR[r.consequence_class ?? ''] ?? '#37474f', color: '#fff' }} />
|
| 611 |
+
</TableCell>
|
| 612 |
+
<TableCell sx={{ fontSize: 11 }}>
|
| 613 |
+
{r.environment.visibility}/{r.environment.lighting}/{r.environment.weather}
|
| 614 |
+
</TableCell>
|
| 615 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 10 }}>{r.cluster_id ?? '—'}</TableCell>
|
| 616 |
+
<TableCell>
|
| 617 |
+
<Button size="small" onClick={() => selectViolation(r.violation_id)}>
|
| 618 |
+
replay
|
| 619 |
+
</Button>
|
| 620 |
+
</TableCell>
|
| 621 |
+
</TableRow>
|
| 622 |
+
))}
|
| 623 |
+
</TableBody>
|
| 624 |
+
</Table>
|
| 625 |
+
</Box>
|
| 626 |
+
) : null}
|
| 627 |
+
</SectionCard>
|
| 628 |
+
</Box>
|
| 629 |
+
) : null}
|
| 630 |
+
|
| 631 |
+
{/* ------------------------------------------------ DATA */}
|
| 632 |
+
{tab === 2 ? (
|
| 633 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 634 |
+
<SectionCard title="Scenario coverage" subtitle="planted opportunity mix of the inspected bank (kind × environment), with commit outcomes for this stack">
|
| 635 |
+
<Table size="small">
|
| 636 |
+
<TableHead>
|
| 637 |
+
<TableRow>
|
| 638 |
+
<TableCell>kind</TableCell>
|
| 639 |
+
<TableCell align="right">scenarios</TableCell>
|
| 640 |
+
<TableCell align="right">opportunities</TableCell>
|
| 641 |
+
<TableCell align="right">committed</TableCell>
|
| 642 |
+
<TableCell>environments</TableCell>
|
| 643 |
+
</TableRow>
|
| 644 |
+
</TableHead>
|
| 645 |
+
<TableBody>
|
| 646 |
+
{Object.entries(
|
| 647 |
+
run.scenario_summaries.reduce<Record<string, { n: number; opp: number; com: number; envs: Set<string> }>>((acc, s) => {
|
| 648 |
+
const e = acc[s.kind] ?? { n: 0, opp: 0, com: 0, envs: new Set<string>() };
|
| 649 |
+
e.n += 1;
|
| 650 |
+
if (s.is_violation_opportunity) e.opp += 1;
|
| 651 |
+
if (s.committed) e.com += 1;
|
| 652 |
+
e.envs.add(`${s.visibility}/${s.lighting}`);
|
| 653 |
+
acc[s.kind] = e;
|
| 654 |
+
return acc;
|
| 655 |
+
}, {})
|
| 656 |
+
).map(([kind, e]) => (
|
| 657 |
+
<TableRow key={kind}>
|
| 658 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{kind}</TableCell>
|
| 659 |
+
<TableCell align="right">{e.n}</TableCell>
|
| 660 |
+
<TableCell align="right">{e.opp}</TableCell>
|
| 661 |
+
<TableCell align="right" sx={{ color: e.com ? '#ef5350' : '#66bb6a' }}>
|
| 662 |
+
{e.com}
|
| 663 |
+
</TableCell>
|
| 664 |
+
<TableCell sx={{ fontSize: 11 }}>{[...e.envs].join(', ')}</TableCell>
|
| 665 |
+
</TableRow>
|
| 666 |
+
))}
|
| 667 |
+
</TableBody>
|
| 668 |
+
</Table>
|
| 669 |
+
</SectionCard>
|
| 670 |
+
|
| 671 |
+
<SectionCard title="Rare patterns" subtitle="singleton clusters — signatures observed exactly once on this run (raremine-style rarity lens)">
|
| 672 |
+
{run.clusters.filter((c) => c.count === 1).length ? (
|
| 673 |
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
| 674 |
+
{run.clusters
|
| 675 |
+
.filter((c) => c.count === 1)
|
| 676 |
+
.map((c) => (
|
| 677 |
+
<Chip key={c.cluster_id} size="small" label={c.key} onClick={() => selectViolation(c.exemplar_violation_id)} sx={{ fontFamily: 'monospace', fontSize: 10 }} />
|
| 678 |
+
))}
|
| 679 |
+
</Box>
|
| 680 |
+
) : (
|
| 681 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 682 |
+
No singleton signatures on this run.
|
| 683 |
+
</Typography>
|
| 684 |
+
)}
|
| 685 |
+
</SectionCard>
|
| 686 |
+
|
| 687 |
+
<SectionCard title="HITL validation queue" subtitle="confirmed violation → failure signature → cluster → regression-test artifact; validation is the ONLY path into the suite">
|
| 688 |
+
<Table size="small">
|
| 689 |
+
<TableHead>
|
| 690 |
+
<TableRow>
|
| 691 |
+
<TableCell>review</TableCell>
|
| 692 |
+
<TableCell>violation</TableCell>
|
| 693 |
+
<TableCell>cluster</TableCell>
|
| 694 |
+
<TableCell>status</TableCell>
|
| 695 |
+
<TableCell />
|
| 696 |
+
</TableRow>
|
| 697 |
+
</TableHead>
|
| 698 |
+
<TableBody>
|
| 699 |
+
{hitl.map((r) => (
|
| 700 |
+
<TableRow key={r.review_id} hover>
|
| 701 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{r.review_id.replace(/^rev-bank-[0-9a-f]+-/, '')}</TableCell>
|
| 702 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{r.violation_id.replace(/^bank-[0-9a-f]+-/, '')}</TableCell>
|
| 703 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 10 }}>{r.cluster_id ?? '—'}</TableCell>
|
| 704 |
+
<TableCell>
|
| 705 |
+
<Chip size="small" label={r.status} sx={{ height: 18, fontSize: 10, bgcolor: r.status === 'PENDING' ? '#e65100' : r.status === 'VALIDATED' ? '#1b5e20' : '#37474f', color: '#fff' }} />
|
| 706 |
+
</TableCell>
|
| 707 |
+
<TableCell>
|
| 708 |
+
{r.status === 'PENDING' ? (
|
| 709 |
+
<>
|
| 710 |
+
<Button size="small" color="success" onClick={() => act(r, 'VALIDATE')}>
|
| 711 |
+
validate
|
| 712 |
+
</Button>
|
| 713 |
+
<Button size="small" color="inherit" onClick={() => act(r, 'REJECT')}>
|
| 714 |
+
reject
|
| 715 |
+
</Button>
|
| 716 |
+
</>
|
| 717 |
+
) : null}
|
| 718 |
+
</TableCell>
|
| 719 |
+
</TableRow>
|
| 720 |
+
))}
|
| 721 |
+
</TableBody>
|
| 722 |
+
</Table>
|
| 723 |
+
</SectionCard>
|
| 724 |
+
|
| 725 |
+
<SectionCard title="Training candidates" subtitle="immutable dataset roles; REGRESSION/LAUNCH members are never training-eligible without a recorded governance override (contamination guard)">
|
| 726 |
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
|
| 727 |
+
<TextField size="small" fullWidth label="governance override reason (required before promoting a protected member)" value={overrideReason} onChange={(e) => setOverrideReason(e.target.value)} />
|
| 728 |
+
</Box>
|
| 729 |
+
<Table size="small">
|
| 730 |
+
<TableHead>
|
| 731 |
+
<TableRow>
|
| 732 |
+
<TableCell>candidate</TableCell>
|
| 733 |
+
<TableCell>violation</TableCell>
|
| 734 |
+
<TableCell>role</TableCell>
|
| 735 |
+
<TableCell>guard</TableCell>
|
| 736 |
+
<TableCell>training-eligible</TableCell>
|
| 737 |
+
<TableCell />
|
| 738 |
+
</TableRow>
|
| 739 |
+
</TableHead>
|
| 740 |
+
<TableBody>
|
| 741 |
+
{candidates.map((c) => (
|
| 742 |
+
<TableRow key={c.candidate_id} hover>
|
| 743 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{c.candidate_id}</TableCell>
|
| 744 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{c.violation_id.replace(/^bank-[0-9a-f]+-/, '')}</TableCell>
|
| 745 |
+
<TableCell>
|
| 746 |
+
<Chip size="small" label={c.dataset_role} sx={{ height: 18, fontSize: 10, fontWeight: 700, bgcolor: ROLE_COLOR[c.dataset_role] ?? '#37474f', color: '#fff' }} />
|
| 747 |
+
</TableCell>
|
| 748 |
+
<TableCell>
|
| 749 |
+
<Chip size="small" label={c.guard_state} sx={{ height: 18, fontSize: 10, bgcolor: c.guard_state === 'PROTECTED' ? '#b71c1c' : c.guard_state === 'OVERRIDDEN' ? '#e65100' : '#1b5e20', color: '#fff' }} />
|
| 750 |
+
</TableCell>
|
| 751 |
+
<TableCell sx={{ fontSize: 11 }}>{c.training_eligible ? 'yes' : 'no'}</TableCell>
|
| 752 |
+
<TableCell>
|
| 753 |
+
<Button size="small" onClick={() => promote(c)}>
|
| 754 |
+
promote to training
|
| 755 |
+
</Button>
|
| 756 |
+
<Button size="small" color="warning" disabled={!overrideReason.trim()} onClick={() => override(c)}>
|
| 757 |
+
record override
|
| 758 |
+
</Button>
|
| 759 |
+
</TableCell>
|
| 760 |
+
</TableRow>
|
| 761 |
+
))}
|
| 762 |
+
</TableBody>
|
| 763 |
+
</Table>
|
| 764 |
+
</SectionCard>
|
| 765 |
+
|
| 766 |
+
<SectionCard title="Regression suite" subtitle={`suite members re-run against every candidate · registry backend: ${suite?.registry_backend ?? '—'}`}>
|
| 767 |
+
{suite?.members.length ? (
|
| 768 |
+
<Table size="small">
|
| 769 |
+
<TableHead>
|
| 770 |
+
<TableRow>
|
| 771 |
+
<TableCell>candidate</TableCell>
|
| 772 |
+
<TableCell>violation</TableCell>
|
| 773 |
+
<TableCell>role</TableCell>
|
| 774 |
+
<TableCell>added by</TableCell>
|
| 775 |
+
<TableCell>added at</TableCell>
|
| 776 |
+
</TableRow>
|
| 777 |
+
</TableHead>
|
| 778 |
+
<TableBody>
|
| 779 |
+
{suite.members.map((m) => (
|
| 780 |
+
<TableRow key={m.candidate_id}>
|
| 781 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{m.candidate_id}</TableCell>
|
| 782 |
+
<TableCell sx={{ fontFamily: 'monospace', fontSize: 11 }}>{m.violation_id.replace(/^bank-[0-9a-f]+-/, '')}</TableCell>
|
| 783 |
+
<TableCell>
|
| 784 |
+
<Chip size="small" label={m.role} sx={{ height: 18, fontSize: 10, bgcolor: ROLE_COLOR[m.role] ?? '#37474f', color: '#fff' }} />
|
| 785 |
+
</TableCell>
|
| 786 |
+
<TableCell sx={{ fontSize: 11 }}>{m.added_by ?? '—'}</TableCell>
|
| 787 |
+
<TableCell sx={{ fontSize: 11 }}>{m.added_at}</TableCell>
|
| 788 |
+
</TableRow>
|
| 789 |
+
))}
|
| 790 |
+
</TableBody>
|
| 791 |
+
</Table>
|
| 792 |
+
) : (
|
| 793 |
+
<Typography variant="caption" sx={{ color: '#8a949e' }}>
|
| 794 |
+
Empty — validate a HITL item to create the first regression artifact.
|
| 795 |
+
</Typography>
|
| 796 |
+
)}
|
| 797 |
+
</SectionCard>
|
| 798 |
+
</Box>
|
| 799 |
+
) : null}
|
| 800 |
+
|
| 801 |
+
{/* ------------------------------------------------ INFRASTRUCTURE */}
|
| 802 |
+
{tab === 3 && metrics ? (
|
| 803 |
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
| 804 |
+
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
| 805 |
+
<Tile
|
| 806 |
+
label="last run wall time (measured in this browser)"
|
| 807 |
+
value={lastRunWallMs != null ? `${(lastRunWallMs / 1000).toFixed(1)}s` : 'not measured this session'}
|
| 808 |
+
hint={lastRunWallMs != null ? `${(run.n_scenarios / (lastRunWallMs / 1000)).toFixed(1)} scenarios/s incl. attribution + counterfactual replay` : 'generate a run to measure'}
|
| 809 |
+
/>
|
| 810 |
+
<Tile label="scenarios in run" value={String(run.n_scenarios)} hint={`bank ${run.bank_id}`} />
|
| 811 |
+
<Tile
|
| 812 |
+
label="seqeval samples used (last comparison)"
|
| 813 |
+
value={latestReg?.seqeval && typeof latestReg.seqeval.samples_used === 'number' ? String(latestReg.seqeval.samples_used) : '—'}
|
| 814 |
+
hint={latestReg?.seqeval && typeof latestReg.seqeval.full_population === 'number' ? `of ${latestReg.seqeval.full_population} population objects (early stopping)` : 'run a comparison first'}
|
| 815 |
+
/>
|
| 816 |
+
<Tile label="evaluation cost" value="NOT MEASURED" color="#8a949e" hint="no cost meter exists in this deployment; a production system could attach one per run" />
|
| 817 |
+
<Tile label="storage footprint" value="NOT MEASURED" color="#8a949e" hint="runs persist as JSON under runs/rotr/; no quota accounting is implemented" />
|
| 818 |
+
</Box>
|
| 819 |
+
<SectionCard title="Deterministic versions" subtitle="everything that must be pinned for a byte-identical re-run">
|
| 820 |
+
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
|
| 821 |
+
<Chip size="small" label={`ruleset ${run.ruleset_version}`} sx={{ fontFamily: 'monospace' }} />
|
| 822 |
+
<Chip size="small" label={`weights ${metrics.weight_calibration.version}`} sx={{ fontFamily: 'monospace' }} />
|
| 823 |
+
<Chip size="small" label={`stop-ship policy ${policy?.policy_version ?? '—'}`} sx={{ fontFamily: 'monospace' }} />
|
| 824 |
+
<Chip size="small" label={`stack ${run.model_version}`} sx={{ fontFamily: 'monospace' }} />
|
| 825 |
+
<Chip size="small" label={`bank ${run.bank_id} (content-addressed)`} sx={{ fontFamily: 'monospace' }} />
|
| 826 |
+
</Box>
|
| 827 |
+
<Typography variant="caption" sx={{ color: '#8a949e', display: 'block', mt: 1 }}>
|
| 828 |
+
{metrics.weight_calibration.method}
|
| 829 |
+
</Typography>
|
| 830 |
+
</SectionCard>
|
| 831 |
+
<SectionCard title="Honesty ledger" subtitle="what is real vs illustrative on this page">
|
| 832 |
+
<Typography variant="body2" sx={{ color: '#c9d1d9', fontSize: 13 }}>
|
| 833 |
+
Real: rule verdicts, attribution evidence, counterfactual trajectories, seqeval statistics, contamination-guard enforcement, all determinism guarantees. Illustrative
|
| 834 |
+
(synthetic substrate): every physical threshold, the scenario kinematics, the exposure-derived weights (calibrated on the bank itself). Surrogate caveat: {metrics.surrogate_caveat}
|
| 835 |
+
</Typography>
|
| 836 |
+
</SectionCard>
|
| 837 |
+
</Box>
|
| 838 |
+
) : null}
|
| 839 |
+
</>
|
| 840 |
+
)}
|
| 841 |
+
</Box>
|
| 842 |
+
);
|
| 843 |
+
}
|
src/services/rotr.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Typed fetch wrappers for the ROTR capability (/api/rotr/*). */
|
| 2 |
+
|
| 3 |
+
import type {
|
| 4 |
+
RotrConsequenceDetail,
|
| 5 |
+
RotrQueryResponse,
|
| 6 |
+
RotrRegressionResult,
|
| 7 |
+
RotrRunListItem,
|
| 8 |
+
RotrRunSummary,
|
| 9 |
+
RotrHITLReview,
|
| 10 |
+
RotrStopshipPolicy,
|
| 11 |
+
RotrSuite,
|
| 12 |
+
RotrTrainingCandidate,
|
| 13 |
+
RotrViolationDetail,
|
| 14 |
+
RotrAttributionMatrix,
|
| 15 |
+
} from '../types/rotr';
|
| 16 |
+
|
| 17 |
+
async function handle<T>(res: Response): Promise<T> {
|
| 18 |
+
if (!res.ok) {
|
| 19 |
+
const body = await res.text().catch(() => '');
|
| 20 |
+
throw new Error(`API ${res.status}: ${body || res.statusText}`);
|
| 21 |
+
}
|
| 22 |
+
return res.json() as Promise<T>;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
| 26 |
+
|
| 27 |
+
function get<T>(url: string): Promise<T> {
|
| 28 |
+
return fetch(url).then((res) => handle<T>(res));
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function post<T>(url: string, body?: unknown): Promise<T> {
|
| 32 |
+
return fetch(url, {
|
| 33 |
+
method: 'POST',
|
| 34 |
+
headers: JSON_HEADERS,
|
| 35 |
+
body: body === undefined ? undefined : JSON.stringify(body),
|
| 36 |
+
}).then((res) => handle<T>(res));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export function createRun(params: {
|
| 40 |
+
n_scenarios: number;
|
| 41 |
+
seed: number;
|
| 42 |
+
model_version: string;
|
| 43 |
+
}): Promise<RotrRunSummary> {
|
| 44 |
+
return post('/api/rotr/runs', params);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export function listRuns(): Promise<RotrRunListItem[]> {
|
| 48 |
+
return get('/api/rotr/runs');
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function getRun(runId: string): Promise<RotrRunSummary> {
|
| 52 |
+
return get(`/api/rotr/runs/${encodeURIComponent(runId)}`);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export function getAttributionMatrix(runId: string): Promise<RotrAttributionMatrix> {
|
| 56 |
+
return get(`/api/rotr/runs/${encodeURIComponent(runId)}/attribution`);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function getViolation(runId: string, violationId: string): Promise<RotrViolationDetail> {
|
| 60 |
+
return get(
|
| 61 |
+
`/api/rotr/runs/${encodeURIComponent(runId)}/violations/${encodeURIComponent(violationId)}`
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export function getConsequence(
|
| 66 |
+
runId: string,
|
| 67 |
+
violationId: string
|
| 68 |
+
): Promise<RotrConsequenceDetail> {
|
| 69 |
+
return get(
|
| 70 |
+
`/api/rotr/runs/${encodeURIComponent(runId)}/violations/${encodeURIComponent(
|
| 71 |
+
violationId
|
| 72 |
+
)}/consequence`
|
| 73 |
+
);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export function structuredQuery(params: {
|
| 77 |
+
run_id: string;
|
| 78 |
+
text?: string;
|
| 79 |
+
filters?: Record<string, string>;
|
| 80 |
+
}): Promise<RotrQueryResponse> {
|
| 81 |
+
return post('/api/rotr/query', params);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
export function runRegression(params: {
|
| 85 |
+
baseline_run_id: string;
|
| 86 |
+
candidate_run_id: string;
|
| 87 |
+
}): Promise<RotrRegressionResult> {
|
| 88 |
+
return post('/api/rotr/regression', params);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
export function listRegressions(): Promise<RotrRegressionResult[]> {
|
| 92 |
+
return get('/api/rotr/regressions');
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export function getHitlQueue(runId: string): Promise<RotrHITLReview[]> {
|
| 96 |
+
return get(`/api/rotr/runs/${encodeURIComponent(runId)}/hitl`);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
export function hitlAction(params: {
|
| 100 |
+
run_id: string;
|
| 101 |
+
review_id: string;
|
| 102 |
+
action: 'VALIDATE' | 'REJECT';
|
| 103 |
+
actor: string;
|
| 104 |
+
notes?: string;
|
| 105 |
+
}): Promise<{ review: RotrHITLReview; candidate: RotrTrainingCandidate | null }> {
|
| 106 |
+
return post('/api/rotr/hitl/action', params);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function getSuite(): Promise<RotrSuite> {
|
| 110 |
+
return get('/api/rotr/flywheel/suite');
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
export function listCandidates(): Promise<RotrTrainingCandidate[]> {
|
| 114 |
+
return get('/api/rotr/flywheel/candidates');
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
export function promoteCandidate(params: {
|
| 118 |
+
candidate_id: string;
|
| 119 |
+
actor: string;
|
| 120 |
+
}): Promise<RotrTrainingCandidate> {
|
| 121 |
+
return post('/api/rotr/flywheel/promote', params);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
export function overrideCandidate(params: {
|
| 125 |
+
candidate_id: string;
|
| 126 |
+
actor: string;
|
| 127 |
+
reason: string;
|
| 128 |
+
}): Promise<RotrTrainingCandidate> {
|
| 129 |
+
return post('/api/rotr/flywheel/override', params);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export function getStopshipPolicy(): Promise<RotrStopshipPolicy> {
|
| 133 |
+
return get('/api/rotr/stopship/policy');
|
| 134 |
+
}
|
src/types/rotr.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Contracts for the ROTR (right-of-the-road) API (/api/rotr/*). */
|
| 2 |
+
|
| 3 |
+
export interface RotrEnvironment {
|
| 4 |
+
visibility: string;
|
| 5 |
+
weather: string;
|
| 6 |
+
lighting: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export interface RotrScenarioSummary {
|
| 10 |
+
scenario_id: string;
|
| 11 |
+
kind: string;
|
| 12 |
+
committed: boolean;
|
| 13 |
+
is_violation_opportunity: boolean;
|
| 14 |
+
expected_rule_id: string | null;
|
| 15 |
+
cause_layer: string | null;
|
| 16 |
+
vulnerability: string;
|
| 17 |
+
visibility: string;
|
| 18 |
+
lighting: string;
|
| 19 |
+
weather: string;
|
| 20 |
+
n_violations_detected: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface RotrLayerEvidence {
|
| 24 |
+
layer: string;
|
| 25 |
+
status: 'SUPPORTED' | 'RULED_OUT' | 'UNKNOWN';
|
| 26 |
+
evidence: string;
|
| 27 |
+
confidence: number;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export interface RotrAttributionRow {
|
| 31 |
+
violation_id: string;
|
| 32 |
+
scenario_id: string;
|
| 33 |
+
rule_id: string;
|
| 34 |
+
primary_layer: string | null;
|
| 35 |
+
layers: Record<string, RotrLayerEvidence>;
|
| 36 |
+
note: string;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export interface RotrAttributionMatrix {
|
| 40 |
+
run_id: string;
|
| 41 |
+
rows: RotrAttributionRow[];
|
| 42 |
+
invariant: string;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
export interface RotrSafetyAssessment {
|
| 46 |
+
min_ttc_s: number | null;
|
| 47 |
+
pet_s: number | null;
|
| 48 |
+
min_clearance_m: number | null;
|
| 49 |
+
stopping_distance_m: number | null;
|
| 50 |
+
max_braking_mps2: number;
|
| 51 |
+
max_lateral_deviation_m: number;
|
| 52 |
+
collision: boolean;
|
| 53 |
+
surrogate_caveat: string;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export interface RotrTrajectoryPoint {
|
| 57 |
+
t: number;
|
| 58 |
+
x: number;
|
| 59 |
+
y: number;
|
| 60 |
+
v: number;
|
| 61 |
+
a: number;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export interface RotrConsequenceSummary {
|
| 65 |
+
consequence_class: string;
|
| 66 |
+
corrected_layers: string[];
|
| 67 |
+
engine: string;
|
| 68 |
+
max_position_divergence_m: number;
|
| 69 |
+
observed_safety: RotrSafetyAssessment;
|
| 70 |
+
corrected_safety: RotrSafetyAssessment;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface RotrConsequenceDetail {
|
| 74 |
+
counterfactual_id: string;
|
| 75 |
+
violation_id: string;
|
| 76 |
+
scenario_id: string;
|
| 77 |
+
corrected_layers: string[];
|
| 78 |
+
consequence_class: string;
|
| 79 |
+
planner_evaluation: {
|
| 80 |
+
engine: string;
|
| 81 |
+
observed_trajectory: RotrTrajectoryPoint[];
|
| 82 |
+
corrected_trajectory: RotrTrajectoryPoint[];
|
| 83 |
+
max_position_divergence_m: number;
|
| 84 |
+
max_speed_divergence_mps: number;
|
| 85 |
+
corrected_max_braking_mps2: number;
|
| 86 |
+
corrected_intervention: boolean;
|
| 87 |
+
};
|
| 88 |
+
observed_safety: RotrSafetyAssessment;
|
| 89 |
+
corrected_safety: RotrSafetyAssessment;
|
| 90 |
+
scenario_geometry?: {
|
| 91 |
+
actual_context: {
|
| 92 |
+
intersection_type: string;
|
| 93 |
+
control: string;
|
| 94 |
+
stop_line_x: number | null;
|
| 95 |
+
intersection_x_min: number | null;
|
| 96 |
+
intersection_x_max: number | null;
|
| 97 |
+
lanes: { lane_id: string; center_y: number; restricted_to: string | null }[];
|
| 98 |
+
crosswalks: { crosswalk_id: string; x_min: number; x_max: number }[];
|
| 99 |
+
};
|
| 100 |
+
actors: {
|
| 101 |
+
actor_id: string;
|
| 102 |
+
class_name: string;
|
| 103 |
+
dims: number[];
|
| 104 |
+
states: { t: number; x: number; y: number; yaw: number }[];
|
| 105 |
+
}[];
|
| 106 |
+
};
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export interface RotrGateEvent {
|
| 110 |
+
violation_id: string;
|
| 111 |
+
legs: Record<string, boolean>;
|
| 112 |
+
fired: boolean;
|
| 113 |
+
note: string;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
export interface RotrGate {
|
| 117 |
+
gate_id: string;
|
| 118 |
+
run_id: string;
|
| 119 |
+
policy_version: string;
|
| 120 |
+
outcome: 'GO' | 'NO_GO';
|
| 121 |
+
events: RotrGateEvent[];
|
| 122 |
+
agentic_advisory: Record<string, unknown> | null;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export interface RotrCluster {
|
| 126 |
+
cluster_id: string;
|
| 127 |
+
key: string;
|
| 128 |
+
count: number;
|
| 129 |
+
member_violation_ids: string[];
|
| 130 |
+
exemplar_violation_id: string;
|
| 131 |
+
environment_spread: string[];
|
| 132 |
+
consequence_distribution: Record<string, number>;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
export interface RotrWeightStratum {
|
| 136 |
+
n: number;
|
| 137 |
+
harm_fraction: number;
|
| 138 |
+
weight: number;
|
| 139 |
+
recall: number | null;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
export interface RotrMetrics {
|
| 143 |
+
n_scenarios: number;
|
| 144 |
+
n_committed_violations: number;
|
| 145 |
+
n_planted_non_violations: number;
|
| 146 |
+
n_detected: number;
|
| 147 |
+
n_violation_records: number;
|
| 148 |
+
rotr_recall: number | null;
|
| 149 |
+
false_accusation_rate: number | null;
|
| 150 |
+
sc_rotr_recall: number | null;
|
| 151 |
+
bcr: number | null;
|
| 152 |
+
cfr: number | null;
|
| 153 |
+
cfr_wilson_95: [number, number];
|
| 154 |
+
weight_calibration: {
|
| 155 |
+
version: string;
|
| 156 |
+
method: string;
|
| 157 |
+
strata: Record<string, RotrWeightStratum>;
|
| 158 |
+
};
|
| 159 |
+
cohorts: Record<string, { n: number; detected: number; recall: number | null }>;
|
| 160 |
+
behavior_rates: Record<
|
| 161 |
+
string,
|
| 162 |
+
{ opportunities: number; committed: number; violation_rate: number | null }
|
| 163 |
+
>;
|
| 164 |
+
surrogate_caveat: string;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
export interface RotrRunSummary {
|
| 168 |
+
run_id: string;
|
| 169 |
+
bank_id: string;
|
| 170 |
+
model_version: string;
|
| 171 |
+
ruleset_version: string;
|
| 172 |
+
n_scenarios: number;
|
| 173 |
+
n_violations: number;
|
| 174 |
+
scenario_summaries: RotrScenarioSummary[];
|
| 175 |
+
metrics: RotrMetrics;
|
| 176 |
+
clusters: RotrCluster[];
|
| 177 |
+
gate: RotrGate;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
export interface RotrRunListItem {
|
| 181 |
+
run_id: string;
|
| 182 |
+
bank_id: string;
|
| 183 |
+
model_version: string;
|
| 184 |
+
n_scenarios: number;
|
| 185 |
+
n_violations: number;
|
| 186 |
+
gate_outcome: string;
|
| 187 |
+
rotr_recall: number | null;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
export interface RotrQueryResultItem {
|
| 191 |
+
violation_id: string;
|
| 192 |
+
scenario_id: string;
|
| 193 |
+
rule_id: string;
|
| 194 |
+
rule_version: string;
|
| 195 |
+
description: string;
|
| 196 |
+
taxonomy: Record<string, string>;
|
| 197 |
+
evidence: Record<string, unknown>;
|
| 198 |
+
confidence: number;
|
| 199 |
+
primary_layer: string | null;
|
| 200 |
+
consequence_class: string | null;
|
| 201 |
+
cluster_id: string | null;
|
| 202 |
+
environment: RotrEnvironment;
|
| 203 |
+
provenance: Record<string, unknown>;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
export interface RotrQueryResponse {
|
| 207 |
+
query: Record<string, string | null>;
|
| 208 |
+
n_results: number;
|
| 209 |
+
results: RotrQueryResultItem[];
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
export interface RotrViolationDetail {
|
| 213 |
+
violation: {
|
| 214 |
+
violation_id: string;
|
| 215 |
+
scenario_id: string;
|
| 216 |
+
rule_id: string;
|
| 217 |
+
rule_version: string;
|
| 218 |
+
description: string;
|
| 219 |
+
actor_ids: string[];
|
| 220 |
+
evidence: Record<string, unknown>;
|
| 221 |
+
confidence: number;
|
| 222 |
+
taxonomy: Record<string, string>;
|
| 223 |
+
};
|
| 224 |
+
attribution: {
|
| 225 |
+
primary_layer: string | null;
|
| 226 |
+
layers: Record<string, RotrLayerEvidence>;
|
| 227 |
+
note: string;
|
| 228 |
+
} | null;
|
| 229 |
+
consequence: RotrConsequenceSummary | null;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
export interface RotrRegressionResult {
|
| 233 |
+
regression_id: string;
|
| 234 |
+
baseline_run_id: string;
|
| 235 |
+
candidate_run_id: string;
|
| 236 |
+
baseline_model: string;
|
| 237 |
+
candidate_model: string;
|
| 238 |
+
metric_deltas: Record<string, number>;
|
| 239 |
+
six_outcomes: Record<string, boolean>;
|
| 240 |
+
primary_outcome: string;
|
| 241 |
+
seqeval: Record<string, unknown> | null;
|
| 242 |
+
distribution_note: string;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
export interface RotrHITLReview {
|
| 246 |
+
review_id: string;
|
| 247 |
+
run_id: string;
|
| 248 |
+
violation_id: string;
|
| 249 |
+
cluster_id: string | null;
|
| 250 |
+
status: 'PENDING' | 'VALIDATED' | 'REJECTED';
|
| 251 |
+
action: string | null;
|
| 252 |
+
actor: string | null;
|
| 253 |
+
notes: string;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
export interface RotrTrainingCandidate {
|
| 257 |
+
candidate_id: string;
|
| 258 |
+
run_id: string;
|
| 259 |
+
violation_id: string;
|
| 260 |
+
cluster_id: string | null;
|
| 261 |
+
dataset_role: string;
|
| 262 |
+
training_eligible: boolean;
|
| 263 |
+
guard_state: string;
|
| 264 |
+
override: { actor: string; reason: string; at: string } | null;
|
| 265 |
+
studio2_entity_id?: string | null;
|
| 266 |
+
registry_backend?: string;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
export interface RotrSuite {
|
| 270 |
+
suite_id: string;
|
| 271 |
+
registry_backend: string;
|
| 272 |
+
members: {
|
| 273 |
+
candidate_id: string;
|
| 274 |
+
violation_id: string;
|
| 275 |
+
cluster_id: string | null;
|
| 276 |
+
run_id: string;
|
| 277 |
+
role: string;
|
| 278 |
+
added_at: string;
|
| 279 |
+
added_by: string | null;
|
| 280 |
+
}[];
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
export interface RotrStopshipPolicy {
|
| 284 |
+
policy: {
|
| 285 |
+
policy_name: string;
|
| 286 |
+
policy_semver: string;
|
| 287 |
+
conjunction: Record<string, unknown>;
|
| 288 |
+
notes: string;
|
| 289 |
+
};
|
| 290 |
+
policy_version: string;
|
| 291 |
+
note: string;
|
| 292 |
+
}
|