@yanlinglabs/winter-provider-catalog 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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # `@yanlinglabs/winter-provider-catalog`
2
+
3
+ Winter's provider and model catalog as inert, validated DATA: 165 provider rows and 604 model rows
4
+ with their endpoints, auth kinds, pricing evidence, admission tier and provenance, plus the validator
5
+ and the vocabularies the JSON Schema restates.
6
+
7
+ No network, no filesystem, no Bun API — the catalog is a bundled JSON module import, which is what
8
+ lets it be decoded anywhere. `PROVENANCE.md` records where every row came from and under what
9
+ evidence; `bun run provenance:tiers -- --check` regenerates its census from the shipped data.
10
+
11
+ ## Install
12
+
13
+ This package is published to **two registries**, and which one you want depends on who you are.
14
+
15
+ ### From public npm (anyone)
16
+
17
+ ```sh
18
+ npm install @yanlinglabs/winter-provider-catalog
19
+ ```
20
+
21
+ Nothing else is needed: the `@yanlinglabs` scope is public on npm.
22
+
23
+ ### From GitHub Packages (the `yanlingLabs` org)
24
+
25
+ GitHub Packages needs the scope pointed at it and an authenticated read, even for a public package.
26
+ In your project's `.npmrc`:
27
+
28
+ ```
29
+ @yanlinglabs:registry=https://npm.pkg.github.com
30
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
31
+ ```
32
+
33
+ …with `GITHUB_TOKEN` in the environment — a personal access token carrying `read:packages`, never a
34
+ literal in the file. Then `npm install @yanlinglabs/winter-provider-catalog` as usual.
35
+
36
+ **The published packages contain COMPILED OUTPUT ONLY.** Each tarball ships `dist/` — the bundled
37
+ JavaScript a consumer imports and the `.d.ts` declarations their type-checker reads — plus its data
38
+ files, `README.md` and `LICENSE`. It does **not** ship `src/`: the TypeScript sources live at
39
+ <https://github.com/yanlingLabs/winter-agent-sdk>, which is where to read them, file an issue, or send
40
+ a patch.
41
+
42
+ ## License
43
+
44
+ MIT — see [`LICENSE`](./LICENSE), which ships in the published tarball.
45
+
46
+ Third-party attribution for the upstream catalog data this package derives from is in [`NOTICE`](./NOTICE), which ships in the tarball beside this file.
package/UPSTREAM.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$comment": "GENERATED — do not edit. The pin `scripts/provider-catalog.ts` stamps onto the merged catalog; the extractor's INPUT pin lives in third_party/omniroute-provider-source/UPSTREAM.json. `tagObject` is the annotated tag's own object id and `commit` is that tag PEELED — they are different objects, and recording only one of them would not be a pin.",
3
+ "upstream": {
4
+ "tag": "v3.8.50",
5
+ "tagObject": "6f5d4e00e817bc01b2ac16fdd66db3840c296416",
6
+ "commit": "5458026c216f77a3da68ea49152dc33470cfe2cb",
7
+ "extractorVersion": "winter.1",
8
+ "overlayVersion": "1"
9
+ }
10
+ }
@@ -0,0 +1,62 @@
1
+ /** The recorded pin. All four fields are checked against the fetched clone before anything is read. */
2
+ export interface UpstreamPin {
3
+ repository: string;
4
+ tag: string;
5
+ /** The annotated tag's OWN object id. */
6
+ tagObject: string;
7
+ /** The tag peeled to a commit — this is what the catalog records as `upstream.commit`. */
8
+ commit: string;
9
+ }
10
+ /** One allowlist path entry. `pattern` is a git sparse-checkout pattern rooted at the repository. */
11
+ export interface AllowlistPath {
12
+ pattern: string;
13
+ role: "extract" | "claim" | "notice";
14
+ why: string;
15
+ }
16
+ export interface MaterializedFile {
17
+ /** Repository-relative, `/`-separated. */
18
+ path: string;
19
+ /** git's own blob id — authoritative, and directly comparable with `git ls-tree` upstream. */
20
+ blobId: string;
21
+ /** sha256 of the materialized bytes, so a manifest entry is verifiable without git. */
22
+ sha256: string;
23
+ bytes: number;
24
+ role: AllowlistPath["role"];
25
+ /** The allowlist pattern that admitted this path. */
26
+ admittedBy: string;
27
+ }
28
+ export interface FetchResult {
29
+ /** Absolute path of the checkout root. Valid until `cleanup()`. */
30
+ root: string;
31
+ commit: string;
32
+ tagObject: string;
33
+ files: MaterializedFile[];
34
+ read(path: string): string;
35
+ cleanup(): void;
36
+ }
37
+ /** Thrown for every refusal in this module. A pin/boundary failure is never a warning. */
38
+ export declare class UpstreamFetchError extends Error {
39
+ constructor(message: string);
40
+ }
41
+ /**
42
+ * Translate one git sparse-checkout `--no-cone` pattern into a matcher.
43
+ *
44
+ * Deliberately supports only the subset the allowlist actually uses — a leading `/` anchor,
45
+ * `**` (any depth) and `*` (one segment) — and rejects anything else, because a pattern this
46
+ * function silently mis-parses is a boundary hole that looks like a boundary.
47
+ */
48
+ export declare function compilePathPattern(pattern: string): (path: string) => boolean;
49
+ export interface FetchOptions {
50
+ /** Where the scratch checkout goes. Defaults to a fresh mkdtemp under the OS temp dir. */
51
+ parentDir?: string;
52
+ /** Overrides `pin.repository` — the tests point this at a `file://` fixture repository. */
53
+ repositoryOverride?: string;
54
+ }
55
+ /**
56
+ * Clone the pinned tag, materialize exactly the allowlisted paths, verify, and hash.
57
+ *
58
+ * The caller MUST call `cleanup()` (the sync script does it in a `finally`): the checkout is a
59
+ * scratch directory outside the repository and nothing but the generated JSON and the two upstream
60
+ * notice files ever survives it.
61
+ */
62
+ export declare function fetchUpstream(pin: UpstreamPin, paths: AllowlistPath[], options?: FetchOptions): FetchResult;
@@ -0,0 +1,113 @@
1
+ import type { MaterializedFile, UpstreamPin } from "./fetch.js";
2
+ export interface DenominatorReport {
3
+ /** Distinct upstream ids across every product-catalog category file. */
4
+ catalogueUnion: number;
5
+ /** Per-category distinct-id counts, in the order WS-13 §1's table lists them. */
6
+ byCategory: Array<{
7
+ category: string;
8
+ count: number;
9
+ }>;
10
+ /** Ids appearing in more than one category file — each would be double-counted by a naive sum. */
11
+ duplicatedAcrossCategories: string[];
12
+ /** Naive sum of per-category counts, i.e. what double-counting would produce. */
13
+ categorySum: number;
14
+ /** Keys of the backend `REGISTRY` map — the EXECUTABLE catalog, always smaller than the product one. */
15
+ registryEntries: number;
16
+ /**
17
+ * How many of those keys the literal walker could actually RESOLVE to an entry object.
18
+ *
19
+ * Reported separately and deliberately: many upstream entries are built by a helper call
20
+ * (`buildOpenAiCompatibleRegistryEntry({…})`), which the extractor rejects as an executable value,
21
+ * so their bindings do not exist. Folding the two numbers into one would report the extractor's
22
+ * own blind spot as an upstream fact — which is exactly the kind of quiet denominator error this
23
+ * report exists to expose.
24
+ */
25
+ registryEntriesResolved: number;
26
+ /** Catalogued ids with no backend registry entry. */
27
+ catalogueWithoutRegistry: number;
28
+ /** Registry ids with no product-catalog row. */
29
+ registryWithoutCatalogue: number;
30
+ /** What upstream itself CLAIMS, per claim source. */
31
+ claims: Array<{
32
+ sourcePath: string;
33
+ claimed: number | undefined;
34
+ note: string;
35
+ }>;
36
+ /** Human-readable summary, embedded verbatim in PROVENANCE.md. */
37
+ summary: string;
38
+ }
39
+ export interface DenominatorInput {
40
+ /** category name -> the distinct upstream ids it contains. */
41
+ byCategory: ReadonlyMap<string, ReadonlySet<string>>;
42
+ /** Every key of the backend `REGISTRY` map, read from the object's TEXT (never from resolved values). */
43
+ registryIds: ReadonlySet<string>;
44
+ /** The subset whose entry object the literal walker resolved. */
45
+ registryIdsResolved: ReadonlySet<string>;
46
+ /** Claim source path -> its raw text, scanned for the headline provider count. */
47
+ claimSources: ReadonlyArray<{
48
+ sourcePath: string;
49
+ text: string;
50
+ }>;
51
+ }
52
+ /**
53
+ * Pull upstream's own headline provider count out of a claim source.
54
+ *
55
+ * Two spellings at the pin: the generated reference says "Total providers: **352**", the README says
56
+ * "352 AI providers". Both are matched, and a source that states neither returns `undefined` rather
57
+ * than a zero — "we could not find a claim" and "upstream claims none" are different facts.
58
+ */
59
+ export declare function findClaimedProviderCount(text: string): number | undefined;
60
+ export declare function computeDenominator(input: DenominatorInput): DenominatorReport;
61
+ export interface ExtractionManifest {
62
+ $comment: string;
63
+ upstream: UpstreamPin & {
64
+ packageVersion?: string;
65
+ };
66
+ extractorVersion: string;
67
+ generatedFrom: string;
68
+ /** Files COPIED into this repository (upstream licence/notices), with their provenance. */
69
+ copiedFiles: Array<{
70
+ upstreamPath: string;
71
+ localPath: string;
72
+ blobId: string;
73
+ sha256: string;
74
+ bytes: number;
75
+ licence: string;
76
+ modifications: "none";
77
+ }>;
78
+ /** Files READ during extraction and deliberately NOT copied. */
79
+ readOnlyFiles: Array<{
80
+ path: string;
81
+ blobId: string;
82
+ sha256: string;
83
+ bytes: number;
84
+ role: string;
85
+ admittedBy: string;
86
+ }>;
87
+ outOfAllowlistImports: string[];
88
+ }
89
+ export interface ManifestInput {
90
+ pin: UpstreamPin & {
91
+ packageVersion?: string;
92
+ };
93
+ extractorVersion: string;
94
+ files: readonly MaterializedFile[];
95
+ /** upstream path -> where it was copied to inside this repository. */
96
+ copiedTo: ReadonlyMap<string, string>;
97
+ outOfAllowlistImports: readonly string[];
98
+ }
99
+ export declare function buildExtractionManifest(input: ManifestInput): ExtractionManifest;
100
+ export type ProvenanceClass = "copied-verbatim" | "mechanically-normalized" | "official-doc-derived" | "live-probe-proven" | "local-override";
101
+ export interface FieldProvenance {
102
+ field: string;
103
+ provenance: ProvenanceClass;
104
+ note: string;
105
+ }
106
+ /**
107
+ * The per-field classification report §6 asks for, as DATA.
108
+ *
109
+ * PROVENANCE.md renders this table rather than restating it in prose, because a hand-written
110
+ * provenance table is exactly the document that goes stale the first time the mapper changes and
111
+ * nobody notices — and a stale provenance claim is worse than none.
112
+ */
113
+ export declare const FIELD_PROVENANCE: readonly FieldProvenance[];
@@ -0,0 +1,94 @@
1
+ export type LiteralValue = string | number | boolean | null | LiteralValue[] | {
2
+ [key: string]: LiteralValue;
3
+ };
4
+ /**
5
+ * Why something did not reach the catalog. The FIELD/VALUE classes are this module's; the CATEGORY
6
+ * and CURATION classes are `ledgers.ts`'s, and both live in one union so the committed ledger has a
7
+ * single closed vocabulary a reviewer can diff across upstream bumps.
8
+ */
9
+ export type ExclusionClass = "executable-value" | "dynamic-expression" | "env-read" | "identity-header" | "url-builder" | "credential-material" | "unresolved-reference" | "unsupported-shape" | "category-no-auth" | "category-oauth" | "category-web-cookie" | "category-search" | "category-audio" | "category-upstream-proxy" | "category-cloud-agent" | "category-system" | "category-local-live-discovery" | "not-allowlisted" | "no-registry-entry" | "duplicate-id" | "unrepresentable-protocol"
10
+ /**
11
+ * NOT an exclusion. A row that DID reach the catalog, carrying a reviewed, recorded deviation from
12
+ * what the pinned tree literally says (a corrected wire id, an adapter the protocol does not imply,
13
+ * a per-row status). It shares the ledger with the exclusions because the ledger's job is "every
14
+ * place the catalog and its source differ, with the reason" — and filing a deliberate normalization
15
+ * under `unrepresentable-protocol` made the ledger's own counts lie about what was dropped.
16
+ */
17
+ | "reviewed-normalization"
18
+ /** A row excluded because it is not a language model at all (WS-13 §4: `tts`/`stt`/media rows never feed the worker-model picker). */
19
+ | "out-of-scope";
20
+ export interface Rejection {
21
+ scope: "module" | "provider" | "model" | "field";
22
+ /** The upstream provider id this rejection belongs to, or `""` for a module-level one. */
23
+ upstreamId: string;
24
+ /** Dotted location inside the module, e.g. `geminiProvider.oauth`. */
25
+ path: string;
26
+ sourcePath: string;
27
+ reason: string;
28
+ exclusionClass: ExclusionClass;
29
+ }
30
+ export interface ExtractOptions {
31
+ /**
32
+ * Exported literals from other ALLOWLISTED modules, keyed `<sourcePath>#<exportName>` and
33
+ * `#<exportName>` (the latter is the "any allowlisted module" fallback used when an import
34
+ * specifier resolves to a materialized file whose own exports are already known).
35
+ */
36
+ externals?: ReadonlyMap<string, LiteralValue>;
37
+ /** Every repository-relative path that was materialized, so an out-of-allowlist import is visible. */
38
+ materializedPaths?: ReadonlySet<string>;
39
+ }
40
+ export interface ModuleLiterals {
41
+ sourcePath: string;
42
+ /** Top-level `const` bindings whose initializer was fully accepted. */
43
+ values: Map<string, LiteralValue>;
44
+ rejections: Rejection[];
45
+ /** Import specifiers naming a module outside the materialized allowlist. */
46
+ outOfAllowlistImports: string[];
47
+ /** Imported name -> the module specifier it came from, for `unresolved-reference` reasons. */
48
+ importOrigins: Map<string, string>;
49
+ }
50
+ /**
51
+ * Resolve a relative import specifier against a repository-relative source path.
52
+ *
53
+ * Upstream mixes explicit `./shared.ts` specifiers with extensionless `./gateways` ones, so an
54
+ * `exists` predicate is consulted for the two implicit spellings. Without it every extensionless
55
+ * import inside the allowlist reads as OUT of the allowlist — a false boundary alarm that would
56
+ * bury the real ones.
57
+ */
58
+ export declare function resolveRelativeSpecifier(sourcePath: string, specifier: string, exists?: (path: string) => boolean): string | undefined;
59
+ /**
60
+ * Parse one module and extract its accepted top-level literal bindings.
61
+ *
62
+ * Never throws on hostile input: a syntactically broken file yields no values and a module-scope
63
+ * rejection. Depth-bounded so a pathological nesting cannot overflow the stack.
64
+ */
65
+ export declare function extractModuleLiterals(sourcePath: string, text: string, options?: ExtractOptions): ModuleLiterals;
66
+ /**
67
+ * Extract a whole materialized set, resolving cross-module identifier references between
68
+ * ALLOWLISTED files only.
69
+ *
70
+ * ITERATES TO A FIXPOINT rather than running a fixed number of passes, because upstream's import
71
+ * graph is neither topologically ordered nor shallow. The real chain at the pin is three deep and
72
+ * runs BACKWARDS against filename order:
73
+ *
74
+ * open-sse/config/providers/index.ts (REGISTRY -> each provider identifier)
75
+ * <- registry/openai/index.ts (a model spreads GPT_5_6_API_CAPABILITIES)
76
+ * <- shared.ts (where that constant is declared)
77
+ *
78
+ * `index.ts` sorts FIRST and `shared.ts` LAST, so a two-pass version resolved the leaf entries but
79
+ * handed `index.ts` the stale first-pass copies — and the whole GPT-5.6 family silently lost its
80
+ * context window, modalities and Responses endpoint while every row still looked plausible. A
81
+ * fixpoint has no such off-by-one: it stops when a pass adds nothing, and the pass budget exists
82
+ * only so a pathological graph terminates rather than looping.
83
+ *
84
+ * Values accumulate into ONE map that later files in the SAME pass can already read, so a forward
85
+ * dependency costs no extra pass at all.
86
+ */
87
+ export declare function extractAll(files: ReadonlyArray<{
88
+ path: string;
89
+ text: string;
90
+ }>): {
91
+ modules: Map<string, ModuleLiterals>;
92
+ exports: Map<string, LiteralValue>;
93
+ passes: number;
94
+ };
@@ -0,0 +1,213 @@
1
+ import type { AdmissionTier, ModelFamilyDescriptor, ProviderProtocol, WinterCatalog, WinterModelDescriptor, WinterProviderDescriptor } from "../types.js";
2
+ import type { ExclusionClass, LiteralValue, Rejection } from "./literal-extractor.js";
3
+ export interface AllowlistProviderRow {
4
+ upstreamId: string;
5
+ winterId: string;
6
+ expectedCategory: string;
7
+ /**
8
+ * The `status` every extracted model of this provider starts at. Absent means `candidate`.
9
+ *
10
+ * R6-16 puts the native-cloud families in the catalog as `experimental`; nothing here can ever
11
+ * reach `supported`, which requires the behavioural corpus (WS-13 §13).
12
+ */
13
+ initialModelStatus?: "candidate" | "experimental";
14
+ /**
15
+ * The display name this provider's row must carry, when the product catalog's own `name` would be
16
+ * AMBIGUOUS rather than merely different.
17
+ *
18
+ * R6b-5 puts a vendor's two documented dialects on two rows, and the upstream product catalog has
19
+ * ONE name for the vendor — so `zai` and `zai-anthropic` both read "Z.AI" in a picker, which is a
20
+ * row a user cannot choose between. The override is a reviewed allowlist edit like `winterId` and
21
+ * `adapterIdOverride`, and it is RECORDED in the ledger; it is not a licence to rename providers
22
+ * for taste.
23
+ */
24
+ displayNameOverride?: string;
25
+ /**
26
+ * The adapter this provider's rows must name, when it is NOT the one its protocol implies.
27
+ *
28
+ * Vertex shares the GenerateContent dialect with the Gemini API, so deriving the adapter from the
29
+ * protocol named `winter.google-generate-content` — and a registry resolves an adapter BY ID, so a
30
+ * Vertex session would have been served by the Gemini API adapter with no location-scoped URL and
31
+ * no ADC credential. A protocol is not an adapter.
32
+ */
33
+ adapterIdOverride?: string;
34
+ risk: {
35
+ class: "approved" | "review-required" | "blocked";
36
+ reasons: string[];
37
+ };
38
+ /**
39
+ * WS-13b §1: how the vendor charges for the credential Winter uses. COPIED VERBATIM onto the
40
+ * generated row — the extractor never derives it, because nothing in the pinned upstream tree
41
+ * states it and a derivation would be Winter guessing at a billing fact.
42
+ */
43
+ pricingBasis: "token" | "subscription" | "free";
44
+ /**
45
+ * WS-13b §1 (D21): the documented third-party path this row ships through, with its citation.
46
+ * Also copied verbatim, and REQUIRED: an allowlist entry missing it fails the run rather than
47
+ * producing a row whose admission nobody can check — see step (0) at the top of
48
+ * `buildUpstreamLayer`, which refuses before anything is classified.
49
+ */
50
+ admission: {
51
+ basis: "api-key" | "oauth-documented" | "keyless-documented" | "local" | "cloud-credential";
52
+ citation: string;
53
+ tier: AdmissionTier;
54
+ };
55
+ /**
56
+ * WS-13b §7/§8.4 (fix-wave R-FW-2): the vendor's own second identity field, copied verbatim like
57
+ * the two above. Absent for every allowlist row today -- only a vendor that DOCUMENTS such a field
58
+ * gets one, and none of the extracted entries does.
59
+ */
60
+ identityHeaders?: Record<string, string>;
61
+ }
62
+ /** A hand-reviewed, per-model deviation from what the pinned upstream tree says. Always recorded. */
63
+ export interface ModelOverride {
64
+ /** Corrects a wire id upstream spells differently from the provider's own documentation. */
65
+ id?: string;
66
+ /** Keeps the row OUT of the catalog entirely (WS-13 §4: a non-`llm` row never reaches the worker-model picker). */
67
+ exclude?: boolean;
68
+ /** Overrides the provider's `initialModelStatus` for this one row. */
69
+ status?: "candidate" | "experimental";
70
+ why: string;
71
+ }
72
+ export interface CategoryDisposition {
73
+ disposition: "blocked" | "candidate-pool" | "winter-owned";
74
+ exclusionClass: ExclusionClass;
75
+ reason: string;
76
+ }
77
+ export interface Allowlist {
78
+ allowlistVersion: number;
79
+ paths: Array<{
80
+ pattern: string;
81
+ role: "extract" | "claim" | "notice";
82
+ why: string;
83
+ }>;
84
+ providers: AllowlistProviderRow[];
85
+ categoryDispositions: Record<string, CategoryDisposition>;
86
+ /** providerId -> upstream model id -> a hand-reviewed override. Every field is optional but `why`. */
87
+ modelOverrides?: Record<string, Record<string, ModelOverride>>;
88
+ blocked: Array<{
89
+ upstreamId: string;
90
+ reason: string;
91
+ }>;
92
+ importBoundary: {
93
+ resolveIdentifiersWithin: string;
94
+ failOnUnresolvedFields: string[];
95
+ };
96
+ }
97
+ /**
98
+ * Upstream `format` -> the path THAT FORMAT'S adapters append to a base URL.
99
+ *
100
+ * Removing this suffix from upstream's `baseUrl` is how the mapper reaches the API ROOT a
101
+ * `defaultEndpoints.api` must carry. See the long note at the `baseUrl` block in
102
+ * `buildUpstreamLayer` for why a consumed row cannot hold the verbatim path, and why this is the
103
+ * only transformation permitted (never a trim to an origin).
104
+ *
105
+ * Keyed on the upstream FORMAT, not on the Winter protocol or adapter: the suffix is a fact about
106
+ * the URL as upstream wrote it. `deepseek` is `format: "openai-responses"` at
107
+ * `https://api.deepseek.com/responses`; `openai` is `format: "openai"` at
108
+ * `https://api.openai.com/v1/chat/completions` even though its allowlist row overrides the adapter
109
+ * to Responses. Both strip their own format's suffix and land on the root their adapter expects.
110
+ *
111
+ * Exported so a test can assert the strip reproduces the five hand-authored overlay endpoints.
112
+ */
113
+ export declare const FORMAT_ENDPOINT_SUFFIX: Readonly<Record<string, string>>;
114
+ /**
115
+ * Adapter id -> the ONE protocol it speaks. The reverse of `PROTOCOL_TO_ADAPTER`, plus the adapters
116
+ * a protocol does not imply.
117
+ *
118
+ * Exported because the cross-layer gate must key on the ADAPTER, not on the provider's `protocols`
119
+ * list: resolution hands a model to `provider.adapterId` and nothing reads `protocols` at all, so a
120
+ * gate that consulted the list passed a responses-only model under a provider that merely DECLARED
121
+ * `openai-responses` while its adapter spoke Chat Completions.
122
+ */
123
+ export declare const ADAPTER_PROTOCOL: Readonly<Record<string, ProviderProtocol>>;
124
+ /**
125
+ * A model row as a LAYER carries it: everything a finished row has EXCEPT the two derived family
126
+ * fields (WS-13c §1).
127
+ *
128
+ * The two are stamped once, at assembly, by `stampFamilyFields` — and `stampFamilyFields` treats a
129
+ * value already on the row as an OVERLAY OVERRIDE it must never overwrite. So a layer that pre-filled
130
+ * them would freeze whatever it guessed: an upstream layer stamped with no families at hand would
131
+ * write `modelFamily: "other"` onto 540 rows, and the merge would then honour that "override"
132
+ * forever. The layer therefore does not carry them, and the committed `upstream-layer.json` is
133
+ * correct exactly as it stands.
134
+ */
135
+ export type UnstampedModelDescriptor = Omit<WinterModelDescriptor, "modelFamily" | "canonicalModelId">;
136
+ export interface UpstreamLayer {
137
+ $comment: string;
138
+ providers: WinterProviderDescriptor[];
139
+ models: UnstampedModelDescriptor[];
140
+ rejections: LedgerRejection[];
141
+ }
142
+ /** A rejection row as it is COMMITTED — the extractor's `Rejection` plus its upstream identity. */
143
+ export interface LedgerRejection {
144
+ upstreamId: string;
145
+ scope: Rejection["scope"];
146
+ exclusionClass: ExclusionClass;
147
+ path: string;
148
+ sourcePath: string;
149
+ reason: string;
150
+ }
151
+ export interface BuildUpstreamLayerInput {
152
+ allowlist: Allowlist;
153
+ /** Upstream provider id -> its `RegistryEntry` literal. */
154
+ registry: ReadonlyMap<string, LiteralValue>;
155
+ /** Upstream provider id -> its product-catalog category (`apikey`, `web-cookie`, …). */
156
+ categories: ReadonlyMap<string, {
157
+ category: string;
158
+ sourcePath: string;
159
+ row: LiteralValue;
160
+ }>;
161
+ /** Upstream provider id -> the repository-relative path its registry entry was read from. */
162
+ registrySourcePaths: ReadonlyMap<string, string>;
163
+ commit: string;
164
+ /** ISO-8601 instant stamped onto every piece of extracted evidence. */
165
+ observedAt: string;
166
+ /** Field/value rejections the extractor already produced, keyed by source path. */
167
+ moduleRejections: readonly Rejection[];
168
+ }
169
+ export declare class ExtractionRefusal extends Error {
170
+ constructor(message: string);
171
+ }
172
+ /**
173
+ * Build the upstream layer from the materialized literals.
174
+ *
175
+ * Order of operations matters and is deliberate:
176
+ * 1. every upstream id in the product catalog is CLASSIFIED (category -> disposition);
177
+ * 2. an allowlisted id whose category is not `candidate-pool` FAILS the run — that is WS-13 §3
178
+ * step 7's blocked class transition, and it is the only reason the allowlist is checked against
179
+ * the catalog rather than simply trusted;
180
+ * 3. surviving ids are mapped, with unknown format/auth/executor values failing the row;
181
+ * 4. everything else lands in the ledger with a class.
182
+ */
183
+ export declare function buildUpstreamLayer(input: BuildUpstreamLayerInput): UpstreamLayer;
184
+ /** Total, stable rejection ordering: a ledger diff between two upstream bumps must show CHANGES, not churn. */
185
+ export declare function compareRejections(a: LedgerRejection, b: LedgerRejection): number;
186
+ /** The overlay files a re-sync must NEVER write. Exported so the sync script's own guard and its test read the same list. */
187
+ export declare const OVERLAY_FILES: readonly ["overlay/providers.json", "overlay/models.json", "overlay/families.json"];
188
+ /**
189
+ * The overlay-wins merge, mirroring `scripts/provider-catalog.ts`.
190
+ *
191
+ * Used to VALIDATE a layer before the frozen script writes anything — never to write the committed
192
+ * catalog. Row-level, not field-level: WS-13 §7's rule is that upstream never overwrites overlay
193
+ * evidence, and a field-level merge would do exactly that for every field the overlay leaves out.
194
+ *
195
+ * WS-13c: the family stamp happens HERE and in the script, through the ONE `stampFamilyFields` both
196
+ * call — a second implementation of "which family is this" would make the standalone gate and the
197
+ * committed catalog disagree about a row while both reported success. `pipeline.test.ts` pins the
198
+ * two assemblers byte-identical on the real layers, which is what keeps the mirroring honest.
199
+ *
200
+ * `families` defaults to EMPTY, and that default is what the upstream layer's standalone check
201
+ * uses: the layer alone has none of the overlay rows the real slots point at, so validating it
202
+ * against the real families would fail `slot-model-missing` on rows that are present in the merged
203
+ * catalog and only there. With no families every row stamps `other`, which the validator accepts —
204
+ * the standalone gate is about the LAYER's own shape, and family assignment is the merged catalog's
205
+ * own gate (`bun run provider:catalog --check` plus the integrity suite).
206
+ */
207
+ export declare function mergeLayers(upstream: {
208
+ providers: WinterProviderDescriptor[];
209
+ models: UnstampedModelDescriptor[];
210
+ }, overlay: {
211
+ providers: WinterProviderDescriptor[];
212
+ models: UnstampedModelDescriptor[];
213
+ }, pin: WinterCatalog["upstream"], families?: readonly ModelFamilyDescriptor[]): WinterCatalog;
@@ -0,0 +1,55 @@
1
+ import type { FamilySlot, ModelFamilyDescriptor, WinterCatalog, WinterModelDescriptor } from "./types.js";
2
+ export declare const SLOT_NAME_RE: RegExp;
3
+ export declare const FAMILY_ID_RE: RegExp;
4
+ export declare const CLAUDE_FAMILY_ID = "claude";
5
+ export declare const OTHER_FAMILY_ID = "other";
6
+ /** D25: reserved to the `claude` family, in the pinned order. */
7
+ export declare const CLAUDE_RESERVED_SLOT_NAMES: readonly string[];
8
+ /** "$10", "10 USD", "€2", "£1", "10 dollars" — pricing lives on rows, never in a slot description. */
9
+ export declare const CURRENCY_RE: RegExp;
10
+ /** The vendor's model identity with the provider's spelling removed (WS-13c §1, R13c-2). Deterministic; a per-row overlay `canonicalModelId` overrides it. */
11
+ export declare function canonicalModelIdOf(upstreamId: string): string;
12
+ export declare function familyIdOf(canonicalModelId: string, families: readonly ModelFamilyDescriptor[]): string;
13
+ export declare function stampFamilyFields<T extends {
14
+ upstreamId: string;
15
+ canonicalModelId?: string;
16
+ modelFamily?: string;
17
+ }>(rows: readonly T[], families: readonly ModelFamilyDescriptor[]): Array<T & {
18
+ canonicalModelId: string;
19
+ modelFamily: string;
20
+ }>;
21
+ export type SlotNameResolution = {
22
+ kind: "slot";
23
+ family: ModelFamilyDescriptor;
24
+ slot: FamilySlot;
25
+ advertised: boolean;
26
+ } | {
27
+ kind: "ambiguous";
28
+ name: string;
29
+ candidates: string[];
30
+ } | {
31
+ kind: "unknown";
32
+ name: string;
33
+ };
34
+ /** WS-13c §3 acceptance: active set first; the Claude names always into `claude`; a unique foreign name; else ambiguous/unknown. */
35
+ export declare function resolveSlotName(name: string, activeFamilyId: string | undefined, families: readonly ModelFamilyDescriptor[]): SlotNameResolution;
36
+ export declare function familyOfModelKey(catalog: WinterCatalog, modelKey: string): ModelFamilyDescriptor | undefined;
37
+ /**
38
+ * Can this row serve a slot at all? (WS-13c §4 step 1.)
39
+ *
40
+ * ONE predicate, deliberately, and it is what fix round 1's I-3 closed: the validator's
41
+ * `slot-model-missing` check and `rowsForCanonicalId` had drifted into two answers — the validator
42
+ * asked only about `status`, the resolver also required a chat/responses endpoint. A slot whose only
43
+ * rows were `endpoints: ["embeddings"]` therefore VALIDATED and resolved to nothing, which makes the
44
+ * natural inference "the catalog validated, so every slot has a candidate row" false in exactly the
45
+ * place a lane relies on it.
46
+ *
47
+ * Structurally typed rather than taking a `WinterModelDescriptor`, because the validator's caller
48
+ * holds `unknown` JSON it has already shape-checked, not a narrowed row.
49
+ */
50
+ export declare function isSlotServableRow(row: {
51
+ status: string;
52
+ endpoints: readonly string[];
53
+ }): boolean;
54
+ /** Candidate rows for a slot (WS-13c §4 step 1). The SAME predicate `validateCatalog` refuses a slot on. */
55
+ export declare function rowsForCanonicalId(catalog: WinterCatalog, canonicalModelId: string): WinterModelDescriptor[];
@@ -0,0 +1,30 @@
1
+ import {
2
+ SLOT_NAME_RE2,
3
+ FAMILY_ID_RE2,
4
+ CLAUDE_FAMILY_ID2,
5
+ OTHER_FAMILY_ID2,
6
+ CLAUDE_RESERVED_SLOT_NAMES2,
7
+ CURRENCY_RE2,
8
+ canonicalModelIdOf2,
9
+ familyIdOf2,
10
+ stampFamilyFields2,
11
+ resolveSlotName2,
12
+ familyOfModelKey2,
13
+ isSlotServableRow2,
14
+ rowsForCanonicalId2
15
+ } from "./index-t40pzh81.js";
16
+ export {
17
+ CLAUDE_FAMILY_ID2 as CLAUDE_FAMILY_ID,
18
+ CLAUDE_RESERVED_SLOT_NAMES2 as CLAUDE_RESERVED_SLOT_NAMES,
19
+ CURRENCY_RE2 as CURRENCY_RE,
20
+ FAMILY_ID_RE2 as FAMILY_ID_RE,
21
+ OTHER_FAMILY_ID2 as OTHER_FAMILY_ID,
22
+ SLOT_NAME_RE2 as SLOT_NAME_RE,
23
+ canonicalModelIdOf2 as canonicalModelIdOf,
24
+ familyIdOf2 as familyIdOf,
25
+ familyOfModelKey2 as familyOfModelKey,
26
+ isSlotServableRow2 as isSlotServableRow,
27
+ resolveSlotName2 as resolveSlotName,
28
+ rowsForCanonicalId2 as rowsForCanonicalId,
29
+ stampFamilyFields2 as stampFamilyFields
30
+ };