@adhd/apigen-base-logical 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Codec for `{type: 'string', format: 'byte'}`.
5
+ *
6
+ * Canonical wire (DESIGN §3): base64 **standard** variant (RFC 4648 §4) with
7
+ * padding. Uses `+` and `/` — NOT the URL-safe `-` and `_` variant.
8
+ *
9
+ * Host type: `Uint8Array`. Encode uses Node.js `Buffer.from` with the
10
+ * `'base64'` option; decode reconstructs a `Uint8Array` from the standard
11
+ * base64 string.
12
+ */
13
+ export declare const byteCodec: LogicalTypeCodec<Uint8Array>;
@@ -0,0 +1,11 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Codec for `{type: 'string', format: 'date-time'}`.
5
+ *
6
+ * Canonical wire (DESIGN §3): RFC 3339 UTC string with ≥ms precision.
7
+ * `Date.prototype.toJSON` already emits this format; `toISOString()` is the
8
+ * explicit, deterministic form. Decode validates that the wire is a string
9
+ * before constructing a `Date`.
10
+ */
11
+ export declare const dateTimeCodec: LogicalTypeCodec<Date>;
@@ -0,0 +1,29 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Branded primitive type for decimal values (mode:'branded', DESIGN §14.2).
5
+ *
6
+ * In TS the default decimal host type is a branded string — zero third-party
7
+ * deps; consumers that need rich arithmetic can opt in to `decimal.js`
8
+ * (DESIGN §14.2, §18 resolved decision). The brand prevents accidental
9
+ * narrowing to `string` at call-sites while keeping the runtime cost zero.
10
+ */
11
+ export type DecimalString = string & {
12
+ readonly __brand: 'DecimalString';
13
+ };
14
+ /**
15
+ * @stable Wrap a plain string as a `DecimalString` branded type.
16
+ *
17
+ * No validation here; the codec validates on decode. Use `makeDecimal` for
18
+ * constructing values from literals or from external strings.
19
+ */
20
+ export declare function makeDecimal(value: string): DecimalString;
21
+ /**
22
+ * @stable Codec for `{type: 'string', format: 'decimal'}`.
23
+ *
24
+ * Canonical wire (DESIGN §3): decimal string (e.g. `"123.456"`), never a
25
+ * float. In TS the host type is the {@link DecimalString} branded string.
26
+ * The encode passes the string through; decode validates the format and brands
27
+ * the result.
28
+ */
29
+ export declare const decimalCodec: LogicalTypeCodec<DecimalString>;
@@ -0,0 +1,24 @@
1
+ import { LogicalTypeRegistry } from '../registry';
2
+
3
+ export { dateTimeCodec } from './date-time';
4
+ export type {} from './date-time';
5
+ export { int64Codec } from './int64';
6
+ export { decimalCodec, makeDecimal } from './decimal';
7
+ export type { DecimalString } from './decimal';
8
+ export { byteCodec } from './byte';
9
+ export { uuidCodec } from './uuid';
10
+ export { numberSpecialCodec } from './number-special';
11
+ /**
12
+ * @stable Register all well-known scalar codecs into `registry`.
13
+ *
14
+ * Idempotent when called with `{override:true}` — subsequent calls replace
15
+ * existing registrations without throwing. Useful in test setups that
16
+ * reconstruct a registry per test.
17
+ *
18
+ * @param registry - The target registry (mutable — must not be frozen).
19
+ * @param opts.override - When `true`, re-registration does not throw
20
+ * `E_DUP_CODEC`. Default: `false`.
21
+ */
22
+ export declare function registerWellKnown(registry: LogicalTypeRegistry, opts?: {
23
+ override?: boolean;
24
+ }): void;
@@ -0,0 +1,12 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Codec for `{type: 'string', format: 'int64'}`.
5
+ *
6
+ * Canonical wire (DESIGN §3): decimal string (e.g. `"9007199254740993"`).
7
+ * Avoids JS f64 precision loss for values beyond `Number.MAX_SAFE_INTEGER`.
8
+ *
9
+ * Host type: `bigint`. The encode uses `String(bigint)` which emits a decimal
10
+ * string. Decode uses `BigInt(string)` which is exact over the full int64 range.
11
+ */
12
+ export declare const int64Codec: LogicalTypeCodec<bigint>;
@@ -0,0 +1,22 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Codec for non-finite JavaScript numbers: `NaN`, `Infinity`, and
5
+ * `-Infinity` (DESIGN §3 row 13).
6
+ *
7
+ * Canonical wire: string sentinels `"NaN"`, `"Infinity"`, `"-Infinity"`.
8
+ * `JSON.stringify` maps these to `null` by default — this codec overrides that.
9
+ *
10
+ * The schema is `{type: 'number'}` (no `format` key). The `matches` predicate
11
+ * therefore checks only `type:'number'`; the codec is the **last-resort** scalar
12
+ * for bare number nodes. The registry resolves codecs in insertion order, so
13
+ * callers should register well-known codecs before arbitrary number ones — but
14
+ * this codec's encode/decode only fire when the registry dispatches it.
15
+ *
16
+ * Note: ordinary finite numbers at a `{type:'number'}` node are passed through
17
+ * without codec involvement (the registry returns `undefined` when no format
18
+ * is present and this codec is not registered). When this codec IS registered,
19
+ * encode maps finite numbers to their numeric value (plain JSON passthrough is
20
+ * done by the walk; encode is only called when the codec wins the dispatch).
21
+ */
22
+ export declare const numberSpecialCodec: LogicalTypeCodec<number>;
@@ -0,0 +1,10 @@
1
+ import { LogicalTypeCodec } from '../contracts';
2
+
3
+ /**
4
+ * @stable Codec for `{type: 'string', format: 'uuid'}`.
5
+ *
6
+ * Canonical wire (DESIGN §3): lowercase hyphenated RFC 4122 UUID.
7
+ * Host type: `string` (UUIDs are naturally strings in TS). The codec
8
+ * normalizes uppercase input on encode to lowercase and validates on decode.
9
+ */
10
+ export declare const uuidCodec: LogicalTypeCodec<string>;
@@ -0,0 +1,60 @@
1
+ import { LogicalTypeRegistry } from './registry';
2
+
3
+ /** @stable Stable identifier for a logical type.
4
+ * Scalars: the JSON-Schema `format` (e.g. "date-time"). Nominal/union: a
5
+ * namespace-qualified id (e.g. "cli.User"). */
6
+ export type LogicalTypeId = string;
7
+ /** @stable */
8
+ export type LogicalKind = 'scalar' | 'nominal' | 'union' | 'map' | 'set';
9
+ /** @stable The wire alphabet — exactly JSON's value space. */
10
+ export type Wire = string | number | boolean | null | Wire[] | {
11
+ [k: string]: Wire;
12
+ };
13
+ /** @stable A resolved (no-$ref-at-root) JSON Schema node. */
14
+ export type SchemaNode = Readonly<Record<string, unknown>>;
15
+ /** @stable Threaded through a transcode walk. */
16
+ export interface TranscodeCtx {
17
+ readonly registry: LogicalTypeRegistry;
18
+ readonly resolve: (ref: string) => SchemaNode;
19
+ readonly seen: WeakSet<object>;
20
+ readonly path: string;
21
+ readonly mode: 'strict' | 'lossy';
22
+ }
23
+ /** @stable Host-agnostic codec. One per logical type. Pure, deterministic, total over its domain. */
24
+ export interface LogicalTypeCodec<Host = unknown> {
25
+ readonly id: LogicalTypeId;
26
+ readonly kind: LogicalKind;
27
+ /** Canonical schema fragment this codec owns (scalar: {type,format}; nominal: object $def). */
28
+ readonly schema: SchemaNode;
29
+ /** Structural, cheap test: does this codec own `node`? (format match, or x-apigen-codec===id). */
30
+ matches(node: SchemaNode): boolean;
31
+ /** Host -> wire. MUST NOT mutate `value`; MUST be deterministic. */
32
+ encode(value: Host, node: SchemaNode, ctx: TranscodeCtx): Wire;
33
+ /** Wire -> host. MUST validate-then-construct; MUST be the inverse of `encode` across the vectors. */
34
+ decode(wire: Wire, node: SchemaNode, ctx: TranscodeCtx): Host;
35
+ }
36
+ /** @stable A language's handling of one logical type (drives codegen). `$` = the value being transformed. */
37
+ export interface TemplateCell {
38
+ encode: string;
39
+ decode: string;
40
+ imports?: string[];
41
+ dep?: {
42
+ name: string;
43
+ version: string;
44
+ };
45
+ mode: 'native' | 'lib' | 'branded';
46
+ construct?: string;
47
+ toJSON?: string;
48
+ }
49
+ /** @stable Used ONLY where the schema cannot disambiguate (type:{} / any). Plain JSON otherwise.
50
+ * Wire: { "$apigen": "<LogicalTypeId>", "v": <Wire> }. */
51
+ export declare const ENVELOPE_KEY: "$apigen";
52
+ export interface ApigenEnvelope {
53
+ readonly [ENVELOPE_KEY]: LogicalTypeId;
54
+ readonly v: Wire;
55
+ }
56
+ /** @stable Schema-walking transcoder (impl is a LATER state — interface only here). */
57
+ export interface Transcoder {
58
+ encode(value: unknown, schema: SchemaNode, ctx?: Partial<TranscodeCtx>): Wire;
59
+ decode(wire: Wire, schema: SchemaNode, ctx?: Partial<TranscodeCtx>): unknown;
60
+ }
@@ -0,0 +1,25 @@
1
+ import { LogicalKind, LogicalTypeId, SchemaNode } from './contracts';
2
+
3
+ /** @stable Reserved descriptor keywords (advisory; structure via format/$ref/oneOf is authoritative). */
4
+ export declare const X_APIGEN_LOGICAL: "x-apigen-logical";
5
+ export declare const X_APIGEN_CODEC: "x-apigen-codec";
6
+ export declare const X_APIGEN_CTOR: "x-apigen-ctor";
7
+ export declare const X_APIGEN_TOJSON: "x-apigen-tojson";
8
+ /** @stable Bumped on any wire-table OR pinned-lib-version change. */
9
+ export declare const LOGICAL_TYPE_VERSION: "0.1.0";
10
+ /**
11
+ * @stable Read the advisory `x-apigen-logical` hint off a resolved schema node.
12
+ *
13
+ * Per invariant `[inv:hints-advisory]` the key is OPTIONAL: returns `undefined`
14
+ * (never throws) when the key is absent or carries an unrecognized value. The
15
+ * authoritative kind is derived from structure (format/$ref/oneOf); this is a
16
+ * dispatch accelerator only.
17
+ */
18
+ export declare function logicalKindOf(node: SchemaNode): LogicalKind | undefined;
19
+ /**
20
+ * @stable Read the advisory `x-apigen-codec` id off a resolved schema node.
21
+ *
22
+ * Per invariant `[inv:hints-advisory]` the key is OPTIONAL: returns `undefined`
23
+ * (never throws) when the key is absent or not a string.
24
+ */
25
+ export declare function codecIdOf(node: SchemaNode): LogicalTypeId | undefined;
package/lib/emit.d.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { LogicalTypeRegistry } from './registry';
2
+ import { LogicalTypeId, SchemaNode, TemplateCell } from './contracts';
3
+
4
+ /**
5
+ * @stable Generate-time emitter (DESIGN §11 codegen-first, §4.4 walk algorithm).
6
+ *
7
+ * apigen always knows the schema at generate-time, so we do NOT ship a
8
+ * schema-interpreting runtime transcoder per host. Instead, the generator walks
9
+ * the JSON-Schema node ONCE and emits a string expression of direct, typed
10
+ * (de)hydration glue, splicing per-language {@link TemplateCell} columns.
11
+ *
12
+ * Everything here is a PURE function over `(schema, registry, template table)`:
13
+ * deterministic, no I/O, no globals. The emitted text is host-language source
14
+ * (the minimal column shipped here is TypeScript) — `emit.ts` itself never runs
15
+ * it.
16
+ */
17
+ /**
18
+ * @stable A per-language template table: maps a {@link LogicalTypeId} (the id a
19
+ * codec is registered under) to that language's {@link TemplateCell}.
20
+ *
21
+ * The registry tells us WHICH logical type owns a node; the table tells us HOW
22
+ * the target language (de)hydrates it. Splitting the two keeps the walk
23
+ * language-agnostic — swap the table to retarget a host.
24
+ */
25
+ export type TemplateTable = Readonly<Record<LogicalTypeId, TemplateCell>>;
26
+ /**
27
+ * @stable Threaded through an emit walk. Mirrors the structural inputs of a
28
+ * transcode (`registry` + `$ref` resolver) but carries codegen-only state: the
29
+ * language `table`, a JSON-Pointer `path` for diagnostics, a cycle guard over
30
+ * resolved `$ref` targets, and a monotonic counter that mints collision-free
31
+ * lambda parameter names for nested `map`/`object` glue.
32
+ */
33
+ export interface EmitCtx {
34
+ /** Resolves which logical type owns a node (format / `x-apigen-codec`). */
35
+ readonly registry: LogicalTypeRegistry;
36
+ /** The active per-language template table. */
37
+ readonly table: TemplateTable;
38
+ /** `$ref` → `$def` resolver (root-bound). See {@link rootRefResolver}. */
39
+ readonly resolve: (ref: string) => SchemaNode;
40
+ /** JSON Pointer to the current node, for diagnostics. */
41
+ readonly path: string;
42
+ /** Cycle guard over `$ref` strings already on the active path. */
43
+ readonly seenRefs: ReadonlySet<string>;
44
+ /** Mints fresh lambda parameter names (`x0`, `x1`, …) for nested glue. */
45
+ readonly mint: () => string;
46
+ }
47
+ /**
48
+ * @stable Thrown when the schema cannot be lowered to glue in `strict` codegen
49
+ * (e.g. a `$ref` to a `$def` the resolver does not know, or a codec-owned node
50
+ * with no matching column in the active template table).
51
+ */
52
+ export declare class EmitError extends Error {
53
+ /** Stable, transport-neutral error code. */
54
+ readonly code: "E_EMIT";
55
+ /** JSON Pointer to the offending node. */
56
+ readonly path: string;
57
+ constructor(message: string, path: string);
58
+ }
59
+ /**
60
+ * @stable Build the default `$ref` resolver for a generate-time walk: standard
61
+ * JSON-Schema `#/$defs/<name>` (and legacy `#/definitions/<name>`) lookup
62
+ * against a `root` document.
63
+ *
64
+ * @param root The schema document holding `$defs` / `definitions`.
65
+ * @returns A resolver suitable for {@link EmitCtx.resolve}.
66
+ */
67
+ export declare function rootRefResolver(root: SchemaNode): (ref: string) => SchemaNode;
68
+ /**
69
+ * @stable Construct an {@link EmitCtx} with sensible defaults.
70
+ *
71
+ * @param registry Resolves the owning codec for a node.
72
+ * @param table The target-language template table.
73
+ * @param opts.root Schema document for the default `$ref` resolver.
74
+ * @param opts.resolve Explicit `$ref` resolver (overrides `root`).
75
+ */
76
+ export declare function createEmitCtx(registry: LogicalTypeRegistry, table: TemplateTable, opts?: {
77
+ root?: SchemaNode;
78
+ resolve?: (ref: string) => SchemaNode;
79
+ }): EmitCtx;
80
+ /**
81
+ * @stable Emit the host→wire expression for `valueExpr` at `node`.
82
+ *
83
+ * @param valueExpr The source expression holding the host value (e.g. `"data"`).
84
+ * @param node The resolved JSON-Schema node describing `valueExpr`.
85
+ * @param ctx The emit context (registry + table + resolver).
86
+ * @returns A target-language expression producing the wire value.
87
+ */
88
+ export declare function emitEncode(valueExpr: string, node: SchemaNode, ctx: EmitCtx): string;
89
+ /**
90
+ * @stable Emit the wire→host expression for `wireExpr` at `node`.
91
+ *
92
+ * @param wireExpr The source expression holding the wire value.
93
+ * @param node The resolved JSON-Schema node describing the host shape.
94
+ * @param ctx The emit context (registry + table + resolver).
95
+ * @returns A target-language expression reconstructing the host value.
96
+ */
97
+ export declare function emitDecode(wireExpr: string, node: SchemaNode, ctx: EmitCtx): string;
98
+ /**
99
+ * @stable The runtime helper pair the envelope fallback (§4.5) compiles against.
100
+ * Emitted glue references `__apigenEnvelopeEncode` / `__apigenEnvelopeDecode`;
101
+ * a host prelude provides them. Exposed as source so a generator can inline a
102
+ * prelude and so the wire shape (`{ $apigen, v }`) is asserted in one place.
103
+ */
104
+ export declare const ENVELOPE_HELPER_SOURCE: string;
105
+ /**
106
+ * @stable A minimal TypeScript template table covering the well-known scalar
107
+ * columns from DESIGN §13.2 (date-time, int64/bigint, byte/bytes). Sufficient
108
+ * to drive and test the walk; the full per-language tables land in later states.
109
+ *
110
+ * Keyed by the {@link LogicalTypeId} a scalar codec registers under (its JSON
111
+ * Schema `format`).
112
+ */
113
+ export declare const TS_TEMPLATE_TABLE: TemplateTable;
package/lib/hints.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { TemplateCell } from './contracts';
2
+
3
+ /**
4
+ * @stable The ordered list of well-known scalar logical-type ids, derived
5
+ * from the canonical codec set. Every language column in {@link TEMPLATE_CELLS}
6
+ * MUST have an entry for each of these ids.
7
+ */
8
+ export declare const CANONICAL_LOGICAL_TYPE_IDS: readonly [string, string, string, string, string, string];
9
+ /** @stable Union of the canonical well-known scalar ids. */
10
+ export type CanonicalLogicalTypeId = (typeof CANONICAL_LOGICAL_TYPE_IDS)[number];
11
+ /**
12
+ * @stable A fully-keyed per-language template table: maps every canonical
13
+ * logical-type id to its {@link TemplateCell}.
14
+ */
15
+ export type LanguageTable = Record<CanonicalLogicalTypeId, TemplateCell>;
16
+ /**
17
+ * @stable The host languages for which a template column exists in
18
+ * {@link TEMPLATE_CELLS}. `'typescript'` and `'python'` are fully filled
19
+ * (§13.2 values verbatim). `'rust'`, `'go'`, and `'java'` are scaffolded —
20
+ * structure complete, expressions use stable placeholders pending the
21
+ * `lt-host-*` states.
22
+ */
23
+ export type HostLanguage = 'typescript' | 'python' | 'rust' | 'go' | 'java';
24
+ /**
25
+ * @stable The template-cell registry: `[language][logicalTypeId] → TemplateCell`.
26
+ *
27
+ * TypeScript and Python columns are fully filled per DESIGN §13.2 (verbatim
28
+ * expressions). Rust, Go, and Java columns are scaffolded — structure complete,
29
+ * expressions use `__SCAFFOLD_*__` placeholders pending `lt-host-*` states.
30
+ *
31
+ * Keyed by {@link HostLanguage}, then by the canonical {@link CanonicalLogicalTypeId}.
32
+ */
33
+ export declare const TEMPLATE_CELLS: Readonly<Record<HostLanguage, LanguageTable>>;
34
+ /**
35
+ * @stable Return the template table for `language` from {@link TEMPLATE_CELLS}.
36
+ *
37
+ * This is the typed accessor the emitter uses to swap target languages —
38
+ * identical to `TEMPLATE_CELLS[language]` but carries the return type.
39
+ */
40
+ export declare function cellsFor(language: HostLanguage): LanguageTable;
41
+ /**
42
+ * @stable Return only the cells (from `language`'s column) for the given
43
+ * logical-type `ids`. Useful when the emitter needs a focused sub-table.
44
+ */
45
+ export declare function depsForLogicalTypes(ids: ReadonlyArray<string>, language: HostLanguage): Array<{
46
+ id: string;
47
+ dep: {
48
+ name: string;
49
+ version: string;
50
+ };
51
+ }>;
52
+ /**
53
+ * @stable Return the TypeScript `format → {name, version}` dep map for the
54
+ * CANONICAL well-known scalar ids.
55
+ *
56
+ * Per-surface minimal-manifest guarantee (DESIGN §14.1): only logical types
57
+ * that actually carry a `dep` entry appear in the map. Stdlib types
58
+ * (`date-time`, `int64`, `byte`, `uuid`, `number-special`) have no dep and
59
+ * are absent — a surface that never uses `Decimal` never pulls `decimal.js`.
60
+ *
61
+ * This is the authoritative source for the inline `TS_LOGICAL_TYPE_DEP_MAP`
62
+ * in `packages/apigen/cli/src/lib/commands/generate.ts` — import this instead.
63
+ *
64
+ * @returns A record keyed by canonical logical-type id (= JSON-Schema `format`).
65
+ */
66
+ export declare function tsDepMap(): Readonly<Record<string, {
67
+ name: string;
68
+ version: string;
69
+ }>>;
70
+ /**
71
+ * @stable Completeness guard: assert that every canonical logical-type id has
72
+ * a cell in `language`'s column of {@link TEMPLATE_CELLS}.
73
+ *
74
+ * This is the programmatic form of §13.3 ("no empty cells"). Throws if any
75
+ * canonical id is missing — enforces: add a logical type → every declared
76
+ * language column must fill it or this guard fires.
77
+ *
78
+ * The optional `_tableOverride` parameter exists ONLY for test injection
79
+ * (DEBT-LT-007): it lets a test drive the production throw path against a
80
+ * simulated incomplete column WITHOUT monkey-patching the frozen TEMPLATE_CELLS.
81
+ * Production callers must never pass it — the default (reading from
82
+ * TEMPLATE_CELLS) is always correct for production use.
83
+ *
84
+ * @throws {Error} With the missing ids listed, if any canonical id lacks a cell.
85
+ */
86
+ export declare function assertNoEmptyCells(language: HostLanguage, _tableOverride?: Partial<LanguageTable>): void;
@@ -0,0 +1,37 @@
1
+ import { LogicalTypeCodec, LogicalTypeId, SchemaNode } from './contracts';
2
+
3
+ /** @stable Keyed by LogicalTypeId. Scalars register by `format`; nominal/union by qualified id. */
4
+ export interface LogicalTypeRegistry {
5
+ /** Register a codec. Throws E_DUP_CODEC on duplicate id unless `{override:true}`. */
6
+ register(codec: LogicalTypeCodec, opts?: {
7
+ override?: boolean;
8
+ }): void;
9
+ /** Resolve the codec owning a RESOLVED schema node, or undefined for a plain JSON node. */
10
+ resolve(node: SchemaNode): LogicalTypeCodec | undefined;
11
+ /** Direct lookup (for $ref / discriminator resolution). */
12
+ get(id: LogicalTypeId): LogicalTypeCodec | undefined;
13
+ /** Introspection (codegen, debugging). */
14
+ ids(): readonly LogicalTypeId[];
15
+ /** Frozen snapshot, safe to share across dispatch calls. */
16
+ freeze(): LogicalTypeRegistry;
17
+ }
18
+ /** @stable E_DUP_CODEC carrier — thrown on duplicate codec registration without `{override:true}`. */
19
+ export declare class CodecRegistryError extends Error {
20
+ /** Stable, transport-neutral error code for duplicate-codec registration. */
21
+ readonly code: "E_DUP_CODEC";
22
+ constructor(message: string);
23
+ }
24
+ /**
25
+ * @stable Factory: a registry pre-loaded with the well-known scalar codecs.
26
+ *
27
+ * NOTE: This is the MINIMAL contract-spine stub. It provides the keyed-by-id
28
+ * store and `register`/`get`/`ids`/`freeze`/`resolve` plumbing with
29
+ * `E_DUP_CODEC` duplicate-detection only. Well-known scalar auto-loading
30
+ * (`opts.wellKnown`) and structural `resolve(node)` dispatch are LATER states —
31
+ * here `resolve` performs the trivial structural match the codec advertises via
32
+ * `matches(node)` so the spine is internally consistent, and `wellKnown` is
33
+ * accepted but not yet populated.
34
+ */
35
+ export declare function createRegistry(opts?: {
36
+ wellKnown?: boolean;
37
+ }): LogicalTypeRegistry;
@@ -0,0 +1,97 @@
1
+ import { LogicalTypeRegistry } from './registry';
2
+ import { LogicalTypeCodec, SchemaNode, Transcoder } from './contracts';
3
+
4
+ /**
5
+ * @stable Validate all `$ref` values in a schema tree against a `$defs` dictionary.
6
+ *
7
+ * Walks `schema` recursively (into `properties`, `items`, `oneOf`, `$ref`,
8
+ * `additionalProperties`, `propertyNames`) and throws if any `$ref` value
9
+ * does not resolve to a key in `defs`. Call this during schema generation
10
+ * (e.g., from `generate-schemas.ts`) to surface unresolvable `$ref` values at
11
+ * build time instead of at first runtime invocation (BUG-APIGEN-CORE-001).
12
+ *
13
+ * @param schema - The root schema node to validate.
14
+ * @param defs - Dictionary of named definitions keyed by full `$ref` URI
15
+ * (e.g., `"#/$defs/MyType"`). If omitted, no cross-schema
16
+ * validation is performed and only the structural walk runs.
17
+ *
18
+ * @throws {Error} When any `$ref` in `schema` cannot be resolved against `defs`.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const defs = { '#/$defs/User': { type: 'object', properties: { ... } } };
23
+ * validateSchemaRefs({ $ref: '#/$defs/User' }, defs); // ok
24
+ * validateSchemaRefs({ $ref: '#/$defs/Missing' }, defs); // throws
25
+ * ```
26
+ */
27
+ export declare function validateSchemaRefs(schema: SchemaNode, defs?: Readonly<Record<string, SchemaNode>>): void;
28
+ /**
29
+ * @stable Build a compile-once, schema-walking `Transcoder` over a frozen
30
+ * registry snapshot.
31
+ *
32
+ * The returned transcoder is the **in-process (run-mode) analog** of the
33
+ * generate-time emitter: it walks `schema` and the value in lockstep at
34
+ * runtime, applying the registered codec at any node the codec claims, and
35
+ * recursing through object properties, array items, `$ref`, and `oneOf`
36
+ * branches (DESIGN.md §4.4 / §11).
37
+ *
38
+ * Call `registry.freeze()` before passing it here to guarantee a stable,
39
+ * immutable view across concurrent dispatch calls.
40
+ *
41
+ * ## DEBT-LT-006 — registration-order sensitivity of `encodeSchemaless`
42
+ *
43
+ * For schema-less positions (nodes with no `type` or `format`), the
44
+ * `encodeSchemaless` function inside this module iterates `registry.ids()` in
45
+ * **insertion order** and returns the FIRST codec whose `encode()` succeeds.
46
+ * This is a first-match-wins policy.
47
+ *
48
+ * **Consequence:** a permissive custom codec registered BEFORE the canonical
49
+ * well-known codecs (date-time, int64, decimal, etc.) could shadow them at
50
+ * schema-less positions, producing incorrect envelopes. The standard
51
+ * registration order (via `registerWellKnown()`) is safe because the
52
+ * well-known codecs are inserted first. Custom codecs should be registered
53
+ * AFTER `registerWellKnown()` unless they intentionally take priority.
54
+ *
55
+ * A future `priority`/`weight` field on `LogicalTypeCodec` would make this
56
+ * explicit and order-independent.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const registry = createRegistry();
61
+ * registry.register(myDateCodec);
62
+ * const transcoder = buildTranscoder(registry.freeze());
63
+ *
64
+ * const wire = transcoder.encode(new Date(), { type: 'string', format: 'date-time' });
65
+ * const host = transcoder.decode(wire, { type: 'string', format: 'date-time' });
66
+ * ```
67
+ */
68
+ export declare function buildTranscoder(registry: LogicalTypeRegistry): Transcoder;
69
+ /**
70
+ * @stable Optional-peer-dep lazy registration (DESIGN.md §14.2).
71
+ *
72
+ * Attempts to register a codec by calling `loader()`. If `loader` throws a
73
+ * module-not-found error (the backing lib is absent), the registration is
74
+ * silently skipped. Any other error is re-thrown so programming mistakes
75
+ * surface immediately.
76
+ *
77
+ * This lets a consumer who never uses `Decimal` never install `decimal.js`
78
+ * and never pay for it. If a surface *does* use the type and the lib is
79
+ * absent, the fail-fast guard (§15.1) catches it at startup.
80
+ *
81
+ * @param registry - The registry to register into.
82
+ * @param _id - Logical type id (informational; the codec carries its own).
83
+ * @param loader - Synchronous factory that returns the codec. May throw
84
+ * `MODULE_NOT_FOUND` when the backing lib is absent.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * tryRegister(registry, 'decimal', () => {
89
+ * // eslint-disable-next-line @typescript-eslint/no-require-imports
90
+ * const { Decimal } = require('decimal.js');
91
+ * return buildDecimalCodec(Decimal);
92
+ * });
93
+ * ```
94
+ */
95
+ export declare function tryRegister(registry: LogicalTypeRegistry, _id: string, loader: () => LogicalTypeCodec): void;
96
+ export type { Transcoder, SchemaNode, TranscodeCtx, Wire } from './contracts';
97
+ export type { LogicalTypeRegistry } from './registry';
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@adhd/apigen-base-logical",
3
+ "version": "0.0.1",
4
+ "main": "./index.js",
5
+ "module": "./index.mjs",
6
+ "typings": "./index.d.ts"
7
+ }