# Santo

An Elixir library for VIN and chassis-number parsing, decoding, and
identity resolution — the decode layer beneath a provenance registry.

Generic VIN decoders (ISO 3779 split + an NHTSA API call) are commodity.
Santo's value is the layer they skip: **marque-level semantics**,
**pre-VIN chassis grammars**, and **chassis identity resolution**.
Porsche is the first marque adapter, chosen because it exercises every
hard case: pre-1981 chassis numbering, US/RoW VDS divergence,
serial-range-encoded model semantics, and the 30-year model-year
ambiguity.

## Installation

Santo requires Elixir 1.14 or later and has no runtime dependencies.

For local service development, keep Santo as a sibling project:

```elixir
defp deps do
  [
    {:santo, path: "../santo"}
  ]
end
```

After the first Hex release:

```elixir
defp deps do
  [
    {:santo, "~> 0.2.0"}
  ]
end
```

The package contains only the runtime library, its compiled reference
tables, license, changelog, and package metadata. The corpus and vPIC
Oracle remain maintainer tooling and are not shipped to consumers.

## Design commitments

1. **Pure functions over compiled data.** No processes, no ETS, no HTTP
   in the decode path. Reference tables (`priv/data/*.csv`, every row
   with a `source` column) compile into function heads and map literals.
   Works in a script, a test, or a Livebook with zero setup.
2. **Binary pattern matching is the dispatch mechanism.** Marque
   routing, market detection, and era detection are function heads over
   binary prefixes and shapes.
3. **Ambiguity is a first-class return value.** The library never
   guesses. A VIN that admits two model years returns both. A chassis
   number that could be two eras returns both, with the evidence that
   would settle it (`:kardex`, `:coa`, `:engine_number`) named in
   `notes`.
4. **VIN ≠ identity.** `Santo.Parsed` describes a string;
   `Santo.Identity` names a physical chassis. Re-stamps, replicas,
   pre-VIN cars, and factory crossover-era cars make identity a
   superset of VIN.

## Usage

```elixir
# Modern US-market: real check digit, model code + plant disambiguation
{:ok, d} = Santo.decode("WP0CA298X5L001502")
d.model        #=> {:carrera_gt, "980"}
d.years        #=> [2005]
d.check_digit  #=> :valid  (position 9 is literally "X" — remainder 10)

# Modern RoW: ZZZ filler *is* the market signal; no check digit applies
{:ok, d} = Santo.decode("WP0ZZZ95ZJS905016")
d.model               #=> {:"959", "959"}
d.attributes.variant  #=> :sport   (905xxx serial block — semantics in the serial range)
d.check_digit         #=> :not_applicable

# Pre-VIN ten-digit grammar (and marque-aware normalization)
{:ok, d} = Santo.decode("911.360.0471")
d.model  #=> {:carrera_rs_27, "911"}
d.years  #=> [1973]
d.notes  #=> [{:evidence_required, :option_package, [:kardex, :coa]}]
         #   (M471 Lightweight vs M472 Touring is *not* in the chassis number)

# 356-era crossover: ambiguity is data
{:ambiguous, [pre_a, a]} = Santo.decode("81192")
Santo.Identity.key("81192")
#=> {:ok, {:disputed,
#      [{:chassis, :porsche, :"356_pre_a", "81192"},
#       {:chassis, :porsche, :"356_a", "81192"}],
#      [:kardex, :engine_number]}}

# Dirty sources: repairs are proposed, never applied
{:error, invalid} = Santo.decode("WP0ZZZ95ZJS90015")   # 16 chars, as printed by Bonhams
invalid.repaired    #=> nil                  (multiple plausible repairs — never guess)
invalid.candidates  #=> ["WP0ZZZ95ZJS900015", ...]  (all decode as in-block 959s)

{:error, invalid} = Santo.decode("WP0CA29845L0O1561") # OCR'd O for 0
invalid.repaired    #=> "WP0CA29845L001561"  (unique repair passes the check digit)
```

Return convention throughout:

```elixir
{:ok, %Santo.Decoded{}}
| {:ambiguous, [%Santo.Decoded{}]}   # multiple valid readings
| {:error, %Santo.Invalid{}}         # reasons + repair proposals
```

## Layout

```
lib/santo.ex               # Public API: parse/1, decode/1, validate/1, identify/1, normalize/1
lib/santo/
  parsed.ex                # positional decomposition (no semantics)
  decoded.ex               # semantic decode struct
  invalid.ex               # diagnosis + repair proposals
  check_digit.ex           # transliteration, weights; market-conditional at the call site
  model_year.ex            # 30-year cycle; candidate lists; pos-7 rule (NA post-1981 only)
  wmi.ex                   # marque routing: compiled heads (hot) + map literal (long tail)
  normalize.ex             # lossless canonicalization
  identity.ex              # VIN ⊃ chassis; :disputed registry keys
  marque.ex                # adapter behaviour (:pass falls through to generic)
  marque/porsche.ex        # dispatcher: routes by shape/era
  marque/porsche/          # modern.ex (17-char) / mid.ex (ten-digit) / early.ex (356)
priv/data/                 # vendored tables; every row carries a source column
test/fixtures/corpus.exs   # golden corpus of real, documented cars
oracle/                    # maintainer-only vPIC differential harness
```

## Test corpus

The golden corpus (`test/fixtures/corpus.exs`) landed before the decoder
did — frozen benchmark first. Real, publicly documented cars: six
Carrera GTs sharing a VDS stem with distinct check digits, 959 Komfort
vs Sport serial blocks, the H/J year-code pair, 1973 Carrera RS 2.7
ten-digit chassis numbers, and 356 Speedsters spanning the Pre-A/A
factory transition.

```
mix test   # zero dependencies
```

## Status / roadmap

- [x] Phase 0 — skeleton + corpus, `Parsed`, check digit (incl. `X`), model-year candidates
- [x] Phase 1 — WMI routing + codegen (compiled heads + long-tail map)
- [x] Phase 2 — Porsche modern adapter (US/RoW split, market-conditional check digit, serial blocks)
- [x] Phase 3 — pre-VIN grammars (ten-digit Mid, 356 Early), normalization, crossover ambiguity
- [x] Phase 5 — identity layer (`identify/1`, `Santo.Identity.key/1`, `:disputed` values)
- [x] Phase 4 — `mix vin.oracle` vPIC differential harness
  ([design](docs/oracle.md); initial snapshot captured 2026-07-30)
- [ ] Pre-1970 911/912 schemes ('65–'69 30xxxx/35xxxx/45xxxx blocks) — fixtures first
- [ ] More marques, only after the behaviour has survived Porsche's full weirdness

Explicit non-goals for the library: persistence, HTTP clients in the
decode path, VIN generation beyond test support.
