Add AI lab action guardrails
This commit is contained in:
@@ -1008,3 +1008,11 @@ Added:
|
|||||||
- Segmentation readiness now shows raster dataset, model availability and tile manifest provenance state before submitting a run.
|
- Segmentation readiness now shows raster dataset, model availability and tile manifest provenance state before submitting a run.
|
||||||
- Added regression coverage for the AI Lab readiness UI contract and styling.
|
- Added regression coverage for the AI Lab readiness UI contract and styling.
|
||||||
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
||||||
|
|
||||||
|
## Sprint 104 AI Lab action guardrails (2026-06-24)
|
||||||
|
|
||||||
|
- Added explicit action guardrails below Detection and Segmentation run-readiness panels.
|
||||||
|
- Detection now distinguishes configured model state from UI-runnable state and blocks the explicit test/demo-only fixture detector in the normal run form.
|
||||||
|
- Segmentation now distinguishes configured model state from UI-runnable state and blocks the explicit test/demo-only fixture segmenter in the normal run form.
|
||||||
|
- Added regression coverage for AI Lab action guardrails and compact guardrail styling.
|
||||||
|
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_lab_distinguishes_configured_model_from_ui_runnable_action() -> None:
|
||||||
|
lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "detectionModelUiRunnable" in lab
|
||||||
|
assert "selectedDetectionModelId !== 'manual-fixture-detector'" in lab
|
||||||
|
assert "detectionRunBlockedReason" in lab
|
||||||
|
assert "Fixture model is explicit test/demo-only" in lab
|
||||||
|
assert "Run action" in lab
|
||||||
|
assert "disabled={runningDetection || !detectionRunReady}" in lab
|
||||||
|
|
||||||
|
|
||||||
|
def test_segmentation_lab_distinguishes_configured_model_from_ui_runnable_action() -> None:
|
||||||
|
lab = (ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "segmentationModelUiRunnable" in lab
|
||||||
|
assert "selectedSegmentationModelId !== 'fixture-segmenter'" in lab
|
||||||
|
assert "segmentationRunBlockedReason" in lab
|
||||||
|
assert "Fixture segmenter is explicit test/demo-only" in lab
|
||||||
|
assert "Run action" in lab
|
||||||
|
assert "disabled={runningSegmentation || !segmentationRunReady}" in lab
|
||||||
|
|
||||||
|
|
||||||
|
def test_ai_lab_guardrail_styles_remain_compact() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".lab-action-guardrail" in css
|
||||||
|
assert ".lab-action-guardrail-ready" in css
|
||||||
|
assert "overflow-wrap: anywhere;" in css
|
||||||
@@ -3760,3 +3760,30 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Continue with V1 usability work that reduces operator confusion without expanding frozen product scope.
|
- Continue with V1 usability work that reduces operator confusion without expanding frozen product scope.
|
||||||
|
|
||||||
|
## Sprint 104 AI Lab action guardrails (2026-06-24)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added explicit action guardrails below the Detection Lab and Segmentation Lab run-readiness panels.
|
||||||
|
- Detection now distinguishes `configured` model registry state from UI-runnable action state, blocking the explicit test/demo-only `manual-fixture-detector` in the normal workbench run form.
|
||||||
|
- Segmentation now distinguishes `configured` model registry state from UI-runnable action state, blocking the explicit test/demo-only `fixture-segmenter` in the normal workbench run form.
|
||||||
|
- Updated run button disabled conditions to use the new readiness/action state.
|
||||||
|
- Added compact guardrail styling and regression coverage in `backend/tests/test_sprint104_ai_lab_action_guardrails.py`.
|
||||||
|
- Updated `CHANGELOG.md` and `docs/TODO.md`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend\tests\test_sprint104_ai_lab_action_guardrails.py -q` failed while the action guardrails and CSS contracts were absent.
|
||||||
|
- `python -m pytest backend\tests\test_sprint104_ai_lab_action_guardrails.py -q` (`3 passed`)
|
||||||
|
- `python -m pytest backend\tests\test_sprint103_ai_lab_run_readiness.py backend\tests\test_sprint104_ai_lab_action_guardrails.py -q` (`6 passed`)
|
||||||
|
- `python -m compileall backend/app`
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Full repository validation, commit, deploy and live browser verification are still pending for this pass.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Frontend action-guardrail guidance only; no backend API, persistence, migration, provider fetching, AI dependency or model execution behavior changed.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- After deploy validation, continue with V1 usability work that reduces operator confusion without expanding frozen product scope.
|
||||||
|
|||||||
@@ -370,3 +370,4 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Add useful default dataset context so Data, Map and Exports are immediately usable after project/demo load.
|
- [x] Add useful default dataset context so Data, Map and Exports are immediately usable after project/demo load.
|
||||||
- [x] Make raster tile handoff to Detection Lab auto-select the configured YOLO run form.
|
- [x] Make raster tile handoff to Detection Lab auto-select the configured YOLO run form.
|
||||||
- [x] Add AI Lab run-readiness checks for Detection and Segmentation before job submission.
|
- [x] Add AI Lab run-readiness checks for Detection and Segmentation before job submission.
|
||||||
|
- [x] Add AI Lab action guardrails so explicit fixture models are not exposed as normal operator runs.
|
||||||
|
|||||||
@@ -89,10 +89,24 @@ export function DetectionLab({
|
|||||||
const detectionHasDataset = selectedDetectionDatasetId.length > 0
|
const detectionHasDataset = selectedDetectionDatasetId.length > 0
|
||||||
const detectionHasModel = selectedDetectionModel !== null
|
const detectionHasModel = selectedDetectionModel !== null
|
||||||
const detectionModelReady = Boolean(selectedDetectionModel?.configured)
|
const detectionModelReady = Boolean(selectedDetectionModel?.configured)
|
||||||
|
const detectionModelUiRunnable = detectionModelReady && selectedDetectionModelId !== 'manual-fixture-detector'
|
||||||
const detectionHasTileManifest =
|
const detectionHasTileManifest =
|
||||||
!detectionRequiresTileManifest || detectionTileManifestPath.trim().length > 0
|
!detectionRequiresTileManifest || detectionTileManifestPath.trim().length > 0
|
||||||
const detectionRunReady =
|
const detectionRunReady =
|
||||||
Boolean(selectedProjectId) && detectionHasDataset && detectionHasModel && detectionModelReady && detectionHasTileManifest
|
Boolean(selectedProjectId) && detectionHasDataset && detectionHasModel && detectionModelUiRunnable && detectionHasTileManifest
|
||||||
|
const detectionRunBlockedReason = !selectedProjectId
|
||||||
|
? 'Select or create a project first'
|
||||||
|
: !detectionHasDataset
|
||||||
|
? 'Select a raster dataset'
|
||||||
|
: !detectionHasModel
|
||||||
|
? 'Select a detection model'
|
||||||
|
: selectedDetectionModelId === 'manual-fixture-detector'
|
||||||
|
? 'Fixture model is explicit test/demo-only'
|
||||||
|
: !detectionModelReady
|
||||||
|
? selectedDetectionModel?.limitation_message ?? 'Selected model is not configured'
|
||||||
|
: !detectionHasTileManifest
|
||||||
|
? 'Provide a raster tile manifest for configured YOLO'
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
||||||
@@ -191,6 +205,10 @@ export function DetectionLab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
|
||||||
|
<span>Run action</span>
|
||||||
|
<strong>{detectionRunReady ? 'Ready to submit a detection job' : detectionRunBlockedReason}</strong>
|
||||||
|
</div>
|
||||||
{rasterDatasets.length === 0 ? (
|
{rasterDatasets.length === 0 ? (
|
||||||
<div className="result-state result-state-empty">
|
<div className="result-state result-state-empty">
|
||||||
<strong>No raster datasets available for detection.</strong>
|
<strong>No raster datasets available for detection.</strong>
|
||||||
@@ -242,7 +260,7 @@ export function DetectionLab({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !selectedProjectId || rasterDatasets.length === 0}>
|
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||||
Run detection
|
Run detection
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,8 +90,19 @@ export function SegmentationLab({
|
|||||||
}: SegmentationLabProps): JSX.Element {
|
}: SegmentationLabProps): JSX.Element {
|
||||||
const segmentationHasDataset = selectedSegmentationDatasetId.length > 0
|
const segmentationHasDataset = selectedSegmentationDatasetId.length > 0
|
||||||
const segmentationHasTileManifest = segmentationTileManifestPath.trim().length > 0
|
const segmentationHasTileManifest = segmentationTileManifestPath.trim().length > 0
|
||||||
|
const segmentationModelUiRunnable =
|
||||||
|
selectedSegmentationModelConfigured && selectedSegmentationModelId !== 'fixture-segmenter'
|
||||||
const segmentationRunReady =
|
const segmentationRunReady =
|
||||||
Boolean(selectedProjectId) && segmentationHasDataset && selectedSegmentationModelConfigured
|
Boolean(selectedProjectId) && segmentationHasDataset && segmentationModelUiRunnable
|
||||||
|
const segmentationRunBlockedReason = !selectedProjectId
|
||||||
|
? 'Select or create a project first'
|
||||||
|
: !segmentationHasDataset
|
||||||
|
? 'Select a raster dataset'
|
||||||
|
: selectedSegmentationModelId === 'fixture-segmenter'
|
||||||
|
? 'Fixture segmenter is explicit test/demo-only'
|
||||||
|
: !selectedSegmentationModelConfigured
|
||||||
|
? selectedSegmentationModelLimitation ?? 'Selected segmentation model is not configured'
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="workspace-panel ai-lab-shell segmentation-lab-shell">
|
<section className="workspace-panel ai-lab-shell segmentation-lab-shell">
|
||||||
@@ -184,6 +195,10 @@ export function SegmentationLab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={segmentationRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
|
||||||
|
<span>Run action</span>
|
||||||
|
<strong>{segmentationRunReady ? 'Ready to submit a segmentation job' : segmentationRunBlockedReason}</strong>
|
||||||
|
</div>
|
||||||
{rasterDatasets.length === 0 ? (
|
{rasterDatasets.length === 0 ? (
|
||||||
<div className="result-state result-state-empty">
|
<div className="result-state result-state-empty">
|
||||||
<strong>No raster datasets available for segmentation.</strong>
|
<strong>No raster datasets available for segmentation.</strong>
|
||||||
@@ -237,7 +252,7 @@ export function SegmentationLab({
|
|||||||
className="primary-action"
|
className="primary-action"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRunSegmentation}
|
onClick={onRunSegmentation}
|
||||||
disabled={runningSegmentation || !selectedProjectId || rasterDatasets.length === 0 || !selectedSegmentationModelConfigured}
|
disabled={runningSegmentation || !segmentationRunReady}
|
||||||
>
|
>
|
||||||
Run segmentation
|
Run segmentation
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2719,6 +2719,40 @@ button.entity-card {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lab-action-guardrail {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
border: 1px solid #d8e3de;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.52rem 0.62rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-action-guardrail-ready {
|
||||||
|
border-color: #b8dcc9;
|
||||||
|
background: #f8fff9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-action-guardrail span {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-action-guardrail strong {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.28;
|
||||||
|
text-align: right;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-lab-shell .model-list {
|
.ai-lab-shell .model-list {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user