Part of the lingo docs (natural-language quantities, units, dates parser) — index: https://lingo.pascal.app/llms.txt

## API

Core exports from `@pascal-app/lingo`:

- `lingo(text, opts?)`
- `parseQuantity(text, opts?)`
- `parseRange(text, opts?)`
- `partialState(text, opts?)`
- `findQuantities(text, opts?)`
- `quantity(value, unitRef, kind?)`
- `convert(value, from, to)`
- `tryConvert(value, from, to)` (non-throwing, returns structured issues)
- `convertDelta(value, from, to)`
- `convertCurrency(amount, from, to, { rates })` (literal currency refs typed)
- `fromMinor(amount, currency)` (literal currency refs typed)
- `fromJSON(json)`
- `describeResource(quantityOrRange)` and `describeResult(result)` from `@pascal-app/lingo/describe` for rich unit labels, formatted output, direct value resources, source text spans, and resource-style parse-result output for quantity/range/conversion/number/date/duration results
- `registerKind(def)`, `registerUnits(kind, units)`, `defineFuzzyVocab(kind, vocab)`, `createRegistry(kinds)`, `createLingo(opts)`
- `describeTemperature(quantity, opts?)`

Options:

- `kind`: Bias unit resolution and restrict expected kind.
- `unit`: Assume a unit for bare numbers and set canonical field unit.
- `currency`: Disambiguate bare currency symbols and ambiguous minor-unit words such as `$`, `¥`, and `cents`; explicit GBP pence forms such as `50p` and `3 quid 50` parse as GBP.
- `system`: Choose US, imperial, or metric unit families.
- `numberFormat`: Resolve ambiguous decimal and grouping separators.
- `locale`: Select a loaded language profile; omit for auto-detection among loaded packs plus English.
- `strictness`: forgiving, confirm, or strict.
- `accept`: Switch ranges, conversions, compounds, fuzzy, numberWords, approximations, bareNumbers.
- `tolerance`: typos fix/suggest/off and ambiguity assume/confirm.
- `escalate`: Map issue codes to error, warning, or info.
- `messages`: Override human copy by issue code.

Parse results are versioned discriminated unions: `{ schemaVersion: 3, ok, type, text, issues }`; successes add `span`, `confidence`, and the parsed value, while failures use `type: "failure"` and may include a full `candidate` result. Success `alternatives` are also discriminated (`type: "quantity"` or `type: "date"`). Built-in `createLingo()` instances keep the same literal-unit checks as top-level `quantity`/`convert`/`tryConvert`; custom registry instances keep broad string refs. `tryConvert()` mirrors absolute `convert()` but returns `{ ok: true, type: "conversion", value, unit, kind }` or `{ ok: false, type: "failure", issues }` instead of throwing. Quantity instances expose `.value`, `.base`, `.unit`, `.kind`, `.to(unitRef)`, `.valueIn(unitRef)`, `.convertDelta(unitRef)`, `.toMinor()` for currencies, `.format(opts?)`, `.toBest(opts?)`, and `.toJSON()`. Format defaults are parseable; `localizedUnits: true` is display-only for Intl unit words. Quantity JSON is self-describing: `{ schemaVersion: 3, type, kind, value, unit, base, baseUnit }`; `@pascal-app/lingo/describe` adds unit labels and formatted strings for values, `describeResource()` returns direct `lingo.quantity` / `lingo.range` resources with grouped `value` and `canonical` amounts, and `describeResult()` returns an opt-in resource-style parse-result view with `object`, `resourceSchemaVersion`, grouped `value`/`canonical` amounts, range `canonicalUnit`, source text spans including full-input failure spans, rich issues, alternatives, candidates, conversion `{ source, target: { unit }, converted }` data, `lingo.date` `{ value: { iso, epochMilliseconds }, calendar, grain, known }`, and `lingo.duration` `{ value, canonical, formatted, parts? }`. Quantity ranges expose `.minBase`, `.maxBase`, `.min()`, `.max()`, `.plusMinus`, `.fuzzy`, `.contains(q)`, `.widthIn(unitRef)`, `.to(unitRef)`, and `.format()`.

Issues are `{ code, severity, message, span, suggestions?, data? }`; parse-path spans are `{ start, end }` half-open offsets into the original input. Issue codes documented by the package: `EMPTY`, `NO_VALUE`, `UNKNOWN_UNIT`, `KIND_MISMATCH`, `RANGE_KIND_MISMATCH`, `CONVERSION_KIND_MISMATCH`, `RATE_REQUIRED`, `TRAILING_INPUT`, `SINGLE_VALUE_EXPECTED`, `APPROX_NOT_ALLOWED`, `UNIT_REQUIRED`, `CONVERSION_NOT_ALLOWED`, `NUMBER_FORMAT`, `NONFINITE`, `LOCALE_NOT_LOADED`, `RANGE_MIN`, `RANGE_MAX`, `RANGE_OPEN_BOUND_NOT_ALLOWED`, `REQUIRED`, `UNSUPPORTED_DATE`, `NOW_REQUIRED`, `TYPO_CORRECTED`, `AMBIGUOUS_NUMBER`, `AMBIGUOUS_UNIT`, `AMBIGUOUS_DATE`, `RANGE_REVERSED`, `COMPOUND_OVERFLOW`, `CIVIL_AVERAGE`, `UNIT_ASSUMED`, `WEEKDAY_ASSUMED_NEXT`, `SLANG_UNIT`, `TZ_IGNORED`, `AMBIGUOUS_TIMEZONE`.

### Data schemas

Every result serializes to a flat, versioned shape (`schemaVersion: 3`) — what `JSON.stringify(result)`, `toJSON()`, and the `/ai` fields emit and re-parse. Every `span` is a half-open `[start, end)` range into the original input.

#### Result

~~~ts
// lingo() / parse*() return a discriminated union on `type`, serialized
// flat (v3). JSON.stringify(result) or result.toJSON() emits this shape.

interface Result {              // success — ok: true
  schemaVersion: 3
  ok: true
  type: "quantity" | "range" | "conversion" | "number"
  text: string                 // the original input
  span: Span                   // the slice the parse consumed
  confidence: number           // 0 to 1
  issues: Issue[]              // warnings and infos ride along on success
  // ...plus the value fields for its `type` (see the Quantity / Range tabs)
}

interface Failure {            // ok: false
  schemaVersion: 3
  ok: false
  type: "failure"
  text: string
  issues: Issue[]              // at least one has severity: "error"
  candidate?: Result           // "what it would have been", when recoverable
}
~~~
#### Quantity

~~~ts
// One value in one unit, stored canonically as `base`. Quantity.toJSON():
interface QuantityJSON {
  schemaVersion: 3
  type: "quantity"
  kind: Kind                   // "length" | "mass" | "currency" | ...
  value: number                // amount in `unit` (the 72 in "72 in")
  unit: string                 // "in"
  base: number                 // canonical amount (1.8288) — the source of truth
  baseUnit: string             // the kind's SI-anchored base ("m")
  parts?: { value: number; unit: string }[]   // compound input: "5 ft 11 in"
  approximate?: boolean        // "about 5 kg"
}
~~~
#### Range

~~~ts
// A min/max, plus-or-minus, or fuzzy range. QuantityRange.toJSON():
interface QuantityRangeJSON {
  schemaVersion: 3
  type: "range"
  kind: Kind
  baseUnit: string
  min?: { value: number; unit: string; base: number; exclusive?: boolean }
  max?: { value: number; unit: string; base: number; exclusive?: boolean }
  plusMinus?: {                // "10 ± 0.5 mm"
    center: { value: number; unit: string; base: number }
    delta: { value: number; unit: string; base: number }
  }
  fuzzy?: { term: string; profile: string }   // { term: "hot", profile: "weather" }
  approximate?: boolean        // "a few minutes"
}
~~~
#### Issue & span

~~~ts
// The one error / warning / info shape on every result's issues[].
interface Issue {
  code: IssueCode              // stable SCREAMING_SNAKE, e.g. "UNKNOWN_UNIT"
  severity: "error" | "warning" | "info"
  message: string              // human copy — override with the `messages` option
  span?: Span                  // where in the input; absent for /ai bound issues
  suggestions?: string[]       // did-you-mean, most likely first (max 3)
  data?: object                // code-specific payload, typed via IssueDataMap
}

// A half-open [start, end) character range into the ORIGINAL input string.
interface Span {
  start: number
  end: number
  text: string                 // input.slice(start, end) — reads for itself
}
~~~

### Type safety

Unit refs are literal-typed from the registry, so cross-kind and unknown-unit mistakes are compile errors at zero runtime cost. Dynamic strings still work as the escape hatch, validated at runtime.

~~~ts
import { convert, quantity } from "@pascal-app/lingo"

convert(5, "in", "cm")    // ✅ number
quantity(5, "kg")         // ✅ Quantity<"mass"> — kind inferred from the unit

// @ts-expect-error 'kg' is mass, 'cm' is length — caught before you run
convert(5, "kg", "cm")
// @ts-expect-error 'nope' isn't a unit
quantity(5, "nope")

const u: string = userInput
quantity(5, u)            // ✅ dynamic strings still compile (validated at runtime)
~~~

Human docs: https://lingo.pascal.app/docs#api