npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cardog/entities

v0.1.1

Published

The Cardog ref grammar — permanent, typed identifiers for the vehicle graph. Builders, parsers, VIN-derived grains (nano/squish), domain type unions, and the canonical spec attribute catalog. Validate refs offline; resolve them via the Cardog API.

Readme

@cardog/entities

The ref grammar for the Cardog vehicle graph — permanent, typed, human-readable identifiers for automotive entities, and the pure functions that build, parse, and validate them.

import { modelYearRef, parseRef, isRefOf, squishFromVin } from "@cardog/entities";

modelYearRef("model:tesla/model-y", 2024); // "model-year:tesla/model-y/2024"
isRefOf("make:honda", "make");             // true — and narrows the type
parseRef("model-year:honda/civic/2021");   // { domain, key, segments }
squishFromVin("5TDGSKFC8RS123456");        // "5TDGSKFCR" — the market grain, offline

Everything in this package is pure and offline. Validating a ref, building one, deriving a nano or squish from a VIN — none of it touches the network, and none of it ever will. Resolving a ref to data (names, children, listings, specs, recalls, live market quotes) is what the Cardog API does.

What a ref is, and why it is permanent

Every entity in the vehicle graph — a make, a model, a model year, a recall campaign — has exactly one ref: an identifier of the form {domain}:{key}, with / separating the segments of composite keys.

make:honda
model:honda/civic
model-year:honda/civic/2021
fuel-type:electric
recall:nhtsa/23v123

Refs are identifiers, not display strings — make:mini is the ref; "MINI" is the display name carried in the entity's data. Refs are permanent join keys. Store them in your database columns, your config, your agent's memory: make:honda will mean Honda for as long as the platform exists, and the ref grammar is versioned (GRAMMAR_VERSION) precisely so that promise is checkable. Data keyed on refs today joins cleanly against every future API response.

Install

npm install @cardog/entities

ESM, TypeScript-first, zero dependencies. The root export is runtime-neutral — Node, browsers, workers, edge runtimes.

The grammar

A ref is {domain}:{key}. Domains are lowercase ([a-z][a-z0-9-]*). Keys are lowercase alphanumerics plus -, ., _, with / between segments — except the two machine-derived VIN domains, which are uppercase (see casing).

| Domain | Shape | Example | | --- | --- | --- | | make | make:{slug} | make:tesla | | model | model:{make}/{model} | model:tesla/model-y | | model-year | model-year:{make}/{model}/{year} | model-year:tesla/model-y/2024 | | recall | recall:{authority}/{campaign} | recall:nhtsa/23v123 | | complaint | complaint:{authority}/{odino} | complaint:nhtsa/11434120 | | safety-rating | safety-rating:{authority}/{make}/{model}/{year}/{body}/{drivetrain\|na}/{release} | safety-rating:nhtsa/tesla/model-y/2024/suv/awd/1 | | nano | nano:{10-or-13-char VIN-derived key} | nano:5TDGSKFCRS | | squish | squish:{9-char VIN-derived key} | squish:5TDGSKFCR | | attribute domains | {domain}:{slug} | fuel-type:electric, body-style:pickup, drive-type:awd-all-wheel-drive, … |

The attribute domains (body-style, fuel-type, drive-type, transmission, vehicle-type, engine-configuration, electrification-level, gvwr, country, and friends) are enumerated as TypeScript unions — see typed unions.

Builders and parsers

Every function validates against the grammar and throws rather than normalizes — a malformed ref is an error at the boundary, never a silent correction that diverges from the canonical form.

Core (@cardog/entities or @cardog/entities/ref)

import {
  isRef, isRefOf, buildRef, parseRef, domainOf, keyOf,
  modelYearRef, parseModelYearRef, recallRef, complaintRef, safetyRatingRef,
  GRAMMAR_VERSION,
} from "@cardog/entities";

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

buildRef("make", "tesla");     // "make:tesla" (throws on grammar violations)
parseRef("model:tesla/model-y");
// { domain: "model", key: "tesla/model-y", segments: ["tesla", "model-y"] }
domainOf("make:tesla");        // "make"
keyOf("model:tesla/model-y");  // "tesla/model-y"

// Composite builders — the only correct way to assemble composite refs:
modelYearRef("model:tesla/model-y", 2024);      // "model-year:tesla/model-y/2024"
parseModelYearRef("model-year:tesla/model-y/2024");
// { make: "make:tesla", model: "model:tesla/model-y", year: 2024 }

recallRef("nhtsa", "23V123");                   // "recall:nhtsa/23v123" — normalizes case
complaintRef("nhtsa", "11434120");              // "complaint:nhtsa/11434120"
safetyRatingRef("nhtsa", "model-year:tesla/model-y/2024", {
  bodyStyle: "SUV",
  driveTrain: "AWD",
  productionRelease: 1,
}); // "safety-rating:nhtsa/tesla/model-y/2024/suv/awd/1"

recallRef lowercases campaign numbers deliberately: the authority's casing (23V123) is display data; the ref is a join key. complaintRef addresses the incident (NHTSA's ODI number), not a source row — one complaint spans many component rows in the source data. safetyRatingRef addresses one rated vehicle configuration — body style, drivetrain, production release — because that is the grain NHTSA rates at; a blank drivetrain becomes the literal segment na so the ref stays structurally complete.

VIN grains (@cardog/entities or @cardog/entities/nano)

Between "one specific vehicle" (a VIN) and "a model year" (millions of vehicles) sit two machine-derived grains, both pure functions of the VIN — derivable offline, forever:

  • squish (9 chars): vin[0:8] + vin[9] — WMI + VDS + model-year char. Every VIN sharing a squish is the same configuration of the same model year. The market grain.
  • nano (11 or 14 chars): the squish plus the plant char (plus the extended-WMI suffix for low-volume manufacturers), with the check digit masked as *. Vehicles sharing a nano are fungible: same build, same origin. The deduplication and comparables grain.
import {
  squishFromVin, nanoFromVin, squishFromNano, nanoFromSquish, parseSquish,
  isSquish, isNano, isExtWmiVin, nano11FromVin, nano14FromVin,
  squishRef, squishFromRef, nanoRef, nanoFromRef,
} from "@cardog/entities";

squishFromVin("5TDGSKFC8RS123456");  // "5TDGSKFCR"
nanoFromVin("5TDGSKFC8RS123456");    // "5TDGSKFC*RS" — 11-char, check digit masked
squishFromNano("5TDGSKFC*RS");       // "5TDGSKFCR" — always agrees with squishFromVin

// Ref form: the masked check digit is a constant, so the ref omits it:
nanoRef("5TDGSKFC*RS");              // "nano:5TDGSKFCRS"
nanoFromRef("nano:5TDGSKFCRS");      // "5TDGSKFC*RS" — pure round-trip
squishRef("5TDGSKFCR");              // "squish:5TDGSKFCR"

The 11- vs 14-char split follows the SAE rule: a WMI whose third character is 9 marks a low-volume manufacturer whose identity needs VIN positions 12–14 (the extended WMI) — for everyone else those positions are serial, and including them would split one identity per serial prefix. nanoFromVin applies the rule for you; the fixed-width variants (nano11FromVin, nano14FromVin) throw if used against the wrong VIN class.

The casing laws

Refs are all-lowercase, as a design position: display casing varies across data sources ("MINI", "Mini", "mini"), and case-splits are how catalogs silently fracture — two spellings of one identity become two entities, and joins quietly miss. Source casing belongs in data columns; the ref never varies. isRef("make:Tesla") is false on purpose.

The one bounded exception: the nano and squish domains are UPPERCASE-only (VIN charset — no I, O, Q). Their keys are machine-derived from VINs by these functions — no human types one from memory and no free-text source produces one, so the mixed-case-producers risk that motivates lowercase everywhere else cannot arise, and uppercase keeps the keys byte-recognizable against the VINs they came from. The exception is enforced in both directions: nano:5tdgskfcrs is invalid.

Typed domain unions

The enumerable domains ship as TypeScript unions (@cardog/entities/types), so a misspelled ref in a fixed vocabulary is a compile error:

import type {
  EntityDomain,          // "make" | "model" | "body-style" | "fuel-type" | ...
  FuelTypeRef,           // "fuel-type:gasoline" | "fuel-type:electric" | ...
  BodyStyleRef, DriveTypeRef, TransmissionRef, VehicleTypeRef,
  ElectrificationLevelRef, EngineConfigurationRef, GvwrRef, CountryRef,
} from "@cardog/entities/types";

const fuel: FuelTypeRef = "fuel-type:electric";   // ✓ compile-time checked

The open-ended domains (make, model) are typed as EntityRef<"make"> / EntityRef<"model"> — validate those at runtime with isRefOf, and resolve free text against the live registry with the API's /v2/entities/resolve.

The spec attribute catalog

@cardog/entities/spec is the canonical spec attribute catalog: 155 attributes (fuelEconomyCity, curbWeight, heatedSeatsFront, …) with names, types, units, and value constraints — the same catalog the API's /v2/specs/catalog serves, and the vocabulary behind spec.{attributeId} filters on /v2/listings/search.

import spec from "@cardog/entities/spec";
import type { SpecAttributeId } from "@cardog/entities";

spec.attributes.fuelEconomyCity;
// { name: "Fuel Economy (City)", type: "quantitative", unit: "MPG", min: 1, max: 200, ... }

const id: SpecAttributeId = "curbWeight"; // compile-time checked attribute ids

Stability contract

  • GRAMMAR_VERSION is 1. It bumps only for a breaking change to the grammar itself — shape, charset, or canonicalization — which would force a migration for every ref-carrying column anywhere, so it effectively never moves. New domains and new entities are additive and do not bump it.
  • Refs are permanent. Once issued, a ref keeps its meaning. Store refs; they will not rot.
  • The vocabulary is versioned. vocabulary (from the package root) carries the provenance of the build: the source-registry commit (registrySha), the generation timestamp, and the grammar version. API responses can be audited against it.
import { vocabulary, domainNames } from "@cardog/entities";

vocabulary;    // { registrySha, generatedAt, grammarVersion }
domainNames(); // ["bed-type", "body-cab", ..., "vehicle-type"]

What this package does NOT contain

Deliberately, and permanently:

  • No entity data. The domain vocabularies themselves — which makes exist, which models belong to them, display names, identifiers — are not in the tarball. Importing @cardog/entities/domains/* fails with an error saying exactly that, and loadDomain() (the /node subpath) throws the same. Resolution and enumeration are API surfaces: GET /v2/entities/resolve?q=... turns free text into refs, GET /v2/entities/{ref} dereferences one, and paginated browse endpoints enumerate.
  • No VIN decoding. Deriving a nano/squish is character arithmetic; decoding a VIN to its identity is GET /v2/vin/{vin} (or the open-source @cardog/corgi decoder).
  • No network calls. Ever. That is the point: you can hold the grammar — validate, build, type, and store refs — with zero API dependency, and reach for the API exactly when you need answers about what a ref names.

Docs: cardog.app/docs/ref-grammar · API reference: cardog.app/docs

License

Apache-2.0