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/)
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).
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)
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).
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"
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
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"
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"
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)
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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-vincalls NHTSA vPIC API directly from the TypeScript backend (vehicles/external/nhtsa/)vin_cachedatabase table (1-year TTL)vehicle.vinDecode)POST /api/ocr/extract/vin) is separate and unchanged by this issueExpected Behavior
POST /api/vehicles/decode-vinvin_cachetable for caching Gemini decode resultsScope
Remove
vehicles/external/nhtsa/directory (nhtsa.client.ts, nhtsa.types.ts, index.ts)Add/Modify
Acceptance Criteria
vin_cachetable still caches decode results/api/ocr/extract/vin) unaffectedPlan: 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 existingGeminiEngineclass (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
New Flow
Key Changes
nhtsaValuerenamed tosourceValueinMatchedField<T>(backend + frontend)mapNHTSAResponse()becomesmapVinDecodeResponse()- simpler since Gemini returns structured fields directly (no NHTSA array parsing)vin_cacheto clear NHTSA-format entriesMilestones
M1: OCR Python endpoint (#224)
Files:
ocr/app/engines/gemini_engine.py,ocr/app/routers/extract.py,ocr/app/models/*.pydecode_vin(vin: str)to GeminiEngine with VIN-specific prompt and JSON response schemaVinDecodeResponsePydantic modelPOST /decode/vinroute accepting{"vin": "..."}JSON body^[A-HJ-NPR-Z0-9]{17}$)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)VinDecodeResponsetype matching OCR service responsedecodeVin(vin: string)toOcrClient(JSON POST, not multipart)MatchedField<T>,DecodedVehicleData,DecodeVinRequest,VinDecodeErrorfrom nhtsa.types.ts into vehicles typesnhtsaValuetosourceValueinMatchedField<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.sqlmapNHTSAResponse()tomapVinDecodeResponse()- takes flat Gemini fields, does dropdown matching007_truncate_vin_cache.sqlmigrationM4: Remove NHTSA code and update docs (#227)
Files: Delete
backend/src/features/vehicles/external/nhtsa/(3 files), modifybackend/src/features/platform/models/responses.ts,backend/src/features/platform/README.md, 5 CLAUDE.md filesvehicles/external/nhtsa/directoryVPICVariable,VPICResponsefrom platform modelsM5: Frontend and tests (#228)
Files:
frontend/src/features/vehicles/types/vehicles.types.ts,frontend/src/features/vehicles/components/VinOcrReviewModal.tsx, guide page sections, test filesnhtsaValuetosourceValuein frontend typesRisk Assessment
nhtsaValue->sourceValue)/api/ocr/extract/vinendpoint is completely separate and unaffectedFiles Summary
vehicles/external/nhtsa/(index.ts, nhtsa.client.ts, nhtsa.types.ts)Verdict: AWAITING_REVIEW | Next: Plan review cycle (QR plan-completeness)
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 existingGeminiEngineclass.Rationale: Reuses lazy Vertex AI init and WIF credentials. Decision Critic verdict: STAND.
New Flow
OCR VIN Decode Response Schema (drives M1 and M2)
Gemini Prompt Design (M1)
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- adddecode_vin(vin: str)method with_VIN_DECODE_PROMPTand_VIN_DECODE_SCHEMAocr/app/routers/decode.py(NEW) - new router with prefix/decode, endpointPOST /vinocr/app/routers/__init__.py- exportdecode_routerocr/app/main.py- registerdecode_routerocr/app/models/__init__.py(or models file) - addVinDecodeResponsePydantic modelocr/tests/test_vin_decode.py(NEW) - unit test for/decode/vinrouteImplementation details:
decode_vin()callsmodel.generate_content([prompt_text], generation_config=vin_config)- text-only input, noPart.from_data_generation_configper method (maintenance schema vs VIN schema), stored as instance vars on first use^[A-HJ-NPR-Z0-9]{17}$, return 400 on failureVinDecodeResponsePydantic model matching the schema aboveM2: Backend OCR client VIN decode method (#225)
Files:
backend/src/features/ocr/domain/ocr.types.ts- addVinDecodeResponsetypebackend/src/features/ocr/external/ocr-client.ts- adddecodeVin(vin: string)methodImplementation details:
OcrClient.decodeVin()uses JSON POST (not multipart):fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({vin}) })VinDecodeResponsematching the Python schema aboveNot 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 herebackend/src/features/vehicles/domain/vehicles.service.ts- addgetVinCached(),saveVinCache(), renamemapNHTSAResponse()tomapVinDecodeResponse(response: VinDecodeResponse), renamenhtsaValuetosourceValueinmatchField()and all MatchedField constructionbackend/src/features/vehicles/domain/vehicles.types.ts(existing file) - addMatchedField<T>,MatchConfidence,DecodedVehicleData,DecodeVinRequest,VinDecodeError,VinCacheEntry(all extracted from nhtsa.types.ts withnhtsaValue->sourceValuerename andrawDatatype changed toVinDecodeResponse)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:
decodeVin()method flow:^[A-HJ-NPR-Z0-9]{17}$), return 400 if invalidvehiclesService.getVinCached(vin)- if hit, return cachedDecodedVehicleDataocrClient.decodeVin(vin)to getVinDecodeResponsevehiclesService.saveVinCache(vin, response)to cachevehiclesService.mapVinDecodeResponse(response)for dropdown matchingDecodedVehicleDatamapVinDecodeResponse(response: VinDecodeResponse): Promise<DecodedVehicleData>:response.year,response.make, etc. (no NHTSAClient.extractValue calls)matchField()logic unchanged (fuzzy matching against dropdowns)MatchedFieldobjects usesourceValueinstead ofnhtsaValueVinCacheEntry.rawDatatype changes fromNHTSADecodeResponsetoVinDecodeResponsesaveVinCache()stores Gemini response asraw_dataJSONBgetVinCached()returns cachedVinDecodeResponsefromraw_data, passes throughmapVinDecodeResponse()if cache hit (so dropdown matching always runs fresh)M4: Remove NHTSA code and update docs (#227)
Files to DELETE:
backend/src/features/vehicles/external/nhtsa/index.tsbackend/src/features/vehicles/external/nhtsa/nhtsa.client.tsbackend/src/features/vehicles/external/nhtsa/nhtsa.types.tsFiles to MODIFY:
backend/src/features/platform/models/responses.ts- removeVPICVariableandVPICResponseinterfaces (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 servicebackend/src/features/vehicles/README.md- update VIN decode section from NHTSA to Gemini/OCRbackend/src/features/vehicles/CLAUDE.md- changeexternal/description from "External service integrations (NHTSA)" to "External service integrations"backend/src/features/vehicles/external/CLAUDE.md- removenhtsa/directory entryocr/app/engines/CLAUDE.md- add VIN decode to GeminiEngine descriptionocr/CLAUDE.md- add "Gemini VIN decode" to service descriptionocr/app/CLAUDE.md- add decode router to subdirectoriesbackend/src/features/ocr/CLAUDE.md- adddecodeVinto OcrClient method listM5: Frontend updates and tests (#228)
Frontend files:
frontend/src/features/vehicles/types/vehicles.types.ts- renamenhtsaValuetosourceValueinMatchedField<T>, updateDecodedVehicleDatacomment from "NHTSA vPIC API" to "VIN decode"frontend/src/features/vehicles/components/VinOcrReviewModal.tsx- renamenhtsaValuereferences (lines 114-118) tosourceValue, updatenhtsaRefsvariable name tosourceRefsfrontend/src/features/vehicles/hooks/useVinOcr.ts- update step 2 label from "Decode VIN using NHTSA" to "Decode VIN", update error messages removing NHTSAfrontend/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 neededVehiclesService.mapVinDecodeResponse()mocking OcrClientOcrClient.decodeVin()methodRegression verification:
POST /api/ocr/extract/vin(image-based VIN extraction) still works - run existing OCR tests, verify PaddleOCR path untouchedMilestone Dependencies
Files Summary
vehicles/external/nhtsa/directoryocr/app/routers/decode.py,ocr/tests/test_vin_decode.py,007_truncate_vin_cache.sqlVerdict: AWAITING_REVIEW | Next: QR plan-code, QR plan-docs
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 thatraw_datahas asuccessfield (present inVinDecodeResponse, absent in oldNHTSADecodeResponse). 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/vinrouter returns:400for invalid VIN format422forGeminiProcessingError(Gemini returned bad data)503forGeminiUnavailableError(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 existingNHTSAClient.validateVin()behavior.RULE 0 FIX: Cache write uses ON CONFLICT upsert
Fix:
saveVinCache()usesINSERT INTO vin_cache ... ON CONFLICT (vin) DO UPDATE- same idempotent pattern as existingNHTSAClient.saveToCache().RULE 1 FIX: Type transition safety
Problem:
VinCacheEntry.rawDatatype change could break at milestone boundaries.Fix: M3 is atomic - it extracts types from nhtsa.types.ts, changes
rawDatatype, 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()createsGenerationConfiglocally within the method body (not cached as instance state). The existingself._generation_configfor maintenance extraction is untouched.RULE 2 NOTE: matchField() reuse confirmed
mapVinDecodeResponse()delegates to the sameprivate 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- removevpicfrom Zod schema inexternalconfig object (lines 46-48). RULE 0: failing to remove this causes config validation error at startup when vpic config is removed from YAML filesbackend/src/core/config/feature-tiers.ts- updateupgradePromptfrom "NHTSA database" to "vehicle database" (line 32)config/app/ci.yml- removevpic:block (lines 26-28)config/app/production.yml.example- removevpic_api_url(line 25)config/shared/production.yml- removevpic: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
Removed from plan
007_truncate_vin_cache.sql- no longer needed (format-aware cache reads instead)Verdict: APPROVED | Next: Create branch, begin execution
Milestone: M1-M3 Complete
Phase: Execution | Agent: Developer | Status: IN_PROGRESS
Completed
POST /decode/vinwith GeminiEngine.decode_vin(), VinDecodeResponse model, VIN validation (400/422/503 error codes), 13 tests passingRemaining
Commits
a75f7b5feat: add VIN decode endpoint to OCR Python service (refs #224)3cd6125feat: add backend OCR client method for VIN decode (refs #225)5cbf9c7feat: rewire vehicles controller to OCR VIN decode (refs #226)Verdict: IN_PROGRESS | Next: M4 and M5 (parallel)
Milestone: All Milestones Complete - PR Open
Phase: Review | Agent: Developer | Status: AWAITING_REVIEW
All 5 Milestones Complete
Quality Checks
PR
PR #229 opened targeting main. Fixes #223, #224, #225, #226, #227, #228.
Verdict: AWAITING_REVIEW | Next: Quality Agent RULE 0/1/2 review