feat: Replace NHTSA VIN decode with Google Gemini via OCR service #223

Closed
opened 2026-02-19 03:11:02 +00:00 by egullickson · 5 comments
Owner

Summary

Replace the NHTSA vPIC API integration for VIN string decoding with Google Gemini, routed through the Python OCR microservice (mvp-ocr) to match the existing receipt/manual extraction architecture.

Current State

  • POST /api/vehicles/decode-vin calls NHTSA vPIC API directly from the TypeScript backend (vehicles/external/nhtsa/)
  • Returns vehicle details: year, make, model, trim, engine, transmission, bodyType, driveType, fuelType
  • Results cached in vin_cache database table (1-year TTL)
  • Pro/Enterprise tier gated (vehicle.vinDecode)
  • OCR image-based VIN extraction (POST /api/ocr/extract/vin) is separate and unchanged by this issue

Expected Behavior

  • VIN string decode uses Google Gemini (same workflow as receipt extraction)
  • Flow: Backend proxy -> Python OCR service -> Gemini -> structured vehicle data
  • Same endpoint and response shape for POST /api/vehicles/decode-vin
  • Keep vin_cache table for caching Gemini decode results
  • Remove all NHTSA vPIC API code and references from the codebase

Scope

Remove

  • vehicles/external/nhtsa/ directory (nhtsa.client.ts, nhtsa.types.ts, index.ts)
  • All NHTSA references in vehicles service, tests, and documentation
  • NHTSA references in platform feature documentation (planned fallback strategy)

Add/Modify

  • New VIN decode endpoint in Python OCR service using Gemini
  • Backend OCR client method for VIN string decode
  • Update vehicles service to route VIN decode through OCR proxy
  • Update tests for new flow
  • Update documentation (platform README, vehicles README, OCR README)

Acceptance Criteria

  • VIN decode returns same response shape (year, make, model, trim, etc. with confidence)
  • VIN decode routes through mvp-ocr Python service using Gemini
  • vin_cache table still caches decode results
  • Zero NHTSA references remain in the codebase
  • Existing OCR image-based VIN extraction (/api/ocr/extract/vin) unaffected
  • All tests pass
  • Tier gating unchanged (Pro/Enterprise)
## Summary Replace the NHTSA vPIC API integration for VIN string decoding with Google Gemini, routed through the Python OCR microservice (mvp-ocr) to match the existing receipt/manual extraction architecture. ## Current State - `POST /api/vehicles/decode-vin` calls NHTSA vPIC API directly from the TypeScript backend (`vehicles/external/nhtsa/`) - Returns vehicle details: year, make, model, trim, engine, transmission, bodyType, driveType, fuelType - Results cached in `vin_cache` database table (1-year TTL) - Pro/Enterprise tier gated (`vehicle.vinDecode`) - OCR image-based VIN extraction (`POST /api/ocr/extract/vin`) is separate and unchanged by this issue ## Expected Behavior - VIN string decode uses Google Gemini (same workflow as receipt extraction) - Flow: Backend proxy -> Python OCR service -> Gemini -> structured vehicle data - Same endpoint and response shape for `POST /api/vehicles/decode-vin` - Keep `vin_cache` table for caching Gemini decode results - Remove all NHTSA vPIC API code and references from the codebase ## Scope ### Remove - `vehicles/external/nhtsa/` directory (nhtsa.client.ts, nhtsa.types.ts, index.ts) - All NHTSA references in vehicles service, tests, and documentation - NHTSA references in platform feature documentation (planned fallback strategy) ### Add/Modify - New VIN decode endpoint in Python OCR service using Gemini - Backend OCR client method for VIN string decode - Update vehicles service to route VIN decode through OCR proxy - Update tests for new flow - Update documentation (platform README, vehicles README, OCR README) ## Acceptance Criteria - [ ] VIN decode returns same response shape (year, make, model, trim, etc. with confidence) - [ ] VIN decode routes through mvp-ocr Python service using Gemini - [ ] `vin_cache` table still caches decode results - [ ] Zero NHTSA references remain in the codebase - [ ] Existing OCR image-based VIN extraction (`/api/ocr/extract/vin`) unaffected - [ ] All tests pass - [ ] Tier gating unchanged (Pro/Enterprise)
egullickson added the
status
backlog
type
feature
labels 2026-02-19 03:11:06 +00:00
egullickson added
status
in-progress
and removed
status
backlog
labels 2026-02-19 03:13:34 +00:00
Author
Owner

Plan: Replace NHTSA VIN decode with Google Gemini via OCR service

Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW

Architecture Decision

Decision: Add decode_vin() method to existing GeminiEngine class (not a new standalone module).
Rationale: GeminiEngine already has lazy Vertex AI init, WIF credentials, and model setup. VIN decode is the same pattern (text-to-structured-data via Gemini). Duplicating init in a separate class adds complexity without SRP benefit at 2 methods. Decision Critic verdict: STAND (all claims verified, risks minor).

Current Flow

Frontend -> POST /api/vehicles/decode-vin -> VehiclesController
  -> NHTSAClient.decodeVin() -> NHTSA vPIC API (external)
  -> NHTSAClient cache (vin_cache table)
  -> VehiclesService.mapNHTSAResponse() -> dropdown matching
  -> DecodedVehicleData response

New Flow

Frontend -> POST /api/vehicles/decode-vin -> VehiclesController
  -> vin_cache check (moved to controller/service)
  -> OcrClient.decodeVin() -> POST /decode/vin on mvp-ocr
    -> GeminiEngine.decode_vin() -> Gemini 2.5 Flash (Vertex AI)
  -> cache Gemini response in vin_cache
  -> VehiclesService.mapVinDecodeResponse() -> dropdown matching
  -> DecodedVehicleData response (nhtsaValue renamed to sourceValue)

Key Changes

  • nhtsaValue renamed to sourceValue in MatchedField<T> (backend + frontend)
  • VIN validation regex moves from NHTSAClient to vehicles controller
  • Cache logic moves from NHTSAClient to vehicles service
  • mapNHTSAResponse() becomes mapVinDecodeResponse() - simpler since Gemini returns structured fields directly (no NHTSA array parsing)
  • Migration truncates vin_cache to clear NHTSA-format entries

Milestones

M1: OCR Python endpoint (#224)

Files: ocr/app/engines/gemini_engine.py, ocr/app/routers/extract.py, ocr/app/models/*.py

  • Add decode_vin(vin: str) to GeminiEngine with VIN-specific prompt and JSON response schema
  • Add VinDecodeResponse Pydantic model
  • Add POST /decode/vin route accepting {"vin": "..."} JSON body
  • VIN format validation (17 chars, regex ^[A-HJ-NPR-Z0-9]{17}$)
  • Returns: year, make, model, trimLevel, bodyType, driveType, fuelType, engine, transmission, confidence

M2: Backend types and OCR client (#225)

Files: backend/src/features/ocr/domain/ocr.types.ts, backend/src/features/ocr/external/ocr-client.ts, backend/src/features/vehicles/domain/vehicles.types.ts (new, extracted from nhtsa.types.ts)

  • Add VinDecodeResponse type matching OCR service response
  • Add decodeVin(vin: string) to OcrClient (JSON POST, not multipart)
  • Extract MatchedField<T>, DecodedVehicleData, DecodeVinRequest, VinDecodeError from nhtsa.types.ts into vehicles types
  • Rename nhtsaValue to sourceValue in MatchedField<T>

M3: Rewire vehicles controller (#226)

Files: backend/src/features/vehicles/api/vehicles.controller.ts, backend/src/features/vehicles/domain/vehicles.service.ts, backend/src/features/vehicles/api/vehicles.routes.ts, backend/src/features/vehicles/migrations/007_truncate_vin_cache.sql

  • Replace NHTSAClient import with OcrClient in controller
  • Move VIN validation into controller
  • Move cache read/write (getCached/saveToCache) into vehicles service
  • Refactor mapNHTSAResponse() to mapVinDecodeResponse() - takes flat Gemini fields, does dropdown matching
  • Add 007_truncate_vin_cache.sql migration
  • Update error handling references

M4: Remove NHTSA code and update docs (#227)

Files: Delete backend/src/features/vehicles/external/nhtsa/ (3 files), modify backend/src/features/platform/models/responses.ts, backend/src/features/platform/README.md, 5 CLAUDE.md files

  • Delete entire vehicles/external/nhtsa/ directory
  • Remove VPICVariable, VPICResponse from platform models
  • Update NHTSA references in platform README
  • Update all affected CLAUDE.md navigation indexes

M5: Frontend and tests (#228)

Files: frontend/src/features/vehicles/types/vehicles.types.ts, frontend/src/features/vehicles/components/VinOcrReviewModal.tsx, guide page sections, test files

  • Rename nhtsaValue to sourceValue in frontend types
  • Update VinOcrReviewModal references
  • Update NHTSA comments in guide sections
  • Update integration/unit tests for new OCR-based flow
  • Verify existing OCR image VIN extraction unaffected

Risk Assessment

  • Cache migration: TRUNCATE is safe - cache is rebuild-on-miss, no data loss
  • Gemini accuracy: VIN decode is well-understood domain, Gemini handles it reliably
  • Response shape: Same structure preserved, only field rename (nhtsaValue -> sourceValue)
  • Existing OCR VIN extraction: Image-based /api/ocr/extract/vin endpoint is completely separate and unaffected

Files Summary

Action Count Details
DELETE 3 vehicles/external/nhtsa/ (index.ts, nhtsa.client.ts, nhtsa.types.ts)
NEW 2 vehicles.types.ts (extracted), 007_truncate_vin_cache.sql
MODIFY ~15 OCR engine, router, models, OcrClient, controller, service, routes, platform models/docs, CLAUDE.md files, frontend types/components/tests

Verdict: AWAITING_REVIEW | Next: Plan review cycle (QR plan-completeness)

## Plan: Replace NHTSA VIN decode with Google Gemini via OCR service **Phase**: Planning | **Agent**: Planner | **Status**: AWAITING_REVIEW ### Architecture Decision **Decision**: Add `decode_vin()` method to existing `GeminiEngine` class (not a new standalone module). **Rationale**: GeminiEngine already has lazy Vertex AI init, WIF credentials, and model setup. VIN decode is the same pattern (text-to-structured-data via Gemini). Duplicating init in a separate class adds complexity without SRP benefit at 2 methods. Decision Critic verdict: STAND (all claims verified, risks minor). ### Current Flow ``` Frontend -> POST /api/vehicles/decode-vin -> VehiclesController -> NHTSAClient.decodeVin() -> NHTSA vPIC API (external) -> NHTSAClient cache (vin_cache table) -> VehiclesService.mapNHTSAResponse() -> dropdown matching -> DecodedVehicleData response ``` ### New Flow ``` Frontend -> POST /api/vehicles/decode-vin -> VehiclesController -> vin_cache check (moved to controller/service) -> OcrClient.decodeVin() -> POST /decode/vin on mvp-ocr -> GeminiEngine.decode_vin() -> Gemini 2.5 Flash (Vertex AI) -> cache Gemini response in vin_cache -> VehiclesService.mapVinDecodeResponse() -> dropdown matching -> DecodedVehicleData response (nhtsaValue renamed to sourceValue) ``` ### Key Changes - `nhtsaValue` renamed to `sourceValue` in `MatchedField<T>` (backend + frontend) - VIN validation regex moves from NHTSAClient to vehicles controller - Cache logic moves from NHTSAClient to vehicles service - `mapNHTSAResponse()` becomes `mapVinDecodeResponse()` - simpler since Gemini returns structured fields directly (no NHTSA array parsing) - Migration truncates `vin_cache` to clear NHTSA-format entries ### Milestones #### M1: OCR Python endpoint (#224) **Files**: `ocr/app/engines/gemini_engine.py`, `ocr/app/routers/extract.py`, `ocr/app/models/*.py` - Add `decode_vin(vin: str)` to GeminiEngine with VIN-specific prompt and JSON response schema - Add `VinDecodeResponse` Pydantic model - Add `POST /decode/vin` route accepting `{"vin": "..."}` JSON body - VIN format validation (17 chars, regex `^[A-HJ-NPR-Z0-9]{17}$`) - Returns: year, make, model, trimLevel, bodyType, driveType, fuelType, engine, transmission, confidence #### M2: Backend types and OCR client (#225) **Files**: `backend/src/features/ocr/domain/ocr.types.ts`, `backend/src/features/ocr/external/ocr-client.ts`, `backend/src/features/vehicles/domain/vehicles.types.ts` (new, extracted from nhtsa.types.ts) - Add `VinDecodeResponse` type matching OCR service response - Add `decodeVin(vin: string)` to `OcrClient` (JSON POST, not multipart) - Extract `MatchedField<T>`, `DecodedVehicleData`, `DecodeVinRequest`, `VinDecodeError` from nhtsa.types.ts into vehicles types - Rename `nhtsaValue` to `sourceValue` in `MatchedField<T>` #### M3: Rewire vehicles controller (#226) **Files**: `backend/src/features/vehicles/api/vehicles.controller.ts`, `backend/src/features/vehicles/domain/vehicles.service.ts`, `backend/src/features/vehicles/api/vehicles.routes.ts`, `backend/src/features/vehicles/migrations/007_truncate_vin_cache.sql` - Replace NHTSAClient import with OcrClient in controller - Move VIN validation into controller - Move cache read/write (getCached/saveToCache) into vehicles service - Refactor `mapNHTSAResponse()` to `mapVinDecodeResponse()` - takes flat Gemini fields, does dropdown matching - Add `007_truncate_vin_cache.sql` migration - Update error handling references #### M4: Remove NHTSA code and update docs (#227) **Files**: Delete `backend/src/features/vehicles/external/nhtsa/` (3 files), modify `backend/src/features/platform/models/responses.ts`, `backend/src/features/platform/README.md`, 5 CLAUDE.md files - Delete entire `vehicles/external/nhtsa/` directory - Remove `VPICVariable`, `VPICResponse` from platform models - Update NHTSA references in platform README - Update all affected CLAUDE.md navigation indexes #### M5: Frontend and tests (#228) **Files**: `frontend/src/features/vehicles/types/vehicles.types.ts`, `frontend/src/features/vehicles/components/VinOcrReviewModal.tsx`, guide page sections, test files - Rename `nhtsaValue` to `sourceValue` in frontend types - Update VinOcrReviewModal references - Update NHTSA comments in guide sections - Update integration/unit tests for new OCR-based flow - Verify existing OCR image VIN extraction unaffected ### Risk Assessment - **Cache migration**: TRUNCATE is safe - cache is rebuild-on-miss, no data loss - **Gemini accuracy**: VIN decode is well-understood domain, Gemini handles it reliably - **Response shape**: Same structure preserved, only field rename (`nhtsaValue` -> `sourceValue`) - **Existing OCR VIN extraction**: Image-based `/api/ocr/extract/vin` endpoint is completely separate and unaffected ### Files Summary | Action | Count | Details | |--------|-------|---------| | DELETE | 3 | `vehicles/external/nhtsa/` (index.ts, nhtsa.client.ts, nhtsa.types.ts) | | NEW | 2 | vehicles.types.ts (extracted), 007_truncate_vin_cache.sql | | MODIFY | ~15 | OCR engine, router, models, OcrClient, controller, service, routes, platform models/docs, CLAUDE.md files, frontend types/components/tests | *Verdict*: AWAITING_REVIEW | *Next*: Plan review cycle (QR plan-completeness)
Author
Owner

Plan v2: Replace NHTSA VIN decode with Google Gemini via OCR service

Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW

Revised based on QR plan-completeness FAIL and TW plan-scrub FAIL. Addresses: response schema, sequencing conflict, incomplete file lists, cache type disposition, prompt design, route registration.

Architecture Decision

Decision: Add decode_vin() method to existing GeminiEngine class.
Rationale: Reuses lazy Vertex AI init and WIF credentials. Decision Critic verdict: STAND.

New Flow

Frontend -> POST /api/vehicles/decode-vin -> VehiclesController
  -> vin_cache check (in VehiclesService)
  -> OcrClient.decodeVin(vin) -> POST /decode/vin on mvp-ocr
    -> GeminiEngine.decode_vin(vin) -> Gemini 2.5 Flash
  -> VehiclesService.saveVinCache(vin, response)
  -> VehiclesService.mapVinDecodeResponse(response) -> dropdown matching
  -> DecodedVehicleData response (sourceValue field)

OCR VIN Decode Response Schema (drives M1 and M2)

{
  "success": true,
  "vin": "1HGBH41JXMN109186",
  "year": 2020,
  "make": "Toyota",
  "model": "Camry",
  "trimLevel": "SE",
  "bodyType": "Sedan 4-Door",
  "driveType": "Front-Wheel Drive",
  "fuelType": "Gasoline",
  "engine": "2.5L 4-Cylinder",
  "transmission": "8-Speed Automatic",
  "confidence": 0.95,
  "processingTimeMs": 1200,
  "error": null
}

Gemini Prompt Design (M1)

Given the VIN (Vehicle Identification Number) below, decode it and return the vehicle specifications.

VIN: {vin}

Return the vehicle's year, make, model, trim level, body type, drive type, fuel type, engine description, and transmission type. If a field cannot be determined from the VIN, return null for that field. Return a confidence score (0.0-1.0) indicating overall decode reliability.

Response schema enforced via GenerationConfig(response_mime_type="application/json", response_schema=_VIN_DECODE_SCHEMA).


Milestones

M1: OCR Python VIN decode endpoint (#224)

Files:

  • ocr/app/engines/gemini_engine.py - add decode_vin(vin: str) method with _VIN_DECODE_PROMPT and _VIN_DECODE_SCHEMA
  • ocr/app/routers/decode.py (NEW) - new router with prefix /decode, endpoint POST /vin
  • ocr/app/routers/__init__.py - export decode_router
  • ocr/app/main.py - register decode_router
  • ocr/app/models/__init__.py (or models file) - add VinDecodeResponse Pydantic model
  • ocr/tests/test_vin_decode.py (NEW) - unit test for /decode/vin route

Implementation details:

  • decode_vin() calls model.generate_content([prompt_text], generation_config=vin_config) - text-only input, no Part.from_data
  • Separate _generation_config per method (maintenance schema vs VIN schema), stored as instance vars on first use
  • VIN format validation in the route handler: regex ^[A-HJ-NPR-Z0-9]{17}$, return 400 on failure
  • Route returns VinDecodeResponse Pydantic model matching the schema above

M2: Backend OCR client VIN decode method (#225)

Files:

  • backend/src/features/ocr/domain/ocr.types.ts - add VinDecodeResponse type
  • backend/src/features/ocr/external/ocr-client.ts - add decodeVin(vin: string) method

Implementation details:

  • OcrClient.decodeVin() uses JSON POST (not multipart): fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({vin}) })
  • Returns VinDecodeResponse matching the Python schema above
  • This is a departure from existing OcrClient methods (which use multipart) - this method sends JSON because VIN decode has no file upload

Not in this milestone: Type extraction and nhtsaValue rename moved to M3 to avoid sequencing breakage.

M3: Rewire vehicles controller and service (#226)

Files:

  • backend/src/features/vehicles/api/vehicles.controller.ts - replace NHTSAClient with OcrClient, move VIN regex validation here
  • backend/src/features/vehicles/domain/vehicles.service.ts - add getVinCached(), saveVinCache(), rename mapNHTSAResponse() to mapVinDecodeResponse(response: VinDecodeResponse), rename nhtsaValue to sourceValue in matchField() and all MatchedField construction
  • backend/src/features/vehicles/domain/vehicles.types.ts (existing file) - add MatchedField<T>, MatchConfidence, DecodedVehicleData, DecodeVinRequest, VinDecodeError, VinCacheEntry (all extracted from nhtsa.types.ts with nhtsaValue -> sourceValue rename and rawData type changed to VinDecodeResponse)
  • backend/src/features/vehicles/api/vehicles.routes.ts - update comment from "NHTSA vPIC API" to "OCR VIN decode"
  • backend/src/features/vehicles/migrations/007_truncate_vin_cache.sql - TRUNCATE TABLE vin_cache;

Implementation details:

  • Controller decodeVin() method flow:
    1. Validate VIN format (regex ^[A-HJ-NPR-Z0-9]{17}$), return 400 if invalid
    2. Call vehiclesService.getVinCached(vin) - if hit, return cached DecodedVehicleData
    3. Call ocrClient.decodeVin(vin) to get VinDecodeResponse
    4. Call vehiclesService.saveVinCache(vin, response) to cache
    5. Call vehiclesService.mapVinDecodeResponse(response) for dropdown matching
    6. Return DecodedVehicleData
  • mapVinDecodeResponse(response: VinDecodeResponse): Promise<DecodedVehicleData>:
    • Reads flat fields directly: response.year, response.make, etc. (no NHTSAClient.extractValue calls)
    • matchField() logic unchanged (fuzzy matching against dropdowns)
    • All MatchedField objects use sourceValue instead of nhtsaValue
  • VinCacheEntry.rawData type changes from NHTSADecodeResponse to VinDecodeResponse
  • saveVinCache() stores Gemini response as raw_data JSONB
  • getVinCached() returns cached VinDecodeResponse from raw_data, passes through mapVinDecodeResponse() if cache hit (so dropdown matching always runs fresh)
  • Error handling: replace "NHTSA API request timed out" with "VIN decode service timed out", replace "NHTSA" error check with "OCR" error check

M4: Remove NHTSA code and update docs (#227)

Files to DELETE:

  • backend/src/features/vehicles/external/nhtsa/index.ts
  • backend/src/features/vehicles/external/nhtsa/nhtsa.client.ts
  • backend/src/features/vehicles/external/nhtsa/nhtsa.types.ts

Files to MODIFY:

  • backend/src/features/platform/models/responses.ts - remove VPICVariable and VPICResponse interfaces (lines 65-79). Verified: no other files import these types.
  • backend/src/features/platform/README.md - remove NHTSA vPIC fallback strategy section, update VIN decode references to point to OCR service
  • backend/src/features/vehicles/README.md - update VIN decode section from NHTSA to Gemini/OCR
  • backend/src/features/vehicles/CLAUDE.md - change external/ description from "External service integrations (NHTSA)" to "External service integrations"
  • backend/src/features/vehicles/external/CLAUDE.md - remove nhtsa/ directory entry
  • ocr/app/engines/CLAUDE.md - add VIN decode to GeminiEngine description
  • ocr/CLAUDE.md - add "Gemini VIN decode" to service description
  • ocr/app/CLAUDE.md - add decode router to subdirectories
  • backend/src/features/ocr/CLAUDE.md - add decodeVin to OcrClient method list

M5: Frontend updates and tests (#228)

Frontend files:

  • frontend/src/features/vehicles/types/vehicles.types.ts - rename nhtsaValue to sourceValue in MatchedField<T>, update DecodedVehicleData comment from "NHTSA vPIC API" to "VIN decode"
  • frontend/src/features/vehicles/components/VinOcrReviewModal.tsx - rename nhtsaValue references (lines 114-118) to sourceValue, update nhtsaRefs variable name to sourceRefs
  • frontend/src/features/vehicles/hooks/useVinOcr.ts - update step 2 label from "Decode VIN using NHTSA" to "Decode VIN", update error messages removing NHTSA
  • frontend/src/features/vehicles/api/vehicles.api.ts - update JSDoc from "NHTSA vPIC API" to "VIN decode service"
  • frontend/src/features/vehicles/components/VehicleForm.tsx - update NHTSA comment (line 510)
  • frontend/src/pages/GuidePage/sections/VehiclesSection.tsx - change "NHTSA database" to "vehicle database"
  • frontend/src/pages/GuidePage/sections/SubscriptionSection.tsx - change "NHTSA database" to "vehicle database"

Test files:

  • backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts - update vin_cache references if needed
  • Add unit tests for VehiclesService.mapVinDecodeResponse() mocking OcrClient
  • Add unit test for OcrClient.decodeVin() method

Regression verification:

  • Confirm POST /api/ocr/extract/vin (image-based VIN extraction) still works - run existing OCR tests, verify PaddleOCR path untouched

Milestone Dependencies

M1 (OCR endpoint) -> M2 (backend client) -> M3 (rewire controller + types + rename)
                                                -> M4 (delete NHTSA, must happen after M3 moves all code)
                                                -> M5 (frontend + tests, after M3 stabilizes API)

Files Summary

Action Count Details
DELETE 3 vehicles/external/nhtsa/ directory
NEW 3 ocr/app/routers/decode.py, ocr/tests/test_vin_decode.py, 007_truncate_vin_cache.sql
MODIFY ~20 OCR engine/models/main/routers, OcrClient/types, vehicles controller/service/types/routes, platform models/README, 8 CLAUDE.md files, 7 frontend files, test files

Verdict: AWAITING_REVIEW | Next: QR plan-code, QR plan-docs

## Plan v2: Replace NHTSA VIN decode with Google Gemini via OCR service **Phase**: Planning | **Agent**: Planner | **Status**: AWAITING_REVIEW *Revised based on QR plan-completeness FAIL and TW plan-scrub FAIL. Addresses: response schema, sequencing conflict, incomplete file lists, cache type disposition, prompt design, route registration.* ### Architecture Decision **Decision**: Add `decode_vin()` method to existing `GeminiEngine` class. **Rationale**: Reuses lazy Vertex AI init and WIF credentials. Decision Critic verdict: STAND. ### New Flow ``` Frontend -> POST /api/vehicles/decode-vin -> VehiclesController -> vin_cache check (in VehiclesService) -> OcrClient.decodeVin(vin) -> POST /decode/vin on mvp-ocr -> GeminiEngine.decode_vin(vin) -> Gemini 2.5 Flash -> VehiclesService.saveVinCache(vin, response) -> VehiclesService.mapVinDecodeResponse(response) -> dropdown matching -> DecodedVehicleData response (sourceValue field) ``` ### OCR VIN Decode Response Schema (drives M1 and M2) ```json { "success": true, "vin": "1HGBH41JXMN109186", "year": 2020, "make": "Toyota", "model": "Camry", "trimLevel": "SE", "bodyType": "Sedan 4-Door", "driveType": "Front-Wheel Drive", "fuelType": "Gasoline", "engine": "2.5L 4-Cylinder", "transmission": "8-Speed Automatic", "confidence": 0.95, "processingTimeMs": 1200, "error": null } ``` ### Gemini Prompt Design (M1) ``` Given the VIN (Vehicle Identification Number) below, decode it and return the vehicle specifications. VIN: {vin} Return the vehicle's year, make, model, trim level, body type, drive type, fuel type, engine description, and transmission type. If a field cannot be determined from the VIN, return null for that field. Return a confidence score (0.0-1.0) indicating overall decode reliability. ``` Response schema enforced via `GenerationConfig(response_mime_type="application/json", response_schema=_VIN_DECODE_SCHEMA)`. --- ### Milestones #### M1: OCR Python VIN decode endpoint (#224) **Files**: - `ocr/app/engines/gemini_engine.py` - add `decode_vin(vin: str)` method with `_VIN_DECODE_PROMPT` and `_VIN_DECODE_SCHEMA` - `ocr/app/routers/decode.py` (NEW) - new router with prefix `/decode`, endpoint `POST /vin` - `ocr/app/routers/__init__.py` - export `decode_router` - `ocr/app/main.py` - register `decode_router` - `ocr/app/models/__init__.py` (or models file) - add `VinDecodeResponse` Pydantic model - `ocr/tests/test_vin_decode.py` (NEW) - unit test for `/decode/vin` route **Implementation details**: - `decode_vin()` calls `model.generate_content([prompt_text], generation_config=vin_config)` - text-only input, no `Part.from_data` - Separate `_generation_config` per method (maintenance schema vs VIN schema), stored as instance vars on first use - VIN format validation in the route handler: regex `^[A-HJ-NPR-Z0-9]{17}$`, return 400 on failure - Route returns `VinDecodeResponse` Pydantic model matching the schema above #### M2: Backend OCR client VIN decode method (#225) **Files**: - `backend/src/features/ocr/domain/ocr.types.ts` - add `VinDecodeResponse` type - `backend/src/features/ocr/external/ocr-client.ts` - add `decodeVin(vin: string)` method **Implementation details**: - `OcrClient.decodeVin()` uses JSON POST (not multipart): `fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({vin}) })` - Returns `VinDecodeResponse` matching the Python schema above - This is a departure from existing OcrClient methods (which use multipart) - this method sends JSON because VIN decode has no file upload **Not in this milestone**: Type extraction and nhtsaValue rename moved to M3 to avoid sequencing breakage. #### M3: Rewire vehicles controller and service (#226) **Files**: - `backend/src/features/vehicles/api/vehicles.controller.ts` - replace NHTSAClient with OcrClient, move VIN regex validation here - `backend/src/features/vehicles/domain/vehicles.service.ts` - add `getVinCached()`, `saveVinCache()`, rename `mapNHTSAResponse()` to `mapVinDecodeResponse(response: VinDecodeResponse)`, rename `nhtsaValue` to `sourceValue` in `matchField()` and all MatchedField construction - `backend/src/features/vehicles/domain/vehicles.types.ts` (existing file) - add `MatchedField<T>`, `MatchConfidence`, `DecodedVehicleData`, `DecodeVinRequest`, `VinDecodeError`, `VinCacheEntry` (all extracted from nhtsa.types.ts with `nhtsaValue` -> `sourceValue` rename and `rawData` type changed to `VinDecodeResponse`) - `backend/src/features/vehicles/api/vehicles.routes.ts` - update comment from "NHTSA vPIC API" to "OCR VIN decode" - `backend/src/features/vehicles/migrations/007_truncate_vin_cache.sql` - `TRUNCATE TABLE vin_cache;` **Implementation details**: - Controller `decodeVin()` method flow: 1. Validate VIN format (regex `^[A-HJ-NPR-Z0-9]{17}$`), return 400 if invalid 2. Call `vehiclesService.getVinCached(vin)` - if hit, return cached `DecodedVehicleData` 3. Call `ocrClient.decodeVin(vin)` to get `VinDecodeResponse` 4. Call `vehiclesService.saveVinCache(vin, response)` to cache 5. Call `vehiclesService.mapVinDecodeResponse(response)` for dropdown matching 6. Return `DecodedVehicleData` - `mapVinDecodeResponse(response: VinDecodeResponse): Promise<DecodedVehicleData>`: - Reads flat fields directly: `response.year`, `response.make`, etc. (no NHTSAClient.extractValue calls) - `matchField()` logic unchanged (fuzzy matching against dropdowns) - All `MatchedField` objects use `sourceValue` instead of `nhtsaValue` - `VinCacheEntry.rawData` type changes from `NHTSADecodeResponse` to `VinDecodeResponse` - `saveVinCache()` stores Gemini response as `raw_data` JSONB - `getVinCached()` returns cached `VinDecodeResponse` from `raw_data`, passes through `mapVinDecodeResponse()` if cache hit (so dropdown matching always runs fresh) - Error handling: replace "NHTSA API request timed out" with "VIN decode service timed out", replace "NHTSA" error check with "OCR" error check #### M4: Remove NHTSA code and update docs (#227) **Files to DELETE**: - `backend/src/features/vehicles/external/nhtsa/index.ts` - `backend/src/features/vehicles/external/nhtsa/nhtsa.client.ts` - `backend/src/features/vehicles/external/nhtsa/nhtsa.types.ts` **Files to MODIFY**: - `backend/src/features/platform/models/responses.ts` - remove `VPICVariable` and `VPICResponse` interfaces (lines 65-79). Verified: no other files import these types. - `backend/src/features/platform/README.md` - remove NHTSA vPIC fallback strategy section, update VIN decode references to point to OCR service - `backend/src/features/vehicles/README.md` - update VIN decode section from NHTSA to Gemini/OCR - `backend/src/features/vehicles/CLAUDE.md` - change `external/` description from "External service integrations (NHTSA)" to "External service integrations" - `backend/src/features/vehicles/external/CLAUDE.md` - remove `nhtsa/` directory entry - `ocr/app/engines/CLAUDE.md` - add VIN decode to GeminiEngine description - `ocr/CLAUDE.md` - add "Gemini VIN decode" to service description - `ocr/app/CLAUDE.md` - add decode router to subdirectories - `backend/src/features/ocr/CLAUDE.md` - add `decodeVin` to OcrClient method list #### M5: Frontend updates and tests (#228) **Frontend files**: - `frontend/src/features/vehicles/types/vehicles.types.ts` - rename `nhtsaValue` to `sourceValue` in `MatchedField<T>`, update `DecodedVehicleData` comment from "NHTSA vPIC API" to "VIN decode" - `frontend/src/features/vehicles/components/VinOcrReviewModal.tsx` - rename `nhtsaValue` references (lines 114-118) to `sourceValue`, update `nhtsaRefs` variable name to `sourceRefs` - `frontend/src/features/vehicles/hooks/useVinOcr.ts` - update step 2 label from "Decode VIN using NHTSA" to "Decode VIN", update error messages removing NHTSA - `frontend/src/features/vehicles/api/vehicles.api.ts` - update JSDoc from "NHTSA vPIC API" to "VIN decode service" - `frontend/src/features/vehicles/components/VehicleForm.tsx` - update NHTSA comment (line 510) - `frontend/src/pages/GuidePage/sections/VehiclesSection.tsx` - change "NHTSA database" to "vehicle database" - `frontend/src/pages/GuidePage/sections/SubscriptionSection.tsx` - change "NHTSA database" to "vehicle database" **Test files**: - `backend/src/features/vehicles/tests/integration/vehicles.integration.test.ts` - update vin_cache references if needed - Add unit tests for `VehiclesService.mapVinDecodeResponse()` mocking OcrClient - Add unit test for `OcrClient.decodeVin()` method **Regression verification**: - Confirm `POST /api/ocr/extract/vin` (image-based VIN extraction) still works - run existing OCR tests, verify PaddleOCR path untouched ### Milestone Dependencies ``` M1 (OCR endpoint) -> M2 (backend client) -> M3 (rewire controller + types + rename) -> M4 (delete NHTSA, must happen after M3 moves all code) -> M5 (frontend + tests, after M3 stabilizes API) ``` ### Files Summary | Action | Count | Details | |--------|-------|---------| | DELETE | 3 | `vehicles/external/nhtsa/` directory | | NEW | 3 | `ocr/app/routers/decode.py`, `ocr/tests/test_vin_decode.py`, `007_truncate_vin_cache.sql` | | MODIFY | ~20 | OCR engine/models/main/routers, OcrClient/types, vehicles controller/service/types/routes, platform models/README, 8 CLAUDE.md files, 7 frontend files, test files | *Verdict*: AWAITING_REVIEW | *Next*: QR plan-code, QR plan-docs
Author
Owner

Plan v3: Revisions from QR plan-code and QR plan-docs

Phase: Plan-Review | Agent: Planner | Status: APPROVED

Addresses 4 RULE 0/1 findings from QR plan-code and 7 missing files from QR plan-docs.


Changes from v2

RULE 0 FIX: Remove TRUNCATE migration, use format-aware cache reads

Problem: TRUNCATE vin_cache is destructive with no rollback if Gemini is unavailable post-deploy.

Fix: Remove 007_truncate_vin_cache.sql. Instead, getVinCached() validates that raw_data has a success field (present in VinDecodeResponse, absent in old NHTSADecodeResponse). If format mismatch, treat as cache miss. Old NHTSA entries expire naturally via 1-year TTL. No destructive migration needed.

RULE 0 FIX: OCR decode router error status codes

Problem: No specified HTTP status for Gemini unavailability vs processing error.

Fix: OCR /decode/vin router returns:

  • 400 for invalid VIN format
  • 422 for GeminiProcessingError (Gemini returned bad data)
  • 503 for GeminiUnavailableError (credentials missing, SDK not installed)

Backend OcrClient.decodeVin() propagates these status codes. Controller maps: 503 -> 502 "VIN decode service unavailable", 422 -> 502 "VIN decode failed", 400 -> 400 "Invalid VIN".

RULE 0 FIX: Explicit VIN validation before cache/OCR

Problem: Gemini will hallucinate data for malformed VINs.

Fix: Controller decodeVin() step 1 explicitly validates VIN format using regex ^[A-HJ-NPR-Z0-9]{17}$ BEFORE cache read and before OCR call. Returns 400 immediately on failure. This matches the existing NHTSAClient.validateVin() behavior.

RULE 0 FIX: Cache write uses ON CONFLICT upsert

Fix: saveVinCache() uses INSERT INTO vin_cache ... ON CONFLICT (vin) DO UPDATE - same idempotent pattern as existing NHTSAClient.saveToCache().

RULE 1 FIX: Type transition safety

Problem: VinCacheEntry.rawData type change could break at milestone boundaries.

Fix: M3 is atomic - it extracts types from nhtsa.types.ts, changes rawData type, AND rewires the controller/service in a single milestone. The old NHTSAClient code continues to exist (deleted in M4) but is no longer imported by any file after M3 completes. No intermediate broken state.

RULE 2 NOTE: GenerationConfig per method

Fix: decode_vin() creates GenerationConfig locally within the method body (not cached as instance state). The existing self._generation_config for maintenance extraction is untouched.

RULE 2 NOTE: matchField() reuse confirmed

mapVinDecodeResponse() delegates to the same private matchField() method. No new fuzzy matching logic.


Additional files added to M3 (config/code)

These files have active vpic/NHTSA references that are functional, not just documentation:

  • backend/src/core/config/config-loader.ts - remove vpic from Zod schema in external config object (lines 46-48). RULE 0: failing to remove this causes config validation error at startup when vpic config is removed from YAML files
  • backend/src/core/config/feature-tiers.ts - update upgradePrompt from "NHTSA database" to "vehicle database" (line 32)
  • config/app/ci.yml - remove vpic: block (lines 26-28)
  • config/app/production.yml.example - remove vpic_api_url (line 25)
  • config/shared/production.yml - remove vpic: block (lines 110-111)

Additional files added to M4 (docs)

  • docs/PLATFORM-SERVICES.md - update "NHTSA vPIC API fallback with circuit breaker" to "Gemini VIN decode via OCR service" (line 38)
  • docs/USER-GUIDE.md - update "NHTSA database" references to "vehicle database" (lines 647, 658)
  • docs/TESTING.md - update vPIC mock references to OCR service mocks (lines 77, 197, 322-323)
  • backend/src/features/vehicles/external/README.md - remove NHTSAClient pattern reference (line 18)

Updated milestone file counts

Milestone Files (v2) Files (v3) Delta
M1 (#224) 6 6 --
M2 (#225) 2 2 --
M3 (#226) 5 10 +5 (config files)
M4 (#227) 12 16 +4 (docs files)
M5 (#228) ~12 ~12 --

Removed from plan

  • 007_truncate_vin_cache.sql - no longer needed (format-aware cache reads instead)

Verdict: APPROVED | Next: Create branch, begin execution

## Plan v3: Revisions from QR plan-code and QR plan-docs **Phase**: Plan-Review | **Agent**: Planner | **Status**: APPROVED *Addresses 4 RULE 0/1 findings from QR plan-code and 7 missing files from QR plan-docs.* --- ### Changes from v2 #### RULE 0 FIX: Remove TRUNCATE migration, use format-aware cache reads **Problem**: TRUNCATE vin_cache is destructive with no rollback if Gemini is unavailable post-deploy. **Fix**: Remove `007_truncate_vin_cache.sql`. Instead, `getVinCached()` validates that `raw_data` has a `success` field (present in `VinDecodeResponse`, absent in old `NHTSADecodeResponse`). If format mismatch, treat as cache miss. Old NHTSA entries expire naturally via 1-year TTL. No destructive migration needed. #### RULE 0 FIX: OCR decode router error status codes **Problem**: No specified HTTP status for Gemini unavailability vs processing error. **Fix**: OCR `/decode/vin` router returns: - `400` for invalid VIN format - `422` for `GeminiProcessingError` (Gemini returned bad data) - `503` for `GeminiUnavailableError` (credentials missing, SDK not installed) Backend `OcrClient.decodeVin()` propagates these status codes. Controller maps: 503 -> 502 "VIN decode service unavailable", 422 -> 502 "VIN decode failed", 400 -> 400 "Invalid VIN". #### RULE 0 FIX: Explicit VIN validation before cache/OCR **Problem**: Gemini will hallucinate data for malformed VINs. **Fix**: Controller `decodeVin()` step 1 explicitly validates VIN format using regex `^[A-HJ-NPR-Z0-9]{17}$` BEFORE cache read and before OCR call. Returns 400 immediately on failure. This matches the existing `NHTSAClient.validateVin()` behavior. #### RULE 0 FIX: Cache write uses ON CONFLICT upsert **Fix**: `saveVinCache()` uses `INSERT INTO vin_cache ... ON CONFLICT (vin) DO UPDATE` - same idempotent pattern as existing `NHTSAClient.saveToCache()`. #### RULE 1 FIX: Type transition safety **Problem**: `VinCacheEntry.rawData` type change could break at milestone boundaries. **Fix**: M3 is atomic - it extracts types from nhtsa.types.ts, changes `rawData` type, AND rewires the controller/service in a single milestone. The old NHTSAClient code continues to exist (deleted in M4) but is no longer imported by any file after M3 completes. No intermediate broken state. #### RULE 2 NOTE: GenerationConfig per method **Fix**: `decode_vin()` creates `GenerationConfig` locally within the method body (not cached as instance state). The existing `self._generation_config` for maintenance extraction is untouched. #### RULE 2 NOTE: matchField() reuse confirmed `mapVinDecodeResponse()` delegates to the same `private matchField()` method. No new fuzzy matching logic. --- ### Additional files added to M3 (config/code) These files have active vpic/NHTSA references that are functional, not just documentation: - `backend/src/core/config/config-loader.ts` - remove `vpic` from Zod schema in `external` config object (lines 46-48). **RULE 0**: failing to remove this causes config validation error at startup when vpic config is removed from YAML files - `backend/src/core/config/feature-tiers.ts` - update `upgradePrompt` from "NHTSA database" to "vehicle database" (line 32) - `config/app/ci.yml` - remove `vpic:` block (lines 26-28) - `config/app/production.yml.example` - remove `vpic_api_url` (line 25) - `config/shared/production.yml` - remove `vpic:` block (lines 110-111) ### Additional files added to M4 (docs) - `docs/PLATFORM-SERVICES.md` - update "NHTSA vPIC API fallback with circuit breaker" to "Gemini VIN decode via OCR service" (line 38) - `docs/USER-GUIDE.md` - update "NHTSA database" references to "vehicle database" (lines 647, 658) - `docs/TESTING.md` - update vPIC mock references to OCR service mocks (lines 77, 197, 322-323) - `backend/src/features/vehicles/external/README.md` - remove NHTSAClient pattern reference (line 18) ### Updated milestone file counts | Milestone | Files (v2) | Files (v3) | Delta | |-----------|-----------|-----------|-------| | M1 (#224) | 6 | 6 | -- | | M2 (#225) | 2 | 2 | -- | | M3 (#226) | 5 | 10 | +5 (config files) | | M4 (#227) | 12 | 16 | +4 (docs files) | | M5 (#228) | ~12 | ~12 | -- | ### Removed from plan - `007_truncate_vin_cache.sql` - no longer needed (format-aware cache reads instead) *Verdict*: APPROVED | *Next*: Create branch, begin execution
Author
Owner

Milestone: M1-M3 Complete

Phase: Execution | Agent: Developer | Status: IN_PROGRESS

Completed

  • M1 (#224): OCR Python VIN decode endpoint - POST /decode/vin with GeminiEngine.decode_vin(), VinDecodeResponse model, VIN validation (400/422/503 error codes), 13 tests passing
  • M2 (#225): Backend OCR client - VinDecodeResponse type, OcrClient.decodeVin() method (JSON POST, not multipart)
  • M3 (#226): Rewire vehicles controller - Replaced NHTSAClient with OcrClient, moved cache logic to VehiclesService with format-aware reads (Gemini vs legacy NHTSA entries), renamed nhtsaValue->sourceValue, removed vpic config from Zod schema and YAML files. TypeScript compiles clean.

Remaining

  • M4 (#227): Remove NHTSA code and update docs (16 files)
  • M5 (#228): Frontend updates and tests (~12 files)

Commits

  • a75f7b5 feat: add VIN decode endpoint to OCR Python service (refs #224)
  • 3cd6125 feat: add backend OCR client method for VIN decode (refs #225)
  • 5cbf9c7 feat: rewire vehicles controller to OCR VIN decode (refs #226)

Verdict: IN_PROGRESS | Next: M4 and M5 (parallel)

## Milestone: M1-M3 Complete **Phase**: Execution | **Agent**: Developer | **Status**: IN_PROGRESS ### Completed - **M1** (#224): OCR Python VIN decode endpoint - `POST /decode/vin` with GeminiEngine.decode_vin(), VinDecodeResponse model, VIN validation (400/422/503 error codes), 13 tests passing - **M2** (#225): Backend OCR client - VinDecodeResponse type, OcrClient.decodeVin() method (JSON POST, not multipart) - **M3** (#226): Rewire vehicles controller - Replaced NHTSAClient with OcrClient, moved cache logic to VehiclesService with format-aware reads (Gemini vs legacy NHTSA entries), renamed nhtsaValue->sourceValue, removed vpic config from Zod schema and YAML files. TypeScript compiles clean. ### Remaining - **M4** (#227): Remove NHTSA code and update docs (16 files) - **M5** (#228): Frontend updates and tests (~12 files) ### Commits - `a75f7b5` feat: add VIN decode endpoint to OCR Python service (refs #224) - `3cd6125` feat: add backend OCR client method for VIN decode (refs #225) - `5cbf9c7` feat: rewire vehicles controller to OCR VIN decode (refs #226) *Verdict*: IN_PROGRESS | *Next*: M4 and M5 (parallel)
egullickson added
status
review
and removed
status
in-progress
labels 2026-02-19 03:53:25 +00:00
Author
Owner

Milestone: All Milestones Complete - PR Open

Phase: Review | Agent: Developer | Status: AWAITING_REVIEW

All 5 Milestones Complete

  • M1 (#224): OCR Python VIN decode endpoint (7 files, 13 tests)
  • M2 (#225): Backend OCR client method (2 files)
  • M3 (#226): Rewire vehicles controller (9 files)
  • M4 (#227): Remove NHTSA code + docs (16 files, 3 deleted)
  • M5 (#228): Frontend + tests (7 files)

Quality Checks

  • Backend TypeScript: 0 errors
  • Frontend TypeScript: 0 errors
  • Backend lint: 0 errors
  • Frontend lint: 0 errors
  • OCR Python tests: 31 pass (13 new + 18 existing)
  • NHTSA grep: 0 references in code (only legacy cache format comments in service)

PR

PR #229 opened targeting main. Fixes #223, #224, #225, #226, #227, #228.

Verdict: AWAITING_REVIEW | Next: Quality Agent RULE 0/1/2 review

## Milestone: All Milestones Complete - PR Open **Phase**: Review | **Agent**: Developer | **Status**: AWAITING_REVIEW ### All 5 Milestones Complete - **M1** (#224): OCR Python VIN decode endpoint (7 files, 13 tests) - **M2** (#225): Backend OCR client method (2 files) - **M3** (#226): Rewire vehicles controller (9 files) - **M4** (#227): Remove NHTSA code + docs (16 files, 3 deleted) - **M5** (#228): Frontend + tests (7 files) ### Quality Checks - Backend TypeScript: 0 errors - Frontend TypeScript: 0 errors - Backend lint: 0 errors - Frontend lint: 0 errors - OCR Python tests: 31 pass (13 new + 18 existing) - NHTSA grep: 0 references in code (only legacy cache format comments in service) ### PR PR #229 opened targeting main. Fixes #223, #224, #225, #226, #227, #228. *Verdict*: AWAITING_REVIEW | *Next*: Quality Agent RULE 0/1/2 review
egullickson added
status
done
and removed
status
review
labels 2026-02-20 15:04:14 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#223