@chartcoach/catalog 0.1.7 → 0.2.0

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.
Files changed (72) hide show
  1. package/README.md +324 -38
  2. package/dist/duckdb-wasm.d.ts +8 -0
  3. package/dist/duckdb-wasm.js +22 -0
  4. package/dist/duckdb.d.ts +8 -0
  5. package/dist/duckdb.js +9 -0
  6. package/dist/index-cache-Cx1gIK6N.js +280 -0
  7. package/dist/index.d.ts +40 -8
  8. package/dist/index.js +22 -7
  9. package/dist/json-CBWjz9b4.js +29 -0
  10. package/dist/model-De0MF-zg.d.ts +255 -0
  11. package/dist/node.d.ts +15 -0
  12. package/dist/node.js +218 -0
  13. package/dist/open-Nnzkr_bE.d.ts +15 -0
  14. package/dist/open-Uu3-r8kp.js +1244 -0
  15. package/dist/registration-CgQGMUIH.d.ts +6 -0
  16. package/dist/registration-DXhS7vgr.js +41 -0
  17. package/dist/tables-DcgKnXwF.js +389 -0
  18. package/package.json +47 -18
  19. package/src/catalog/artifacts.ts +309 -100
  20. package/src/catalog/description.ts +198 -0
  21. package/src/catalog/errors.ts +26 -1
  22. package/src/catalog/identity.ts +69 -0
  23. package/src/catalog/json.ts +32 -0
  24. package/src/catalog/labels.ts +8 -6
  25. package/src/catalog/manifest.ts +81 -20
  26. package/src/catalog/markdown.ts +2 -2
  27. package/src/catalog/model.ts +222 -37
  28. package/src/catalog/open.ts +428 -0
  29. package/src/catalog/parquet.ts +164 -0
  30. package/src/catalog/profile-layout.ts +86 -0
  31. package/src/catalog/profile.ts +345 -0
  32. package/src/catalog/query.ts +125 -0
  33. package/src/catalog/read.ts +101 -0
  34. package/src/catalog/references.ts +336 -0
  35. package/src/catalog/registration.ts +74 -0
  36. package/src/catalog/tables.ts +250 -0
  37. package/src/catalog/wire.ts +38 -65
  38. package/src/duckdb-wasm.ts +33 -0
  39. package/src/duckdb.ts +18 -0
  40. package/src/index.ts +51 -20
  41. package/src/node/index-cache.ts +432 -0
  42. package/src/node.ts +347 -0
  43. package/dist/catalog/artifacts.d.ts +0 -28
  44. package/dist/catalog/artifacts.d.ts.map +0 -1
  45. package/dist/catalog/artifacts.js +0 -89
  46. package/dist/catalog/chartcoach-defaults.d.ts +0 -17
  47. package/dist/catalog/chartcoach-defaults.d.ts.map +0 -1
  48. package/dist/catalog/chartcoach-defaults.js +0 -8
  49. package/dist/catalog/errors.d.ts +0 -4
  50. package/dist/catalog/errors.d.ts.map +0 -1
  51. package/dist/catalog/errors.js +0 -6
  52. package/dist/catalog/labels.d.ts +0 -9
  53. package/dist/catalog/labels.d.ts.map +0 -1
  54. package/dist/catalog/labels.js +0 -23
  55. package/dist/catalog/load-parquet-core.d.ts +0 -15
  56. package/dist/catalog/load-parquet-core.d.ts.map +0 -1
  57. package/dist/catalog/load-parquet-core.js +0 -45
  58. package/dist/catalog/manifest.d.ts +0 -15
  59. package/dist/catalog/manifest.d.ts.map +0 -1
  60. package/dist/catalog/manifest.js +0 -143
  61. package/dist/catalog/markdown.d.ts +0 -3
  62. package/dist/catalog/markdown.d.ts.map +0 -1
  63. package/dist/catalog/markdown.js +0 -13
  64. package/dist/catalog/model.d.ts +0 -33
  65. package/dist/catalog/model.d.ts.map +0 -1
  66. package/dist/catalog/model.js +0 -67
  67. package/dist/catalog/wire.d.ts +0 -18
  68. package/dist/catalog/wire.d.ts.map +0 -1
  69. package/dist/catalog/wire.js +0 -78
  70. package/dist/index.d.ts.map +0 -1
  71. package/src/catalog/chartcoach-defaults.ts +0 -17
  72. package/src/catalog/load-parquet-core.ts +0 -78
@@ -0,0 +1,1244 @@
1
+ import { a as CatalogError, i as parseJson, n as isJsonObject, r as isJsonString, t as isJsonNumber } from "./json-CBWjz9b4.js";
2
+ import { a as citationRecords, c as catalogEntriesDigest, d as sha256Bytes, f as normalizeLabel, l as compareUnicode, n as catalogTableInfo, o as sourceRecords, p as parseLabel, s as canonicalJson, t as catalogTable, u as manifestDigest } from "./tables-DcgKnXwF.js";
3
+ import { parquetReadObjects } from "hyparquet";
4
+ import { compressors } from "hyparquet-compressors";
5
+ //#region src/catalog/manifest.ts
6
+ const REQUIRED_MANIFEST_HEADINGS = ["Section Roles", "Label Families"];
7
+ const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
8
+ const codeSpanPattern = /`([^`\n]+)`/g;
9
+ function manifestDefinitions() {
10
+ return Object.create(null);
11
+ }
12
+ function parseCatalogManifest(markdown) {
13
+ const requiredSeen = /* @__PURE__ */ new Set();
14
+ const definitions = {
15
+ "Section Roles": manifestDefinitions(),
16
+ "Label Families": manifestDefinitions()
17
+ };
18
+ let currentHeading;
19
+ let currentName;
20
+ let currentLines = [];
21
+ function flushDefinition() {
22
+ if (currentHeading !== "Section Roles" && currentHeading !== "Label Families") {
23
+ currentLines = [];
24
+ return;
25
+ }
26
+ if (currentName === void 0) {
27
+ currentLines = [];
28
+ return;
29
+ }
30
+ const description = currentLines.join("\n").trim();
31
+ if (!description) throw new CatalogError(`Manifest definition ${currentHeading}/${currentName} must include prose.`);
32
+ definitions[currentHeading][currentName] = {
33
+ name: currentName,
34
+ description,
35
+ examples: Array.from(description.matchAll(codeSpanPattern), (match) => match[1] ?? "")
36
+ };
37
+ currentName = void 0;
38
+ currentLines = [];
39
+ }
40
+ for (const line of markdown.split("\n")) {
41
+ const match = headingPattern.exec(line);
42
+ if (!match) {
43
+ if (currentName !== void 0) currentLines.push(line);
44
+ continue;
45
+ }
46
+ const level = match[1].length;
47
+ const title = match[2].trim();
48
+ if (level === 2) {
49
+ flushDefinition();
50
+ currentHeading = title;
51
+ currentName = void 0;
52
+ currentLines = [];
53
+ if (title === "Section Roles" || title === "Label Families") requiredSeen.add(title);
54
+ continue;
55
+ }
56
+ if (level === 3 && (currentHeading === "Section Roles" || currentHeading === "Label Families")) {
57
+ flushDefinition();
58
+ if (!title) throw new CatalogError(`Manifest heading ${currentHeading} contains an empty subheading.`);
59
+ currentName = title;
60
+ currentLines = [];
61
+ continue;
62
+ }
63
+ if (currentName !== void 0) currentLines.push(line);
64
+ }
65
+ flushDefinition();
66
+ const missing = REQUIRED_MANIFEST_HEADINGS.filter((heading) => !requiredSeen.has(heading));
67
+ if (missing.length > 0) throw new CatalogError(`MANIFEST.md is missing required heading(s): ${missing.join(", ")}.`);
68
+ for (const heading of REQUIRED_MANIFEST_HEADINGS) if (Object.keys(definitions[heading]).length === 0) throw new CatalogError(`Manifest heading ${heading} must contain definitions.`);
69
+ validateLabelFamilyExamples(Object.values(definitions["Label Families"]));
70
+ return copyCatalogManifest({
71
+ markdown: markdown.endsWith("\n") ? markdown : `${markdown}\n`,
72
+ sectionRoles: definitions["Section Roles"],
73
+ labelFamilies: definitions["Label Families"]
74
+ });
75
+ }
76
+ function copyCatalogManifest(manifest) {
77
+ return Object.freeze({
78
+ markdown: manifest.markdown,
79
+ sectionRoles: copyDefinitions(manifest.sectionRoles),
80
+ labelFamilies: copyDefinitions(manifest.labelFamilies)
81
+ });
82
+ }
83
+ function validateManifestCoverage(guidelines, manifest) {
84
+ const usedRoles = /* @__PURE__ */ new Set();
85
+ const usedFamilies = /* @__PURE__ */ new Set();
86
+ for (const guideline of guidelines) {
87
+ for (const section of guideline.sections) {
88
+ const role = section.role.trim();
89
+ if (!role) throw new CatalogError("Section role values must not be empty.");
90
+ if (role === "__dangling__") continue;
91
+ usedRoles.add(role);
92
+ }
93
+ for (const label of guideline.labels) usedFamilies.add(parseLabel(label, `label ${JSON.stringify(label)}`).family);
94
+ }
95
+ const missingRoles = Array.from(usedRoles).filter((role) => !Object.hasOwn(manifest.sectionRoles, role)).sort();
96
+ const missingFamilies = Array.from(usedFamilies).filter((family) => !Object.hasOwn(manifest.labelFamilies, family)).sort();
97
+ const errors = [];
98
+ if (missingRoles.length > 0) errors.push(`undefined section role(s): ${missingRoles.join(", ")}`);
99
+ if (missingFamilies.length > 0) errors.push(`undefined label family/families: ${missingFamilies.join(", ")}`);
100
+ if (errors.length > 0) throw new CatalogError(`Catalog manifest validation failed: ${errors.join("; ")}.`);
101
+ }
102
+ function validateLabelFamilyExamples(definitions) {
103
+ for (const definition of definitions) {
104
+ const familyExamples = [];
105
+ const invalidExamples = [];
106
+ for (const example of definition.examples) {
107
+ let parsed;
108
+ try {
109
+ parsed = parseLabel(example, `manifest label example ${JSON.stringify(example)}`);
110
+ } catch {
111
+ continue;
112
+ }
113
+ if (parsed.family === definition.name) familyExamples.push(parsed.value);
114
+ else if (example.includes(":")) invalidExamples.push(example);
115
+ }
116
+ if (invalidExamples.length > 0) throw new CatalogError(`Label family ${definition.name} has example(s) from another family: ${invalidExamples.join(", ")}.`);
117
+ if (familyExamples.length === 0) throw new CatalogError(`Label family ${definition.name} must include at least one label example.`);
118
+ }
119
+ }
120
+ function copyDefinitions(definitions) {
121
+ const owned = manifestDefinitions();
122
+ for (const [name, definition] of Object.entries(definitions)) owned[name] = Object.freeze({
123
+ name: definition.name,
124
+ description: definition.description,
125
+ examples: Object.freeze([...definition.examples])
126
+ });
127
+ return Object.freeze(owned);
128
+ }
129
+ //#endregion
130
+ //#region src/catalog/description.ts
131
+ const identityCache = /* @__PURE__ */ new WeakMap();
132
+ async function describeCatalog(catalog, context, options = {}) {
133
+ if (Object.prototype.toString.call(options) !== "[object Object]") throw new CatalogError("Description options must be an object.");
134
+ const identity = await catalogIdentity(catalog);
135
+ const selected = selectProfile(context.profiles, options.profile);
136
+ let profile = null;
137
+ if (selected) {
138
+ options.signal?.throwIfAborted();
139
+ const metadata = await loadProfile(context, selected, options.signal);
140
+ if (metadata.entries_digest !== identity.entriesDigest) throw new CatalogError("Profile entries digest does not match the catalog entries.", {
141
+ code: "incompatible_profile",
142
+ details: { profile: selected.name }
143
+ });
144
+ if (metadata.manifest_digest !== identity.manifestDigest) throw new CatalogError("Profile manifest digest does not match MANIFEST.md.", {
145
+ code: "incompatible_profile",
146
+ details: { profile: selected.name }
147
+ });
148
+ if (metadata.projection === null !== (selected.projection === null)) throw new CatalogError("Profile projection metadata does not match the release inventory.", {
149
+ code: "incompatible_profile",
150
+ details: { profile: selected.name }
151
+ });
152
+ context.profileCache.set(selected.name, metadata);
153
+ profile = profileInfo(selected.name, metadata);
154
+ }
155
+ return Object.freeze({
156
+ resolved_location: context.resolvedLocation,
157
+ release_digest: context.releaseDigest,
158
+ entries_digest: identity.entriesDigest,
159
+ manifest_digest: identity.manifestDigest,
160
+ tables: catalogTableInfo(catalog),
161
+ section_roles: vocabulary(Object.values(catalog.manifest.sectionRoles)),
162
+ label_families: vocabulary(Object.values(catalog.manifest.labelFamilies)),
163
+ profiles: Object.freeze(context.profiles.map((item) => item.name)),
164
+ profile
165
+ });
166
+ }
167
+ async function catalogIdentity(catalog) {
168
+ const cached = identityCache.get(catalog);
169
+ if (cached) return cached;
170
+ const [entriesDigest, catalogManifestDigest] = await Promise.all([catalogEntriesDigest(catalog.guidelines), manifestDigest(catalog.manifest.markdown)]);
171
+ const identity = Object.freeze({
172
+ entriesDigest,
173
+ manifestDigest: catalogManifestDigest
174
+ });
175
+ identityCache.set(catalog, identity);
176
+ return identity;
177
+ }
178
+ function selectProfile(profiles, name) {
179
+ if (name === void 0) return void 0;
180
+ if (!name) throw new CatalogError("Profile must be a non-empty string.");
181
+ const profile = profiles.find((item) => item.name === name);
182
+ if (profile) return profile;
183
+ throw new CatalogError(`Unknown profile: ${name}`, {
184
+ code: "lookup",
185
+ details: {
186
+ profile: name,
187
+ available: profiles.map((item) => item.name)
188
+ },
189
+ hints: [`Available profiles: ${profiles.map((item) => JSON.stringify(item.name)).join(", ") || "none"}.`]
190
+ });
191
+ }
192
+ async function loadProfile(context, profile, signal) {
193
+ const cached = context.profileCache.get(profile.name);
194
+ if (cached) return cached;
195
+ if (!context.profileLoader) throw new CatalogError("Profile metadata loading is unavailable for caller-provided bytes.", {
196
+ code: "unavailable_capability",
197
+ details: { profile: profile.name },
198
+ hints: ["Use openCatalog with an HTTP or HTTPS release URL."]
199
+ });
200
+ return context.profileLoader(profile, signal);
201
+ }
202
+ function profileInfo(name, metadata) {
203
+ return Object.freeze({
204
+ name,
205
+ profile_schema_version: metadata.schema_version,
206
+ documents_version: metadata.documents_version,
207
+ embedding_functions: metadata.embedding_functions,
208
+ dimensions: metadata.dimensions,
209
+ distance_metric: metadata.distance_metric,
210
+ python_requirements: metadata.python_requirements,
211
+ lancedb_version: metadata.lancedb_version,
212
+ projection: metadata.projection
213
+ });
214
+ }
215
+ function vocabulary(definitions) {
216
+ return Object.freeze(definitions.map((definition) => Object.freeze({
217
+ name: definition.name,
218
+ description: definition.description,
219
+ examples: Object.freeze([...definition.examples])
220
+ })));
221
+ }
222
+ //#endregion
223
+ //#region src/catalog/query.ts
224
+ function queryCatalog(catalog, options = {}) {
225
+ if (Object.prototype.toString.call(options) !== "[object Object]") throw new CatalogError("Query options must be an object.");
226
+ const ids = stringArray(options.ids, "ids");
227
+ const labels = stringArray(options.labels, "labels");
228
+ const labelPrefixes = stringArray(options.labelPrefixes, "labelPrefixes");
229
+ const limit = options.limit ?? 50;
230
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new CatalogError("Query limit must be at least 1.");
231
+ validateLabels(catalog, labels);
232
+ validateLabelPrefixes(catalog, labelPrefixes);
233
+ const selected = ids.length > 0 ? ids.map((id) => catalog.require(id)) : [...catalog.guidelines];
234
+ let contains;
235
+ if (options.contains !== void 0) {
236
+ if (!isJsonString(options.contains)) throw new CatalogError("Query contains must be a string.");
237
+ if (options.contains.length > 0) {
238
+ contains = normalizeSearchText(options.contains);
239
+ if (!contains) throw new CatalogError("contains must not consist only of whitespace, hyphens, or underscores.");
240
+ }
241
+ }
242
+ return Object.freeze(selected.filter((guideline) => labels.every((label) => guideline.labels.includes(label))).filter((guideline) => labelPrefixes.every((prefix) => guideline.labels.some((label) => label.startsWith(prefix)))).filter((guideline) => contains === void 0 || matchesText(guideline, contains)).slice(0, limit).map((guideline) => Object.freeze({
243
+ id: guideline.id,
244
+ title: guideline.title,
245
+ description: guideline.description,
246
+ labels: Object.freeze([...guideline.labels])
247
+ })));
248
+ }
249
+ function validateLabels(catalog, labels) {
250
+ const available = new Set(catalog.labels());
251
+ const missing = Array.from(new Set(labels.filter((label) => !available.has(label)))).sort();
252
+ if (missing.length === 0) return;
253
+ throw new CatalogError(`Unknown label(s): ${missing.join(", ")}`, {
254
+ code: "lookup",
255
+ details: { labels: missing },
256
+ hints: ["Call `await catalog.describe()` to inspect label families."]
257
+ });
258
+ }
259
+ function validateLabelPrefixes(catalog, prefixes) {
260
+ const available = catalog.labels();
261
+ const missing = Array.from(new Set(prefixes.filter((prefix) => !available.some((label) => label.startsWith(prefix))))).sort();
262
+ if (missing.length === 0) return;
263
+ throw new CatalogError(`No labels match prefix(es): ${missing.join(", ")}`, {
264
+ code: "lookup",
265
+ details: { label_prefixes: missing },
266
+ hints: ["Call `await catalog.describe()` to inspect label families."]
267
+ });
268
+ }
269
+ function stringArray(value, name) {
270
+ if (value === void 0) return Object.freeze([]);
271
+ if (!Array.isArray(value) || !value.every(isJsonString)) throw new CatalogError(`${name} must be an array of strings.`);
272
+ return value;
273
+ }
274
+ function matchesText(guideline, contains) {
275
+ return [
276
+ guideline.id,
277
+ guideline.title,
278
+ guideline.description
279
+ ].some((value) => normalizeSearchText(value).includes(contains));
280
+ }
281
+ function normalizeSearchText(value) {
282
+ return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
283
+ }
284
+ //#endregion
285
+ //#region src/catalog/read.ts
286
+ function readCatalog(catalog, options) {
287
+ if (Object.prototype.toString.call(options) !== "[object Object]") throw new CatalogError("Read options must be an object.");
288
+ if (!Array.isArray(options.ids) || !options.ids.every(isJsonString)) throw new CatalogError("ids must be an array of strings.");
289
+ const roles = options.roles ?? [];
290
+ if (!Array.isArray(roles) || !roles.every(isJsonString)) throw new CatalogError("roles must be an array of strings.");
291
+ const sourceDetail = options.sourceDetail ?? "minimal";
292
+ if (sourceDetail !== "none" && sourceDetail !== "minimal" && sourceDetail !== "full") throw new CatalogError(`Unknown source detail: ${JSON.stringify(sourceDetail)}.`, { hints: ["Choose one of: none, minimal, full."] });
293
+ const missingRoles = Array.from(new Set(roles.filter((role) => !Object.hasOwn(catalog.manifest.sectionRoles, role)))).sort();
294
+ if (missingRoles.length > 0) throw new CatalogError(`Unknown section role(s): ${missingRoles.join(", ")}`, {
295
+ code: "lookup",
296
+ details: { roles: missingRoles },
297
+ hints: ["Call `await catalog.describe()` to inspect section roles."]
298
+ });
299
+ if (options.ids.length === 0) return Object.freeze([]);
300
+ const roleSet = roles.length > 0 ? new Set(roles) : void 0;
301
+ return Object.freeze(options.ids.map((id) => {
302
+ const guideline = catalog.require(id);
303
+ const sections = Object.freeze(guideline.sections.filter((section) => roleSet === void 0 || roleSet.has(section.role)).map((section) => Object.freeze({
304
+ role: section.role,
305
+ title: section.title,
306
+ content: section.content
307
+ })));
308
+ const record = {
309
+ id: guideline.id,
310
+ title: guideline.title,
311
+ description: guideline.description,
312
+ labels: Object.freeze([...guideline.labels]),
313
+ sections,
314
+ sources: sourceDetail === "none" ? Object.freeze([]) : sourceRecords(catalog, guideline.id, sourceDetail)
315
+ };
316
+ return Object.freeze(sourceDetail === "full" ? {
317
+ ...record,
318
+ references: Object.freeze([...guideline.references])
319
+ } : record);
320
+ }));
321
+ }
322
+ //#endregion
323
+ //#region src/catalog/profile-layout.ts
324
+ const profileFiles = /* @__PURE__ */ new Set([
325
+ "profile.json",
326
+ "index.tar.gz",
327
+ "documents.parquet",
328
+ "projection.parquet"
329
+ ]);
330
+ const profileIdPattern = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
331
+ const windowsDeviceNames = /* @__PURE__ */ new Set([
332
+ "CON",
333
+ "PRN",
334
+ "AUX",
335
+ "NUL",
336
+ "CONIN$",
337
+ "CONOUT$"
338
+ ]);
339
+ function releaseProfiles(release) {
340
+ const profiles = /* @__PURE__ */ new Map();
341
+ for (const path of Object.keys(release.artifacts)) {
342
+ const parts = path.split("/");
343
+ if (parts[0] !== "profiles") continue;
344
+ if (parts.length < 3) throw new CatalogError(`Invalid profile artifact path: ${JSON.stringify(path)}.`);
345
+ const filename = parts.at(-1);
346
+ if (!profileFiles.has(filename)) throw new CatalogError(`Unknown profile artifact: ${JSON.stringify(path)}.`);
347
+ const profile = parts.slice(1, -1).join("/");
348
+ const artifacts = profiles.get(profile) ?? /* @__PURE__ */ new Set();
349
+ artifacts.add(filename);
350
+ profiles.set(profile, artifacts);
351
+ }
352
+ const result = [];
353
+ for (const [profile, artifacts] of profiles) {
354
+ if (!isProfileId(profile)) throw new CatalogError("Profile ID must be a lowercase portable single-component name.");
355
+ if (!artifacts.has("profile.json")) throw new CatalogError(`Profile ${JSON.stringify(profile)} is missing profile.json.`);
356
+ if (!artifacts.has("index.tar.gz")) throw new CatalogError(`Profile ${JSON.stringify(profile)} is missing index.tar.gz.`);
357
+ const root = `profiles/${profile}`;
358
+ result.push(Object.freeze({
359
+ name: profile,
360
+ metadata: `${root}/profile.json`,
361
+ index: `${root}/index.tar.gz`,
362
+ documents: artifacts.has("documents.parquet") ? `${root}/documents.parquet` : null,
363
+ projection: artifacts.has("projection.parquet") ? `${root}/projection.parquet` : null
364
+ }));
365
+ }
366
+ return Object.freeze(result.sort((left, right) => left.name < right.name ? -1 : 1));
367
+ }
368
+ function releaseProfileNames(release) {
369
+ return Object.freeze(releaseProfiles(release).map((profile) => profile.name));
370
+ }
371
+ function isProfileId(value) {
372
+ if (!profileIdPattern.test(value) || value.includes("/")) return false;
373
+ const basename = value.split(".", 1)[0].toUpperCase();
374
+ return !windowsDeviceNames.has(basename) && !/^(?:COM|LPT)[1-9]$/.test(basename);
375
+ }
376
+ //#endregion
377
+ //#region src/catalog/artifacts.ts
378
+ const CATALOG_ARTIFACT_BASE_URL = "https://artifacts.chartcoach.dev/";
379
+ const RELEASE_SCHEMA_VERSION = 1;
380
+ const REQUIRED_ARTIFACTS = ["MANIFEST.md", "entries.parquet"];
381
+ const FORBIDDEN_PATH_CHARACTERS = /* @__PURE__ */ new Set("<>:\"\\|?*#%");
382
+ const WINDOWS_DEVICE_NAMES = /* @__PURE__ */ new Set([
383
+ "CON",
384
+ "PRN",
385
+ "AUX",
386
+ "NUL",
387
+ "CONIN$",
388
+ "CONOUT$"
389
+ ]);
390
+ function parseCatalogRelease(value) {
391
+ const record = requireRecord(value, "Catalog release");
392
+ requireExactKeys(record, [
393
+ "schema_version",
394
+ "digest",
395
+ "artifacts"
396
+ ], "Catalog release");
397
+ requireSchemaVersion(record);
398
+ const artifactRecords = requireRecord(record.artifacts, "Catalog artifacts");
399
+ const artifacts = Object.create(null);
400
+ for (const [path, artifact] of Object.entries(artifactRecords).map(([path, value]) => [requireRelativePath(path, "Catalog artifact path"), parseReleaseArtifact(value)]).sort(([left], [right]) => compareUnicode(left, right))) artifacts[path] = artifact;
401
+ validateArtifactPaths(Object.keys(artifacts));
402
+ for (const path of REQUIRED_ARTIFACTS) if (!Object.hasOwn(artifacts, path)) throw new CatalogError(`Catalog release is missing artifact: ${path}.`);
403
+ return Object.freeze({
404
+ schema_version: RELEASE_SCHEMA_VERSION,
405
+ digest: requiredSha256(record, "digest", "Catalog release"),
406
+ artifacts: Object.freeze(artifacts)
407
+ });
408
+ }
409
+ function copyCatalogRelease(release) {
410
+ return parseCatalogRelease({
411
+ schema_version: release.schema_version,
412
+ digest: release.digest,
413
+ artifacts: Object.fromEntries(Object.entries(release.artifacts).map(([path, artifact]) => [path, {
414
+ sha256: artifact.sha256,
415
+ bytes: artifact.bytes
416
+ }]))
417
+ });
418
+ }
419
+ async function assertReleaseDigest(release, expected = release.digest, label = "Catalog release") {
420
+ const actual = await catalogReleaseDigest(release);
421
+ if (release.digest !== actual) throw new CatalogError(`${label} digest ${release.digest} does not match its artifact set.`, {
422
+ code: "integrity",
423
+ details: {
424
+ expected_digest: release.digest,
425
+ actual_digest: actual
426
+ }
427
+ });
428
+ if (release.digest !== expected) throw new CatalogError(`${label} digest ${release.digest} does not match ${expected}.`, {
429
+ code: "integrity",
430
+ details: {
431
+ expected_digest: expected,
432
+ actual_digest: release.digest
433
+ }
434
+ });
435
+ }
436
+ async function assertReleaseArtifactBytes(path, artifact, data) {
437
+ const bytes = bytesView(data);
438
+ if (bytes.byteLength !== artifact.bytes) throw new CatalogError(`Catalog artifact byte count mismatch: ${path}`, {
439
+ code: "integrity",
440
+ details: {
441
+ path,
442
+ expected_bytes: artifact.bytes,
443
+ actual_bytes: bytes.byteLength
444
+ }
445
+ });
446
+ const actual = await sha256Bytes(bytes);
447
+ if (actual !== artifact.sha256) throw new CatalogError(`Catalog artifact SHA-256 mismatch: ${path}`, {
448
+ code: "integrity",
449
+ details: {
450
+ path,
451
+ expected_sha256: artifact.sha256,
452
+ actual_sha256: actual
453
+ }
454
+ });
455
+ }
456
+ function catalogUrl() {
457
+ return new URL("catalog.json", CATALOG_ARTIFACT_BASE_URL).toString();
458
+ }
459
+ function releaseArtifact(release, path) {
460
+ const artifact = release.artifacts[path];
461
+ if (artifact === void 0) throw new CatalogError(`Catalog release is missing artifact: ${path}.`, {
462
+ code: "integrity",
463
+ details: { path }
464
+ });
465
+ return artifact;
466
+ }
467
+ function releaseArtifactUrl(releaseUrl, path) {
468
+ return new URL(path, releaseUrl).toString();
469
+ }
470
+ function assertReleaseLocation(releaseUrl, release) {
471
+ const url = requireReleaseUrl(releaseUrl);
472
+ if (url.pathname.split("/").at(-1) !== "release.json") return;
473
+ const parent = url.pathname.split("/").at(-2);
474
+ if (parent && /^[a-f0-9]{64}$/.test(parent) && parent !== release.digest) throw new CatalogError("Catalog release digest does not match its digest-addressed location.", {
475
+ code: "integrity",
476
+ details: {
477
+ expected_digest: parent,
478
+ actual_digest: release.digest
479
+ }
480
+ });
481
+ }
482
+ function sanitizeReleaseUrl(value) {
483
+ const url = requireReleaseUrl(value);
484
+ url.username = "";
485
+ url.password = "";
486
+ url.search = "";
487
+ url.hash = "";
488
+ return url.toString();
489
+ }
490
+ function requireReleaseUrl(value) {
491
+ try {
492
+ return new URL(value);
493
+ } catch {
494
+ throw new CatalogError("Catalog release URL must be absolute.");
495
+ }
496
+ }
497
+ function parseReleaseArtifact(value) {
498
+ const record = requireRecord(value, "Catalog release artifact");
499
+ requireExactKeys(record, ["sha256", "bytes"], "Catalog release artifact");
500
+ return Object.freeze({
501
+ sha256: requiredSha256(record, "sha256", "Catalog release artifact"),
502
+ bytes: requiredInteger(record, "bytes", "Catalog release artifact")
503
+ });
504
+ }
505
+ function validateArtifactPaths(paths) {
506
+ const seen = /* @__PURE__ */ new Map();
507
+ for (const path of paths) {
508
+ const folded = asciiLower(path);
509
+ const previous = seen.get(folded);
510
+ if (previous !== void 0) throw new CatalogError(`Catalog artifact paths collide: ${previous}, ${path}.`);
511
+ seen.set(folded, path);
512
+ }
513
+ for (const [folded, path] of seen) {
514
+ const parts = folded.split("/");
515
+ if (folded === "release.json") throw new CatalogError(`Catalog artifact path is reserved: ${path}.`);
516
+ for (let length = 1; length < parts.length; length += 1) {
517
+ const ancestor = parts.slice(0, length).join("/");
518
+ const previous = seen.get(ancestor);
519
+ if (previous !== void 0) throw new CatalogError(`Catalog artifact paths collide: ${previous}, ${path}.`);
520
+ }
521
+ }
522
+ }
523
+ async function catalogReleaseDigest(release) {
524
+ return sha256Bytes(new TextEncoder().encode(canonicalJson({
525
+ schema_version: release.schema_version,
526
+ artifacts: release.artifacts
527
+ })));
528
+ }
529
+ function bytesView(data) {
530
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
531
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
532
+ }
533
+ function requireRecord(value, label) {
534
+ if (!isJsonObject(value)) throw new CatalogError(`${label} must be an object.`);
535
+ return value;
536
+ }
537
+ function requireExactKeys(value, expected, label) {
538
+ const actual = Object.keys(value);
539
+ const missing = expected.filter((key) => !actual.includes(key)).sort();
540
+ const unexpected = actual.filter((key) => !expected.includes(key)).sort();
541
+ if (missing.length > 0) throw new CatalogError(`${label} is missing fields: ${missing.join(", ")}.`);
542
+ if (unexpected.length > 0) throw new CatalogError(`${label} has unsupported fields: ${unexpected.join(", ")}.`);
543
+ }
544
+ function requireSchemaVersion(value) {
545
+ if (requiredInteger(value, "schema_version", "Catalog release") !== RELEASE_SCHEMA_VERSION) throw new CatalogError(`Catalog release schema_version must be ${RELEASE_SCHEMA_VERSION}.`);
546
+ }
547
+ function requireRelativePath(path, label) {
548
+ const parts = path.split("/");
549
+ if (!pathIsAscii(path) || parts.some((part) => !isPortablePathSegment(part))) throw new CatalogError(`${label} must be relative: ${path}`);
550
+ return path;
551
+ }
552
+ function isPortablePathSegment(value) {
553
+ if (value === "" || value === "." || value === ".." || value.endsWith(".") || Array.from(value).some((character) => FORBIDDEN_PATH_CHARACTERS.has(character))) return false;
554
+ const basename = value.split(".", 1)[0].toUpperCase();
555
+ return !WINDOWS_DEVICE_NAMES.has(basename) && !/^(?:COM|LPT)[1-9]$/.test(basename);
556
+ }
557
+ function requiredString(value, key, label) {
558
+ const raw = value[key];
559
+ if (!isJsonString(raw) || raw.length === 0) throw new CatalogError(`${label} ${key} must be a string.`);
560
+ return raw;
561
+ }
562
+ function requiredInteger(value, key, label) {
563
+ const raw = value[key];
564
+ if (!isJsonNumber(raw) || !Number.isSafeInteger(raw) || raw < 0) throw new CatalogError(`${label} ${key} must be an integer from 0 through ${Number.MAX_SAFE_INTEGER}.`);
565
+ return raw;
566
+ }
567
+ function requiredSha256(value, key, label) {
568
+ const digest = requiredString(value, key, label);
569
+ requireSha256$1(digest, `${label} ${key}`);
570
+ return digest;
571
+ }
572
+ function requireSha256$1(value, label) {
573
+ if (!/^[a-f0-9]{64}$/.test(value)) throw new CatalogError(`${label} must be a lowercase SHA-256 digest.`);
574
+ }
575
+ function pathIsAscii(value) {
576
+ if (value.length === 0) return false;
577
+ for (const character of value) {
578
+ const code = character.charCodeAt(0);
579
+ if (code < 33 || code > 126) return false;
580
+ }
581
+ return true;
582
+ }
583
+ function asciiLower(value) {
584
+ return value.replace(/[A-Z]/g, (character) => character.toLowerCase());
585
+ }
586
+ //#endregion
587
+ //#region src/catalog/model.ts
588
+ const releaseByCatalog = /* @__PURE__ */ new WeakMap();
589
+ const descriptionByCatalog = /* @__PURE__ */ new WeakMap();
590
+ var Catalog = class {
591
+ guidelines;
592
+ manifest;
593
+ byId;
594
+ constructor(guidelines, manifest) {
595
+ const records = Array.from(guidelines, copyGuideline);
596
+ const ownedManifest = copyCatalogManifest(manifest);
597
+ const byId = /* @__PURE__ */ new Map();
598
+ for (const guideline of records) {
599
+ if (byId.has(guideline.id)) throw new CatalogError(`Catalog contains duplicate guideline entry ID: ${guideline.id}.`);
600
+ byId.set(guideline.id, guideline);
601
+ }
602
+ validateManifestCoverage(records, ownedManifest);
603
+ this.guidelines = Object.freeze(records);
604
+ this.manifest = ownedManifest;
605
+ this.byId = byId;
606
+ Object.freeze(this);
607
+ }
608
+ get release() {
609
+ return releaseByCatalog.get(this)?.release;
610
+ }
611
+ get releaseUrl() {
612
+ return releaseByCatalog.get(this)?.releaseUrl;
613
+ }
614
+ get length() {
615
+ return this.guidelines.length;
616
+ }
617
+ async artifact(path, options = {}) {
618
+ const context = releaseByCatalog.get(this);
619
+ if (!context) throw new CatalogError("Artifact access requires a catalog release.", { code: "unavailable_capability" });
620
+ if (!Object.hasOwn(context.release.artifacts, path)) throw new CatalogError(`Unknown release artifact: ${path}`, {
621
+ code: "lookup",
622
+ details: {
623
+ path,
624
+ available: Object.keys(context.release.artifacts)
625
+ }
626
+ });
627
+ return context.artifactLoader(path, options.signal);
628
+ }
629
+ get(id) {
630
+ return this.byId.get(id);
631
+ }
632
+ require(id) {
633
+ const guideline = this.get(id);
634
+ if (!guideline) throw new CatalogError(`Unknown guideline entry ID: ${id}`, {
635
+ code: "lookup",
636
+ details: { id },
637
+ hints: ["Call `catalog.query()` to inspect guideline entry IDs."]
638
+ });
639
+ return guideline;
640
+ }
641
+ query(options = {}) {
642
+ return queryCatalog(this, options);
643
+ }
644
+ read(options) {
645
+ return readCatalog(this, options);
646
+ }
647
+ cite(options) {
648
+ return citationRecords(this, options);
649
+ }
650
+ table(name) {
651
+ return catalogTable(this, name);
652
+ }
653
+ labels() {
654
+ return sortedUnique(this.guidelines.flatMap((guideline) => [...guideline.labels]));
655
+ }
656
+ sectionRoles() {
657
+ return sortedUnique(this.guidelines.flatMap((guideline) => guideline.sections.map((section) => section.role).filter((role) => role !== "__dangling__")));
658
+ }
659
+ describe(options = {}) {
660
+ return describeCatalog(this, descriptionByCatalog.get(this) ?? emptyDescriptionContext(), options);
661
+ }
662
+ [Symbol.iterator]() {
663
+ return this.guidelines[Symbol.iterator]();
664
+ }
665
+ };
666
+ function catalogWithRelease(catalog, context) {
667
+ const release = copyCatalogRelease(context.release);
668
+ const profiles = releaseProfiles(release);
669
+ releaseByCatalog.set(catalog, Object.freeze({
670
+ release,
671
+ releaseUrl: context.releaseUrl,
672
+ artifactLoader: context.artifactLoader
673
+ }));
674
+ descriptionByCatalog.set(catalog, Object.freeze({
675
+ resolvedLocation: context.releaseUrl,
676
+ releaseDigest: release.digest,
677
+ profiles,
678
+ profileLoader: context.profileLoader,
679
+ profileCache: /* @__PURE__ */ new Map()
680
+ }));
681
+ return catalog;
682
+ }
683
+ function emptyDescriptionContext() {
684
+ return Object.freeze({
685
+ resolvedLocation: null,
686
+ releaseDigest: null,
687
+ profiles: Object.freeze([]),
688
+ profileCache: /* @__PURE__ */ new Map()
689
+ });
690
+ }
691
+ function isGuidelineInput(value) {
692
+ if (!isJsonObject(value)) return false;
693
+ const { description, id, labels, references, sections, title } = value;
694
+ if (!isJsonString(id) || id.length === 0) return false;
695
+ if (!isJsonString(title) || !isJsonString(description)) return false;
696
+ if (!isStringArray(labels) || !isStringArray(references)) return false;
697
+ if (!Array.isArray(sections) || sections.length === 0 || !sections.every(isGuidelineSection)) return false;
698
+ const dangling = sections.flatMap((section, index) => section.role === "__dangling__" ? [index] : []);
699
+ if (dangling.length > 0 && (dangling.length !== 1 || dangling[0] !== 0)) return false;
700
+ try {
701
+ return labels.every((label) => normalizeLabel(label) === label);
702
+ } catch {
703
+ return false;
704
+ }
705
+ }
706
+ function copyGuideline(value) {
707
+ if (!isGuidelineInput(value)) throw new CatalogError("Invalid guideline record.");
708
+ const sections = Object.freeze(value.sections.map((section) => Object.freeze({
709
+ role: section.role,
710
+ title: section.title,
711
+ content: section.content
712
+ })));
713
+ return Object.freeze({
714
+ id: value.id,
715
+ title: value.title,
716
+ description: value.description,
717
+ labels: Object.freeze([...value.labels]),
718
+ body: bodyFromSections(sections),
719
+ sections,
720
+ references: Object.freeze([...value.references])
721
+ });
722
+ }
723
+ function isGuidelineSection(value) {
724
+ if (!isJsonObject(value)) return false;
725
+ const { content, role, title } = value;
726
+ return isJsonString(role) && role.length > 0 && role === role.trim() && isJsonString(title) && title === title.trim() && isJsonString(content) && content === content.trim() && (role === "__dangling__" ? title.length === 0 && content.length > 0 : title.length > 0);
727
+ }
728
+ function isStringArray(value) {
729
+ return Array.isArray(value) && value.every(isJsonString);
730
+ }
731
+ function bodyFromSections(sections) {
732
+ return sections.map((section) => {
733
+ if (section.role === "__dangling__") return section.content;
734
+ return `## ${section.title} <!-- role: ${section.role} -->\n\n${section.content}`;
735
+ }).join("\n\n");
736
+ }
737
+ function sortedUnique(values) {
738
+ return Array.from(new Set(values)).sort();
739
+ }
740
+ //#endregion
741
+ //#region src/catalog/wire.ts
742
+ const catalogRowFields = [
743
+ "id",
744
+ "title",
745
+ "description",
746
+ "labels",
747
+ "sections",
748
+ "references"
749
+ ];
750
+ const sectionFields = [
751
+ "role",
752
+ "title",
753
+ "content"
754
+ ];
755
+ function hasFields(value, fields) {
756
+ return Object.keys(value).length === fields.length && fields.every((field) => Object.hasOwn(value, field));
757
+ }
758
+ function isCatalogRowWire(value) {
759
+ if (!isJsonObject(value) || !hasFields(value, catalogRowFields)) return false;
760
+ const sections = value.sections;
761
+ if (!Array.isArray(sections) || !sections.every(sectionHasExactFields)) return false;
762
+ return isGuidelineInput(value);
763
+ }
764
+ function guidelineFromWire(value) {
765
+ if (!isCatalogRowWire(value)) return null;
766
+ const sections = value.sections.map((section) => ({ ...section }));
767
+ return {
768
+ id: value.id,
769
+ title: value.title,
770
+ description: value.description,
771
+ labels: [...value.labels],
772
+ sections,
773
+ references: [...value.references]
774
+ };
775
+ }
776
+ function requireGuidelineFromWire(value, context) {
777
+ const guideline = guidelineFromWire(value);
778
+ if (guideline) return guideline;
779
+ const suffix = context ? ` (${context})` : "";
780
+ throw new CatalogError(`Invalid catalog row format${suffix}.`);
781
+ }
782
+ function sectionHasExactFields(value) {
783
+ return isJsonObject(value) && hasFields(value, sectionFields);
784
+ }
785
+ //#endregion
786
+ //#region src/catalog/parquet.ts
787
+ const MAX_CORE_ARTIFACT_BYTES$1 = 67108864;
788
+ function normalizeParquetBytes(bytes) {
789
+ if (bytes instanceof ArrayBuffer) return bytes;
790
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice().buffer;
791
+ }
792
+ function isParquetBytes(bytes) {
793
+ return bytes instanceof ArrayBuffer || ArrayBuffer.isView(bytes);
794
+ }
795
+ async function loadCatalog(input) {
796
+ return loadCatalogWithProfileLoader(input);
797
+ }
798
+ async function loadCatalogWithProfileLoader(input, profileLoader, artifactLoader) {
799
+ const release = copyCatalogRelease(input.release);
800
+ await assertReleaseDigest(release);
801
+ assertReleaseLocation(input.releaseUrl, release);
802
+ releaseProfileNames(release);
803
+ assertCoreArtifactSize(release, "entries.parquet", input.entries.byteLength);
804
+ assertCoreArtifactSize(release, "MANIFEST.md", input.manifest.byteLength);
805
+ await Promise.all([assertReleaseArtifactBytes("entries.parquet", releaseArtifact(release, "entries.parquet"), input.entries), assertReleaseArtifactBytes("MANIFEST.md", releaseArtifact(release, "MANIFEST.md"), input.manifest)]);
806
+ let manifestText;
807
+ try {
808
+ manifestText = new TextDecoder("utf-8", { fatal: true }).decode(normalizeParquetBytes(input.manifest));
809
+ } catch {
810
+ throw new CatalogError("Catalog manifest must contain valid UTF-8.");
811
+ }
812
+ return parseCatalogData({
813
+ entries: input.entries,
814
+ manifestText
815
+ }, {
816
+ release,
817
+ releaseUrl: sanitizeReleaseUrl(input.releaseUrl),
818
+ profileLoader,
819
+ artifactLoader: artifactLoader ?? suppliedArtifacts(input)
820
+ });
821
+ }
822
+ function suppliedArtifacts(input) {
823
+ const entries = new Uint8Array(normalizeParquetBytes(input.entries)).slice();
824
+ const manifest = new Uint8Array(normalizeParquetBytes(input.manifest)).slice();
825
+ return async (path, signal) => {
826
+ signal?.throwIfAborted();
827
+ if (path === "entries.parquet") return entries.slice();
828
+ if (path === "MANIFEST.md") return manifest.slice();
829
+ throw new CatalogError(`Artifact bytes were not supplied: ${path}`, { code: "unavailable_capability" });
830
+ };
831
+ }
832
+ function assertCoreArtifactSize(release, path, receivedBytes) {
833
+ if (releaseArtifact(release, path).bytes > MAX_CORE_ARTIFACT_BYTES$1 || receivedBytes > MAX_CORE_ARTIFACT_BYTES$1) throw new CatalogError(`Catalog artifact exceeds size limit: ${path}`);
834
+ }
835
+ async function loadCatalogData(input) {
836
+ return parseCatalogData(input);
837
+ }
838
+ async function parseCatalogData(input, releaseContext) {
839
+ const manifest = parseCatalogManifest(input.manifestText);
840
+ const normalizedFile = isParquetBytes(input.entries) ? normalizeParquetBytes(input.entries) : input.entries;
841
+ const catalog = new Catalog((await parquetReadObjects({
842
+ file: normalizedFile,
843
+ compressors
844
+ })).map((row, index) => requireGuidelineFromWire(row, `parquet row ${index}`)), manifest);
845
+ return releaseContext ? catalogWithRelease(catalog, releaseContext) : catalog;
846
+ }
847
+ //#endregion
848
+ //#region src/catalog/profile.ts
849
+ const profileFields = [
850
+ "schema_version",
851
+ "documents_version",
852
+ "entries_digest",
853
+ "manifest_digest",
854
+ "embedding_functions",
855
+ "dimensions",
856
+ "distance_metric",
857
+ "python_requirements",
858
+ "lancedb_version",
859
+ "projection"
860
+ ];
861
+ const bindingFields = [
862
+ "name",
863
+ "model",
864
+ "source_column",
865
+ "vector_column"
866
+ ];
867
+ const projectionFields = ["algorithm", "options"];
868
+ const sha256Pattern = /^[a-f0-9]{64}$/;
869
+ const distributionPattern = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
870
+ const versionPattern = /^[A-Za-z0-9][A-Za-z0-9.!+_-]*$/;
871
+ const variablePattern = /^\$var:[^:]+$/;
872
+ const sensitiveNamePattern = /(?:^|[_-])(?:access[_-]?key|api[_-]?key|authorization|client[_-]?secret|cookie|credential|password|private[_-]?key|secret|signature|token)(?:$|[_-])/i;
873
+ const sensitiveQueryNames = /* @__PURE__ */ new Set([
874
+ "access_key",
875
+ "api_key",
876
+ "apikey",
877
+ "authorization",
878
+ "credential",
879
+ "password",
880
+ "secret",
881
+ "signature",
882
+ "sig",
883
+ "token"
884
+ ]);
885
+ function parseProfileMetadata(value) {
886
+ const record = requireObject(value, "Profile metadata");
887
+ requireExactFields(record, profileFields, "Profile metadata");
888
+ if (record.schema_version !== 1) throw new CatalogError("Profile schema_version must be 1.");
889
+ if (record.documents_version !== 1) throw new CatalogError("Profile documents_version must be 1.");
890
+ const entriesDigest = requireSha256(record.entries_digest, "Profile entries_digest");
891
+ const manifestDigest = requireSha256(record.manifest_digest, "Profile manifest_digest");
892
+ if (!Array.isArray(record.embedding_functions) || record.embedding_functions.length !== 1) throw new CatalogError("Profile must contain exactly one embedding binding.");
893
+ const binding = parseBinding(record.embedding_functions[0]);
894
+ const dimensions = record.dimensions;
895
+ if (!isJsonNumber(dimensions) || !Number.isSafeInteger(dimensions) || dimensions < 1) throw new CatalogError("Profile dimensions must be a positive safe integer.");
896
+ const distanceMetric = record.distance_metric;
897
+ if (distanceMetric !== "cosine" && distanceMetric !== "l2" && distanceMetric !== "dot") throw new CatalogError("Profile distance_metric must be cosine, l2, or dot.");
898
+ const pythonRequirements = parseRequirements(record.python_requirements);
899
+ const lancedbVersion = requireVersion(record.lancedb_version, "Profile lancedb_version");
900
+ const projection = record.projection === null ? null : parseProjection(record.projection);
901
+ return Object.freeze({
902
+ schema_version: 1,
903
+ documents_version: 1,
904
+ entries_digest: entriesDigest,
905
+ manifest_digest: manifestDigest,
906
+ embedding_functions: Object.freeze([binding]),
907
+ dimensions,
908
+ distance_metric: distanceMetric,
909
+ python_requirements: pythonRequirements,
910
+ lancedb_version: lancedbVersion,
911
+ projection
912
+ });
913
+ }
914
+ function parseBinding(value) {
915
+ const record = requireObject(value, "Profile embedding binding");
916
+ requireExactFields(record, bindingFields, "Profile embedding binding");
917
+ const name = record.name;
918
+ if (!isJsonString(name) || !name || name !== name.trim()) throw new CatalogError("Profile embedding name must be a non-empty string.");
919
+ if (record.source_column !== "text" || record.vector_column !== "vector") throw new CatalogError("Profile embedding binding must map text to vector.");
920
+ const model = requireObject(record.model, "Profile embedding model");
921
+ validateModel(model);
922
+ return Object.freeze({
923
+ name,
924
+ model: freezeJsonObject(model),
925
+ source_column: "text",
926
+ vector_column: "vector"
927
+ });
928
+ }
929
+ function parseProjection(value) {
930
+ const record = requireObject(value, "Profile projection");
931
+ requireExactFields(record, projectionFields, "Profile projection");
932
+ const algorithm = record.algorithm;
933
+ if (algorithm !== "empty" && algorithm !== "linear" && algorithm !== "umap") throw new CatalogError("Profile projection algorithm is unsupported.");
934
+ return Object.freeze({
935
+ algorithm,
936
+ options: freezeJsonObject(requireObject(record.options, "Profile projection options"))
937
+ });
938
+ }
939
+ function parseRequirements(value) {
940
+ const record = requireObject(value, "Profile Python requirements");
941
+ const requirements = Object.create(null);
942
+ for (const [name, requirement] of Object.entries(record)) {
943
+ if (!distributionPattern.test(name) || !isJsonString(requirement) || !versionPattern.test(requirement) || !/[0-9]/.test(requirement)) throw new CatalogError("Profile python_requirements must map distribution names to exact versions.");
944
+ requirements[name] = requirement;
945
+ }
946
+ return Object.freeze(requirements);
947
+ }
948
+ function validateModel(model) {
949
+ for (const [key, value] of Object.entries(model)) {
950
+ const variables = variableValues(value);
951
+ if (variables.some((variable) => !variablePattern.test(variable)) || variables.length > 0 && !isJsonString(value)) throw new CatalogError(`Profile embedding setting ${JSON.stringify(key)} has an invalid variable.`);
952
+ if (sensitiveNamePattern.test(key) && value !== null && (!isJsonString(value) || !variablePattern.test(value))) throw new CatalogError(`Profile embedding setting ${JSON.stringify(key)} must use a registry variable.`);
953
+ if (key === "default_headers" && value !== null && (!isJsonObject(value) || Object.keys(value).length > 0)) throw new CatalogError("Profile embedding default_headers must be empty.");
954
+ if (containsSensitiveKey(value)) throw new CatalogError(`Profile embedding setting ${JSON.stringify(key)} contains authentication data.`);
955
+ rejectCredentialUrls(value, key);
956
+ }
957
+ if (Object.hasOwn(model, "trust_remote_code") && model.trust_remote_code !== false) throw new CatalogError("Profile embedding must disable remote model code.");
958
+ }
959
+ function containsSensitiveKey(value) {
960
+ if (Array.isArray(value)) return value.some(containsSensitiveKey);
961
+ if (!isJsonObject(value)) return false;
962
+ return Object.entries(value).some(([key, item]) => sensitiveNamePattern.test(key) || containsSensitiveKey(item));
963
+ }
964
+ function rejectCredentialUrls(value, path) {
965
+ if (Array.isArray(value)) {
966
+ value.forEach((item, index) => rejectCredentialUrls(item, `${path}[${index}]`));
967
+ return;
968
+ }
969
+ if (isJsonObject(value)) {
970
+ for (const [key, item] of Object.entries(value)) rejectCredentialUrls(item, `${path}.${key}`);
971
+ return;
972
+ }
973
+ if (!isJsonString(value)) return;
974
+ let parsed;
975
+ try {
976
+ parsed = new URL(value);
977
+ } catch {
978
+ return;
979
+ }
980
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
981
+ if (parsed.username || parsed.password) throw new CatalogError(`Profile embedding URL setting ${JSON.stringify(path)} contains credentials.`);
982
+ if (Array.from(parsed.searchParams.keys()).some((name) => sensitiveQueryNames.has(name.toLowerCase()) || sensitiveNamePattern.test(name))) throw new CatalogError(`Profile embedding URL setting ${JSON.stringify(path)} contains authentication parameters.`);
983
+ }
984
+ function variableValues(value) {
985
+ if (isJsonString(value)) return value.startsWith("$var:") ? [value] : [];
986
+ if (Array.isArray(value)) return value.flatMap(variableValues);
987
+ if (!isJsonObject(value)) return [];
988
+ return Object.values(value).flatMap(variableValues);
989
+ }
990
+ function freezeJsonObject(value) {
991
+ const result = Object.create(null);
992
+ for (const [key, item] of Object.entries(value)) result[key] = freezeJson(item);
993
+ return Object.freeze(result);
994
+ }
995
+ function freezeJson(value) {
996
+ if (Object.prototype.toString.call(value) === "[object Number]" && !isJsonNumber(value)) throw new CatalogError("Profile metadata must contain finite JSON numbers.");
997
+ if (Array.isArray(value)) return Object.freeze(value.map(freezeJson));
998
+ if (isJsonObject(value)) return freezeJsonObject(value);
999
+ return value;
1000
+ }
1001
+ function requireObject(value, label) {
1002
+ if (!isJsonObject(value)) throw new CatalogError(`${label} must be a JSON object.`);
1003
+ return value;
1004
+ }
1005
+ function requireExactFields(value, fields, label) {
1006
+ const actual = Object.keys(value);
1007
+ const missing = fields.filter((field) => !Object.hasOwn(value, field));
1008
+ const unknown = actual.filter((field) => !fields.includes(field));
1009
+ if (missing.length > 0) throw new CatalogError(`${label} is missing fields: ${missing.join(", ")}.`);
1010
+ if (unknown.length > 0) throw new CatalogError(`${label} has unsupported fields: ${unknown.join(", ")}.`);
1011
+ }
1012
+ function requireSha256(value, label) {
1013
+ if (!isJsonString(value) || !sha256Pattern.test(value)) throw new CatalogError(`${label} must be a lowercase SHA-256 digest.`);
1014
+ return value;
1015
+ }
1016
+ function requireVersion(value, label) {
1017
+ if (!isJsonString(value) || !versionPattern.test(value) || !/[0-9]/.test(value)) throw new CatalogError(`${label} must be a non-empty exact version.`);
1018
+ return value;
1019
+ }
1020
+ //#endregion
1021
+ //#region src/catalog/open.ts
1022
+ const CATALOG_REQUEST_TIMEOUT_MS = 9e5;
1023
+ const CATALOG_BODY_READ_ERROR = "Failed to read catalog resource body.";
1024
+ const MAX_CATALOG_JSON_BYTES = 1048576;
1025
+ const MAX_CORE_ARTIFACT_BYTES = 67108864;
1026
+ const MAX_PROFILE_METADATA_BYTES = 65536;
1027
+ async function openCatalog(location = catalogUrl(), options = {}) {
1028
+ const fetch = options.fetch ?? globalThis.fetch;
1029
+ const descriptor = requireDescriptorUrl(location, options.fetch !== void 0);
1030
+ const release = parseCatalogRelease(await fetchJson(descriptor.url, fetch, options.signal, descriptor.kind === "catalog" ? "no-cache" : void 0));
1031
+ await assertReleaseDigest(release);
1032
+ if (descriptor.kind === "release") assertReleaseLocation(descriptor.url, release);
1033
+ return loadCatalogFromRelease(fetch, descriptor.kind === "catalog" ? new URL(`catalog/releases/${release.digest}/`, descriptor.url).toString() : new URL(".", descriptor.url).toString(), descriptor.kind === "catalog" ? new URL(`catalog/releases/${release.digest}/release.json`, descriptor.url).toString() : descriptor.url, release, options);
1034
+ }
1035
+ async function loadCatalogFromRelease(fetch, artifactBase, releaseUrl, release, options) {
1036
+ const signal = options.signal;
1037
+ const artifacts = /* @__PURE__ */ new Map();
1038
+ const loadArtifact = async (path, signal) => {
1039
+ const timeout = AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS);
1040
+ signal = signal ? AbortSignal.any([signal, timeout]) : timeout;
1041
+ signal?.throwIfAborted();
1042
+ const descriptor = releaseArtifact(release, path);
1043
+ const maximum = path === "entries.parquet" || path === "MANIFEST.md" ? MAX_CORE_ARTIFACT_BYTES : 1024 ** 3;
1044
+ if (descriptor.bytes > maximum) throw new CatalogError(`Catalog artifact exceeds size limit: ${path}`);
1045
+ const retain = path === "entries.parquet" || path === "MANIFEST.md" || descriptor.bytes <= MAX_PROFILE_METADATA_BYTES;
1046
+ const memory = artifacts.get(path);
1047
+ if (memory) return memory.slice();
1048
+ const cached = await options.cache?.get(descriptor.sha256, descriptor.bytes);
1049
+ if (cached) try {
1050
+ await assertReleaseArtifactBytes(path, descriptor, cached);
1051
+ signal?.throwIfAborted();
1052
+ if (retain) artifacts.set(path, cached.slice());
1053
+ return cached.slice();
1054
+ } catch (error) {
1055
+ if (!(error instanceof CatalogError) || error.code !== "integrity") throw error;
1056
+ }
1057
+ const bytes = new Uint8Array(await fetchReleaseArtifact(fetch, artifactBase, path, descriptor, signal));
1058
+ await assertReleaseArtifactBytes(path, descriptor, bytes);
1059
+ signal?.throwIfAborted();
1060
+ await options.cache?.put(descriptor.sha256, bytes.slice());
1061
+ signal?.throwIfAborted();
1062
+ if (retain) artifacts.set(path, bytes);
1063
+ return bytes.slice();
1064
+ };
1065
+ const controller = new AbortController();
1066
+ const requestSignal = signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
1067
+ try {
1068
+ const [entries, manifest] = await Promise.all([loadArtifact("entries.parquet", requestSignal), loadArtifact("MANIFEST.md", requestSignal)]);
1069
+ return loadCatalogWithProfileLoader({
1070
+ entries,
1071
+ manifest,
1072
+ release,
1073
+ releaseUrl
1074
+ }, profileLoader(loadArtifact, release), loadArtifact);
1075
+ } catch (error) {
1076
+ controller.abort(error);
1077
+ throw error;
1078
+ }
1079
+ }
1080
+ async function fetchJson(url, fetch, signal, cache) {
1081
+ const timeout = AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS);
1082
+ signal = signal ? AbortSignal.any([signal, timeout]) : timeout;
1083
+ const data = await readResponseBytes(await fetchCatalogResource(url, fetch, signal, cache), MAX_CATALOG_JSON_BYTES, "Catalog JSON exceeds size limit.", signal);
1084
+ try {
1085
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(data);
1086
+ return parseJson(text);
1087
+ } catch {
1088
+ throw new CatalogError("Catalog JSON must contain valid UTF-8 JSON.");
1089
+ }
1090
+ }
1091
+ async function fetchReleaseArtifact(fetch, artifactBase, path, artifact, signal) {
1092
+ return readArtifactBytes(await fetchCatalogResource(releaseArtifactUrl(artifactBase, path), fetch, signal), path, artifact, signal);
1093
+ }
1094
+ async function readArtifactBytes(response, path, artifact, signal) {
1095
+ const maximum = path === "entries.parquet" || path === "MANIFEST.md" ? MAX_CORE_ARTIFACT_BYTES : 1024 ** 3;
1096
+ const absoluteLimitApplies = artifact.bytes > maximum;
1097
+ return readResponseBytes(response, Math.min(artifact.bytes, maximum), absoluteLimitApplies ? `Catalog artifact exceeds size limit: ${path}` : `Catalog artifact byte count mismatch: ${path}`, signal);
1098
+ }
1099
+ async function readResponseBytes(response, limit, limitError, signal) {
1100
+ signal?.throwIfAborted();
1101
+ let reader;
1102
+ try {
1103
+ reader = response.body?.getReader();
1104
+ } catch {
1105
+ throw new CatalogError(CATALOG_BODY_READ_ERROR);
1106
+ }
1107
+ if (!reader) return /* @__PURE__ */ new ArrayBuffer(0);
1108
+ const activeReader = reader;
1109
+ const abort = () => {
1110
+ activeReader.cancel(signal?.reason).catch(() => {});
1111
+ };
1112
+ signal?.addEventListener("abort", abort, { once: true });
1113
+ if (signal?.aborted) abort();
1114
+ const chunks = [];
1115
+ let size = 0;
1116
+ let failure;
1117
+ while (!failure) try {
1118
+ const result = await reader.read();
1119
+ signal?.throwIfAborted();
1120
+ if (result.done) break;
1121
+ const { value } = result;
1122
+ size += value.byteLength;
1123
+ if (size > limit) {
1124
+ reader.cancel().catch(() => {});
1125
+ failure = new CatalogError(limitError);
1126
+ break;
1127
+ }
1128
+ chunks.push(value);
1129
+ } catch {
1130
+ failure = signal?.aborted ? abortReason(signal) : new CatalogError(CATALOG_BODY_READ_ERROR);
1131
+ }
1132
+ signal?.removeEventListener("abort", abort);
1133
+ try {
1134
+ reader.releaseLock();
1135
+ } catch {
1136
+ failure ??= new CatalogError(CATALOG_BODY_READ_ERROR);
1137
+ }
1138
+ if (failure) throw failure;
1139
+ try {
1140
+ const data = new Uint8Array(size);
1141
+ let offset = 0;
1142
+ for (const chunk of chunks) {
1143
+ data.set(chunk, offset);
1144
+ offset += chunk.byteLength;
1145
+ }
1146
+ return data.buffer;
1147
+ } catch {
1148
+ throw new CatalogError(CATALOG_BODY_READ_ERROR);
1149
+ }
1150
+ }
1151
+ async function fetchCatalogResource(url, fetch, signal, cache) {
1152
+ const timeout = AbortSignal.timeout(CATALOG_REQUEST_TIMEOUT_MS);
1153
+ const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
1154
+ const init = cache ? {
1155
+ signal: requestSignal,
1156
+ cache
1157
+ } : { signal: requestSignal };
1158
+ let response;
1159
+ try {
1160
+ response = await fetch(url, init);
1161
+ } catch {
1162
+ if (signal?.aborted) throw abortReason(signal);
1163
+ throw new CatalogError("Failed to load catalog resource.");
1164
+ }
1165
+ let ok;
1166
+ let status;
1167
+ try {
1168
+ ok = response.ok;
1169
+ status = response.status;
1170
+ } catch {
1171
+ throw new CatalogError("Failed to load catalog resource.");
1172
+ }
1173
+ if (!ok) {
1174
+ try {
1175
+ await response.body?.cancel();
1176
+ } catch {}
1177
+ const message = Number.isInteger(status) ? `Failed to load catalog resource: HTTP ${status}.` : "Failed to load catalog resource.";
1178
+ throw new CatalogError(message);
1179
+ }
1180
+ return response;
1181
+ }
1182
+ function profileLoader(loadArtifact, release) {
1183
+ return async (profile, signal) => {
1184
+ const path = profile.metadata;
1185
+ if (releaseArtifact(release, path).bytes > MAX_PROFILE_METADATA_BYTES) throw new CatalogError("Profile metadata exceeds the 64 KiB limit.", {
1186
+ code: "incompatible_profile",
1187
+ details: { profile: profile.name }
1188
+ });
1189
+ let data;
1190
+ try {
1191
+ data = await loadArtifact(path, signal);
1192
+ } catch (error) {
1193
+ if (signal?.aborted) throw abortReason(signal);
1194
+ throw new CatalogError(error instanceof CatalogError ? error.message : `Profile metadata could not be loaded: ${profile.name}.`, {
1195
+ code: "integrity",
1196
+ details: { profile: profile.name }
1197
+ });
1198
+ }
1199
+ let value;
1200
+ try {
1201
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(data);
1202
+ value = parseJson(text);
1203
+ } catch {
1204
+ throw new CatalogError(`Profile metadata is not valid UTF-8 JSON: ${profile.name}.`, {
1205
+ code: "incompatible_profile",
1206
+ details: { profile: profile.name }
1207
+ });
1208
+ }
1209
+ try {
1210
+ return parseProfileMetadata(value);
1211
+ } catch (error) {
1212
+ if (error instanceof CatalogError) throw new CatalogError(error.message, {
1213
+ code: "incompatible_profile",
1214
+ details: { profile: profile.name }
1215
+ });
1216
+ throw error;
1217
+ }
1218
+ };
1219
+ }
1220
+ function abortReason(signal) {
1221
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
1222
+ }
1223
+ function requireDescriptorUrl(input, customFetch) {
1224
+ const value = input.toString();
1225
+ let url;
1226
+ try {
1227
+ url = new URL(value);
1228
+ } catch {
1229
+ throw new CatalogError("Catalog location must be an absolute HTTP or HTTPS URL.");
1230
+ }
1231
+ if (!customFetch && url.protocol !== "http:" && url.protocol !== "https:") throw new CatalogError("Catalog location must use HTTP or HTTPS.");
1232
+ const name = url.pathname.split("/").at(-1);
1233
+ if (name === "catalog.json") return {
1234
+ kind: "catalog",
1235
+ url: url.toString()
1236
+ };
1237
+ if (name === "release.json") return {
1238
+ kind: "release",
1239
+ url: url.toString()
1240
+ };
1241
+ throw new CatalogError("Catalog location URL must name catalog.json or release.json.");
1242
+ }
1243
+ //#endregion
1244
+ export { Catalog as a, releaseArtifact as c, loadCatalogData as i, parseCatalogManifest as l, parseProfileMetadata as n, assertReleaseDigest as o, loadCatalog as r, parseCatalogRelease as s, openCatalog as t };