The ref grammar

Hold the platform's identifier language offline: @cardog/entities validates, builds, and types refs with zero API calls — resolution is where the API begins.

Every identifier in the Vehicle Graphmake:honda, model-year:honda/civic/2021, squish:5TDGSKFCR — obeys one grammar, and that grammar is published as an open-source package. Install it and your code can validate, build, parse, and type refs entirely offline: no API key, no network call, no rate limit. The language is free to hold. The answers are what the API serves.

bash
npm install @cardog/entities

ESM, TypeScript-first, zero dependencies, Apache-2.0. Runs in Node, browsers, workers, and edge runtimes.

Validate and build, offline

ts
import {
  isRef, isRefOf, buildRef, parseRef,
  modelYearRef, parseModelYearRef, recallRef,
} from "@cardog/entities";

isRef("make:tesla");                        // true
isRef("make:Tesla");                        // false — casing is part of the grammar
isRefOf(input, "model-year");               // narrows to EntityRef<"model-year">

modelYearRef("model:tesla/model-y", 2024);  // "model-year:tesla/model-y/2024"
parseModelYearRef("model-year:honda/civic/2021");
// { make: "make:honda", model: "model:honda/civic", year: 2021 }

recallRef("nhtsa", "23V123");               // "recall:nhtsa/23v123"

Every builder validates against the grammar and throws rather than normalizes — the same never-fuzzy law the API enforces at its boundary, available in your process. Validate user input before it ever costs a call; reject a malformed ref in a form handler; assert grammar in your tests.

The VIN grains, offline

nano and squish — the grains between one VIN and a whole model year — are pure functions of the VIN, so the package derives them locally:

ts
import { squishFromVin, nanoFromVin, squishRef, nanoRef } from "@cardog/entities";

squishFromVin("5TDGSKFC8RS123456");  // "5TDGSKFCR" — the exact-config market grain
nanoFromVin("5TDGSKFC8RS123456");    // "5TDGSKFC*RS" — the comparables grain
squishRef("5TDGSKFCR");              // "squish:5TDGSKFCR" — ready for /v2/quotes

A VIN in your database becomes a market-grain ref without touching the network — the API call you then make is the one that quotes it, not the one that derives it.

Typed unions

The enumerable domains ship as TypeScript unions, so a misspelled ref in a fixed vocabulary fails at compile time, not at request time:

ts
import type { FuelTypeRef, BodyStyleRef, EntityDomain } from "@cardog/entities/types";
import type { SpecAttributeId } from "@cardog/entities";

const fuel: FuelTypeRef = "fuel-type:electric";  // ✓ checked by tsc
const attr: SpecAttributeId = "curbWeight";       // the 155-attribute catalog, typed

The open-ended domains (make, model) are typed structurally (EntityRef<"make">) — their vocabularies live in the registry, and free text meets them through resolve.

The package also carries the spec attribute catalog itself (@cardog/entities/spec) — the same catalog GET /v2/specs/catalog serves, importable for building filter UIs and validating spec.{attributeId} query keys before sending them.

The stability contract

Refs are permanent join keys, and the package states that machine-checkably:

  • GRAMMAR_VERSION is 1 — it bumps only for a breaking change to the grammar itself, which the platform treats as a never-event. New domains and entities are additive.
  • vocabulary carries the provenance of the build the package was generated from, so what your process validates against is auditable.
  • A ref, once issued, keeps its meaning for the life of the platform. Store refs in your columns and your config; they will not rot.

Where the API begins

The package deliberately contains no entity data — no make list, no model vocabulary, no display names — and no VIN decoding. Holding the grammar tells you a ref is well-formed; only the registry can tell you what it names:

bash
# free text → refs (the one door for unstructured input)
curl "https://api.cardog.app/v2/entities/resolve?q=2021%20civic" -H "x-api-key: $CARDOG_API_KEY"

# ref → the entity: names, hierarchy, counts, links into every group
curl "https://api.cardog.app/v2/entities/model:honda%2Fcivic" -H "x-api-key: $CARDOG_API_KEY"

# VIN → identity as refs
curl "https://api.cardog.app/v2/vin/5TDGSKFC8RS123456" -H "x-api-key: $CARDOG_API_KEY"

That is the intended division of labour: hold the language locally, ask the platform for answers. Validate and construct refs offline all day; the moment a question needs the registry — resolution, enumeration, specs, recalls, quotes — /v2/entities and its sibling groups are the doors.