# Model And Reference Catalog Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a read-only local model asset catalog and make official reference providers visually distinct from AI model selection. **Architecture:** Model files remain runtime assets in the mounted model directory; persisted domain state remains in jobs, analysis runs, detections, datasets and vector features. Detection runs keep `model_id="yolo-configured"` and optionally include a validated `model_asset_id` resolved by a dedicated backend service. **Tech Stack:** FastAPI, Pydantic, SQLAlchemy, React, TypeScript, Vite, MapLibre, existing GeoIntel API envelope helpers. --- ## File Structure - Create `backend/app/services/model_asset_catalog_service.py`: read-only scanner and resolver for local model assets. - Modify `backend/app/core/config.py`: add `YOLO_MODELS_DIR` setting with safe default `/app/models`. - Modify `backend/app/schemas/detection.py`: add model asset response schemas and optional request fields. - Modify `backend/app/api/routes/detection.py`: add model asset endpoint and pass selected asset IDs to preflight/detection. - Modify `backend/app/services/detection_service.py`: resolve selected model asset into run-local YOLO settings and persist selection in parameters. - Modify `backend/app/services/yolo_preflight_service.py`: accept selected asset ID and preflight against that asset path. - Add `backend/tests/test_model_asset_catalog.py`: unit tests for scanning, resolving and envelope behavior. - Add or extend `backend/tests/test_sprint8b_yolo_foundation.py`: detection run parameter persistence for selected assets. - Modify `frontend/src/types.ts`: add model asset types and optional request fields. - Modify `frontend/src/services/api/detection.ts`: add model asset endpoint and preflight query param. - Modify `frontend/src/hooks/useDetectionWorkflow.ts`: load/select model assets and send selected asset ID. - Modify `frontend/src/components/detection/DetectionLab.tsx`: add local model asset picker and clearer model state. - Modify `frontend/src/components/providers/ProviderPanel.tsx`: sharpen reference-provider catalog copy. - Update docs listed in the design document. ## Task 1: Backend Catalog Tests **Files:** - Create: `backend/tests/test_model_asset_catalog.py` - [ ] **Step 1: Write catalog unit tests** ```python from pathlib import Path from app.core.config import Settings from app.services.model_asset_catalog_service import ModelAssetCatalogService def test_model_asset_catalog_lists_supported_files(tmp_path: Path) -> None: model_file = tmp_path / "building-detector.pt" model_file.write_bytes(b"local model") ignored_file = tmp_path / "notes.txt" ignored_file.write_text("ignore me", encoding="utf-8") settings = Settings(yolo_models_dir=str(tmp_path), yolo_model_path=str(model_file), yolo_enabled=True) response = ModelAssetCatalogService.list_assets(settings=settings) assert response.total == 1 asset = response.items[0] assert asset.filename == "building-detector.pt" assert asset.active is True assert asset.model_path == str(model_file) assert asset.size_bytes == len(b"local model") assert len(asset.sha256) == 64 assert asset.will_download_models is False def test_model_asset_catalog_resolves_known_asset(tmp_path: Path) -> None: model_file = tmp_path / "building-detector.pt" model_file.write_bytes(b"local model") settings = Settings(yolo_models_dir=str(tmp_path), yolo_enabled=True) asset = ModelAssetCatalogService.resolve_asset("building-detector-pt", settings=settings) assert asset.filename == "building-detector.pt" assert asset.model_path == str(model_file) def test_model_asset_catalog_rejects_unknown_asset(tmp_path: Path) -> None: settings = Settings(yolo_models_dir=str(tmp_path), yolo_enabled=True) try: ModelAssetCatalogService.resolve_asset("missing-model", settings=settings) except Exception as exc: assert getattr(exc, "code", "") == "DETECTION_MODEL_ASSET_NOT_FOUND" else: raise AssertionError("unknown model asset should fail") ``` - [ ] **Step 2: Run the tests to verify they fail before implementation** Run: ```bash cd backend python -m pytest tests/test_model_asset_catalog.py -q ``` Expected: import failure for `model_asset_catalog_service`. ## Task 2: Backend Catalog Implementation **Files:** - Create: `backend/app/services/model_asset_catalog_service.py` - Modify: `backend/app/core/config.py` - Modify: `backend/app/schemas/detection.py` - [ ] **Step 1: Add settings and schemas** Add `yolo_models_dir` to settings: ```python yolo_models_dir: str = Field(default="/app/models", validation_alias="YOLO_MODELS_DIR") ``` Add schemas: ```python class ModelAssetRead(BaseModel): model_asset_id: str filename: str display_name: str model_path: str suffix: str framework: str task_type: str size_bytes: int sha256: str active: bool status: str limitation_message: str will_download_models: bool = False class ModelAssetListResponse(BaseModel): items: list[ModelAssetRead] total: int model_directory: str ``` - [ ] **Step 2: Implement the scanner and resolver** Implement a service that: - resolves `YOLO_MODELS_DIR`; - scans `.pt`, `.onnx`, `.engine`; - uses deterministic slug IDs such as `building-detector-pt`; - computes SHA-256; - marks active file by comparing resolved paths; - raises `AppError(code="DETECTION_MODEL_ASSET_NOT_FOUND", status_code=404)` for unknown IDs. - [ ] **Step 3: Run focused tests** Run: ```bash cd backend python -m pytest tests/test_model_asset_catalog.py -q ``` Expected: pass. ## Task 3: API And Service Wiring **Files:** - Modify: `backend/app/api/routes/detection.py` - Modify: `backend/app/services/detection_service.py` - Modify: `backend/app/services/yolo_preflight_service.py` - Modify: `backend/app/schemas/detection.py` - Test: `backend/tests/test_model_asset_catalog.py` - [ ] **Step 1: Add endpoint test** Add a FastAPI test that calls `GET /api/v1/detection/model-assets` and asserts: ```python assert "data" in response.json() assert response.json()["data"]["total"] == 1 ``` - [ ] **Step 2: Add optional request fields** Extend `DetectionRunRequest` with: ```python model_asset_id: str | None = None ``` Extend preflight query handling with `model_asset_id: str | None = None`. - [ ] **Step 3: Wire endpoint** Add route: ```python @router.get("/model-assets", response_model=dict) def list_detection_model_assets() -> dict: return envelope(ModelAssetCatalogService.list_assets().model_dump()) ``` - [ ] **Step 4: Resolve selected asset in detection service** When `model_asset_id` is present and `model_id` equals the configured YOLO model ID: - resolve the asset through `ModelAssetCatalogService`; - copy settings with `yolo_model_path=asset.model_path`; - persist `model_asset_id`, `model_asset_path`, and `model_asset_sha256` in run parameters. - [ ] **Step 5: Resolve selected asset in preflight** When `model_asset_id` is present: - resolve the asset; - preflight against that asset path; - include `model_asset_id` in the response. - [ ] **Step 6: Run focused tests** Run: ```bash cd backend python -m pytest tests/test_model_asset_catalog.py tests/test_sprint8b_yolo_foundation.py -q ``` Expected: pass. ## Task 4: Frontend Model Asset Selection **Files:** - Modify: `frontend/src/types.ts` - Modify: `frontend/src/services/api/detection.ts` - Modify: `frontend/src/hooks/useDetectionWorkflow.ts` - Modify: `frontend/src/components/detection/DetectionLab.tsx` - [ ] **Step 1: Add frontend types and API call** Add: ```ts export interface ModelAssetRead { model_asset_id: string filename: string display_name: string model_path: string suffix: string framework: string task_type: string size_bytes: number sha256: string active: boolean status: string limitation_message: string will_download_models: boolean } export interface ModelAssetListResponse { items: ModelAssetRead[] total: number model_directory: string } ``` Add `model_asset_id?: string | null` to `DetectionRunRequest` and preflight params. - [ ] **Step 2: Add hook state** Add: ```ts const [modelAssets, setModelAssets] = useState([]) const [selectedModelAssetId, setSelectedModelAssetId] = useState('') const [modelAssetError, setModelAssetError] = useState(null) ``` Load assets alongside detection models and default to the active asset when one exists. - [ ] **Step 3: Send selected asset** Send `model_asset_id: selectedModelAssetId || null` in: - `getYoloPreflight`; - `run`. - [ ] **Step 4: Add UI picker** In `DetectionLab`, render a local model asset picker when `selectedDetectionModelId === "yolo-configured"`. Show: - filename; - active badge; - size in MB; - checksum prefix; - no-download warning. - [ ] **Step 5: Typecheck** Run: ```bash cd frontend npm run typecheck ``` Expected: pass. ## Task 5: Provider Catalog Clarity **Files:** - Modify: `frontend/src/components/providers/ProviderPanel.tsx` - [ ] **Step 1: Update copy and grouping** Keep existing provider cards but add a concise heading that states: - GRB/OSM are source capabilities, not model choices; - manual upload is the currently configured reference data path; - fixture is test/demo only. - [ ] **Step 2: Build** Run: ```bash cd frontend npm run build ``` Expected: pass. ## Task 6: Documentation **Files:** - Modify: `docs/API_CONTRACTS.md` - Modify: `docs/AI_PIPELINES.md` - Modify: `backend/README.md` - Modify: `frontend/README.md` - Modify: `docs/CODEX_EXECUTION_LOG.md` - Modify: `CHANGELOG.md` - Modify: `docs/TODO.md` - [ ] **Step 1: Document backend API** Document: - `GET /api/v1/detection/model-assets`; - optional `model_asset_id` on preflight and run; - no model downloads; - `YOLO_MODELS_DIR`. - [ ] **Step 2: Document UI behavior** Document: - model assets are local runtime files; - GRB/OSM are reference data providers; - manual upload remains the configured reference path. ## Task 7: Full Validation **Files:** none unless validation reveals a bug. - [ ] **Step 1: Run backend compile** ```bash python -m compileall backend/app ``` - [ ] **Step 2: Run backend tests** ```bash cd backend python -m pytest ``` - [ ] **Step 3: Run readiness** ```bash bash scripts/run_readiness_check.sh ``` - [ ] **Step 4: Run frontend typecheck and build** ```bash cd frontend npm run typecheck npm run build ``` - [ ] **Step 5: Run migration checks** ```bash cd backend python -m alembic heads python -m alembic upgrade head --sql bash ../scripts/live_migration_smoke.sh ``` - [ ] **Step 6: Runtime smoke after deploy** Call: ```bash curl http://192.168.10.150:1202/api/v1/detection/model-assets curl "http://192.168.10.150:1202/api/v1/detection/yolo/preflight?model_asset_id=&tile_manifest_path=" ``` Expected: canonical envelopes and no model downloads.