Discovery Contract — subaru (us)
The vetting gate between discovery and coding. This file is the human narrative + probe evidence; the authoritative gate state lives in the sidecar
discovery-contract.gate.json, validated bycheck_contract_gate.py.
- OEM / market:
subaru/us - Discovered:
2026-07-03 - Gate status: ✅ all hard gates green (
check_contract_gate.py→VERDICT: PASS) — endpoints recorded in Postman (Public OEM APIs → Subaru → US)
Gate summary
| # | Gate | Entity / param | Status | Evidence |
|---|---|---|---|---|
| 1 | Pagination contract | inventory | ✅ PASS | 229/229 unique @ size 100 |
| 2 | Enumerator (model) | models | ✅ PASS | 10 carline codes |
| 3 | Enumerator (year) | models | ✅ PASS | 59 years (1968–2026) |
| 4 | Enumerator (trim) | features | ✅ PASS | 6 trims / OBK 2026 |
| 5 | Enumerator (dealer) | dealers | ✅ PASS | 643 dealer codes (US) |
| 6 | Multi-value filter | inventory | ✅ PASS | dealerCode CSV batching clean |
| 7 | Body shape / bot-block | inventory | ✅ PASS | 2xx all JSON (origin+referer) |
| 8 | Body shape / bot-block | features (configurator) | ✅ PASS | 2xx all JSON |
| 9 | Data quality | inventory | ✅ PASS | vin/tsrp/dealerCode ok (msrp=0 sentinel) |
| 10 | Data quality | dealers | ✅ PASS | lat/lon valid 200/200 |
| 11 | Data quality | models | ✅ PASS | code/msrp ok |
| 12 | Data quality | features | ✅ PASS | modelCode FK + feature code ok |
| 13 | Cross-source identity key | inventory→dealers | ✅ PASS | dealerCode = dealer.id |
| 14 | Cross-source identity key | inventory→models | ✅ PASS | modelCode = code |
| 15 | Cross-source identity key | inventory→features | ✅ PASS | (modelCode,trimCode) |
| 16 | Cross-source identity key | features→models | ✅ PASS | modelCode = code |
| 17 | Real fixture | inventory | ✅ PASS | tests/data/us/retailer_inventory_sample.json |
| 18 | Real fixture | dealers | ✅ PASS | tests/data/us/dealers_by_zipcode_sample.json |
| 19 | Real fixture | models | ✅ PASS | tests/data/us/models_basicdata_sample.json |
| 20 | Real fixture | features | ✅ PASS | tests/data/us/trims_limiteddetails_sample.json |
| 21 | Postman record | inventory | ✅ PASS | Public OEM APIs → Subaru → US |
| 22 | Postman record | dealers | ✅ PASS | Public OEM APIs → Subaru → US |
| 23 | Postman record | models | ✅ PASS | Public OEM APIs → Subaru → US |
| 24 | Postman record | features | ✅ PASS | Public OEM APIs → Subaru → US |
Per-entity endpoints
All on https://www.subaru.com. GET /services/* need only a UA; the inventory POST
also needs origin + referer. No TLS impersonation required.
| Entity | Endpoint (method + URL) | Auth / protocol | Fixture | Notes |
|---|---|---|---|---|
| inventory | POST /services/graphql/retailerinventory/v2 | JSON body; needs origin+referer (else 403) | retailer_inventory_sample.json | body {page,dealerCode(CSV),sortBy,itemsPerPage}; records pagedListWrapper.items, id vinNumber, total pagedListWrapper.pager.totalItems; price = tsrp/internetPrice, msrp=0 sentinel; destinationFee nullable |
| dealers | GET /services/dealers/distances/by/zipcode?zipcode=&count=&type=Active | UA only | dealers_by_zipcode_sample.json | root array of {dealer,distance}; id dealer.id; count=1000 from central zip → all 643 US dealers; geo at dealer.location.{latitude,longitude} |
| models | GET /services/vehicles/models/basicdata | UA only | models_basicdata_sample.json | root array; id code+year; enumerator for trims fan-out. Year enumerator: GET /services/vehicles/modelyears |
| features / trims / colors | GET /services/vehicles/trims/limiteddetails/{year}/{modelCode} | UA only | trims_limiteddetails_sample.json | root array of trims; trim id code; featureGroups[].features[] (features), exteriorColors[].msrp (color premium); FK modelCode |
Bonus source (not gated): GET /services/specialoffers/byDealer?type=&year=&model=&trim= — incentives/APR.
Cross-source identity / FK alignment (HARD)
| FK | Referencing entity.field | Target entity.field | Example value | Verified |
|---|---|---|---|---|
| inventory→dealers | inventory.dealerCode | dealer.id | 020250 (Liberty Subaru) | ✅ |
| inventory→models | inventory.modelCode | models.code | IMP | ✅ |
| inventory→features | inventory.(modelCode,trimCode) | trims.(modelCode,code) | IMP + TLD | ✅ |
| features→models | trims.modelCode | models.code | IMP | ✅ |
Probe evidence
1. Pagination contract — inventory ✅ PASS
POST retailerinventory/v2, dealerCode=020250 baseline, page/itemsPerPage.
- size 100 → 229/229 unique, 3 pages,
reached_end=true,param_ignored=false. - size 30 → 225/229 (8 pages): the reported total moved 227→229 mid-fetch — live inventory churn, not a silent cap (re-run at larger page size closed the gap).
2–5. Enumerators ✅ PASS
- model —
vehicles/models/basicdata,code→ 10 distinct (ASC BRZ CTK FOR IMP OBK SOL TSK UNC WRX). - year —
vehicles/modelyears,year→ 59 distinct (1968–2026). - trim —
vehicles/trims/limiteddetails/{year}/{model},code→ 6 for OBK 2026; per(year,model). - dealer —
dealers/distances/by/zipcode?count=1000,dealer.id→ 643 (full US). This is the fan-out enumerator forinventory.dealerCode(national query with nodealerCodereturnspagedListWrapper:null, so per-dealer/dealer-batch fan-out is required).
6. Multi-value filter — inventory ✅ PASS
dealerCode CSV 020250,020207: shared rows kept identical tsrp/dealerCode/modelCode, combined
call returned the full union (311), 0 contaminated fields → dealer codes may be safely batched.
7–8. Body shape / bot-block ✅ PASS
- inventory
POST(withorigin+referer): 100% of 2xx parse as JSON; realistic UA not required. Withoutorigin+referer→ 403. Nobrowser_sessionneeded. - configurator
GET trims/limiteddetails: 100% JSON, no friction.
9–12. Data quality / plausibility ✅ PASS
- inventory —
vinNumber30/30 non-empty & distinct,tsrppositive,dealerCodenon-empty. ⚠️msrpis a constant0— map price totsrp/internetPrice, nevermsrp. - dealers —
location.latitude/longitudevalid & in-range 200/200;idnon-empty (no 0.0 geo). - models —
codenon-empty 17/17,msrppositive. - features — carrying trim's
modelCodeFK non-empty 6/6, featurecodenon-empty.
13–16. Cross-source identity keys ✅ PASS
Value-verified against the correlated fixtures (dealer 020250 → its IMP/TLD inventory →
IMP trims → models.code=IMP). See alignment table above.
17–20. Real fixtures ✅ PASS
Five correlated live fixtures under projects/ai_subaru/tests/data/us/ (see per-entity table).
No synthetic data.
21–24. Postman records ✅ PASS
All endpoints are recorded in the team Public OEM APIs collection under Subaru → US
(collection uid 30657947-981c813f-173d-49cc-9215-3cab23957381, US folder
30657947-3183f53c-63fb-4a26-851c-27dac72d7ee7). Request IDs: inventory
2a4ed48c-2644-0cbb-7da2-79c880d1ee38, dealers 4a4f1c80-d13a-f933-a81c-1fe96414fda2,
models 3b247a76-9110-6960-5223-630f0d78b006, features/configurator
7a044911-7082-b925-eda3-d317f8491d89 (plus model-years, dealers-maxDist, incentives, and
geographic-data utility requests). The Subaru/US folders were created by hand (this MCP
deployment has no folder-create tools); requests were then added via createCollectionRequest.
A full v2.1 export also remains at docs/content/oems/subaru/subaru-us.postman_collection.json.
Notes for the implement phase
- Inventory fan-out: enumerate all dealer codes (
dealers …?count=1000from a central zip → 643) and pageretailerinventory/v2per dealer or per dealer-batch (CSVdealerCodeproven safe to combine; site uses ~22/call). Confirm the CSV batch-size ceiling during implement. - Pricing: inventory
tsrp(total suggested retail) +internetPrice;msrpis a0sentinel.destinationFeenullable — decide null policy. Trim MSRP fromtrims/limiteddetails.msrp;destinationCharge/dealerInvoiceare0there (use inventorydestinationFeefor per-vehicle freight).
Overrides
None required — every hard gate passed on probe evidence.
| Gate | OVERRIDE reason | Approved by |
|---|---|---|
| — | — | — |