keepset 0.0.4 → 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.
package/dist/index.d.ts CHANGED
@@ -1 +1,589 @@
1
- export {}
1
+ import { z } from "zod";
2
+ //#region src/targets/graph-v1.d.ts
3
+ /** A JSON scalar: the only values graph/v1 metadata may hold, so `meta` stays a flat record any consumer can tabulate. `z.number()` already rejects NaN and the infinities, which JSON cannot carry. */
4
+ export declare const JsonScalarSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
5
+ type JsonScalar = z.infer<typeof JsonScalarSchema>;
6
+ /** A node: an id, an optional type, and whatever else the dataset carries. The target fixes only what every graph consumer needs, so extra fields pass through untouched. */
7
+ export declare const GraphNodeSchema: z.ZodObject<{
8
+ id: z.ZodString;
9
+ type: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$loose>;
11
+ type GraphNode = z.infer<typeof GraphNodeSchema>;
12
+ /** An edge: source and target node ids, an optional type, and whatever else the dataset carries. Direction is the dataset's own: graph/v1 records it in `meta`, never by duplicating edges. */
13
+ export declare const GraphEdgeSchema: z.ZodObject<{
14
+ source: z.ZodString;
15
+ target: z.ZodString;
16
+ type: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$loose>;
18
+ type GraphEdge = z.infer<typeof GraphEdgeSchema>;
19
+ /**
20
+ * The graph/v1 normalisation target: `{ meta, nodes, edges }`, the shape the thesis pipeline's benchmark cache already uses. It describes the result `fetchDataset` resolves a graph entry to, not what a source file may contain. Beyond the field shapes it checks the two properties every consumer relies on: node ids are unique, and every edge endpoint names a node.
21
+ */
22
+ export declare const GraphV1Schema: z.ZodObject<{
23
+ meta: z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>, unknown>;
24
+ nodes: z.ZodArray<z.ZodObject<{
25
+ id: z.ZodString;
26
+ type: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$loose>>;
28
+ edges: z.ZodArray<z.ZodObject<{
29
+ source: z.ZodString;
30
+ target: z.ZodString;
31
+ type: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$loose>>;
33
+ }, z.core.$strict> & {
34
+ is: (value: unknown) => value is {
35
+ meta: Record<string, string | number | boolean | null>;
36
+ nodes: {
37
+ [x: string]: unknown;
38
+ id: string;
39
+ type?: string | undefined;
40
+ }[];
41
+ edges: {
42
+ [x: string]: unknown;
43
+ source: string;
44
+ target: string;
45
+ type?: string | undefined;
46
+ }[];
47
+ };
48
+ };
49
+ type GraphV1 = z.infer<typeof GraphV1Schema>;
50
+ //#endregion
51
+ //#region src/categories.d.ts
52
+ /** Every category id the registry knows. A category id is `<kind>/v<version>`: the kind of data, and the version of its normalisation target, so a breaking change to a target is a new category rather than an edit to an old one. */
53
+ export declare const CategoryIdSchema: z.ZodEnum<{
54
+ "graph/v1": "graph/v1";
55
+ }>;
56
+ type CategoryId = z.infer<typeof CategoryIdSchema>;
57
+ /** The normalisation target each category resolves to, keyed by category id. */
58
+ interface CategoryTargets {
59
+ readonly "graph/v1": GraphV1;
60
+ }
61
+ /** The normalised shape of any category: what `fetchDataset` can resolve to. */
62
+ type CategoryTarget = CategoryTargets[CategoryId];
63
+ /** One category: its id, what it holds, and the schema of its normalisation target. */
64
+ interface Category<Id extends CategoryId = CategoryId> {
65
+ readonly id: Id;
66
+ readonly description: string;
67
+ readonly target: z.ZodType<CategoryTargets[Id]>;
68
+ }
69
+ /**
70
+ * Looks up a category by id.
71
+ * @param id - Any string; category ids usually come from entry documents, so the argument is untrusted.
72
+ * @returns The category.
73
+ * @throws UnknownCategoryError when no category has that id.
74
+ */
75
+ export declare function getCategory<Id extends CategoryId>(id: Id): Category<Id>;
76
+ export declare function getCategory(id: string): Category;
77
+ /**
78
+ * Returns the schema of a category's normalisation target.
79
+ * @param id - Any string, as for `getCategory`.
80
+ * @returns The target schema.
81
+ * @throws UnknownCategoryError when no category has that id.
82
+ */
83
+ export declare function getTarget<Id extends CategoryId>(id: Id): z.ZodType<CategoryTargets[Id]>;
84
+ export declare function getTarget(id: string): z.ZodType<CategoryTarget>;
85
+ /**
86
+ * The kind of data a category holds: the part of its id before the version.
87
+ * @param id - A category id.
88
+ * @returns The kind, such as `graph`.
89
+ */
90
+ export declare function kindOf(id: CategoryId): string;
91
+ /** @returns Every registered category, in id order. */
92
+ export declare function listCategories(): readonly Category[];
93
+ //#endregion
94
+ //#region src/errors.d.ts
95
+ /** The root of this package's error taxonomy: every error keepset throws is a `KeepsetError`, so callers can catch the family with one check. */
96
+ export declare class KeepsetError extends Error {
97
+ constructor(message: string);
98
+ }
99
+ /** Thrown when a category id names no category in the registry. Carries the ids that do exist, so the message says what would have been accepted. */
100
+ export declare class UnknownCategoryError extends KeepsetError {
101
+ readonly categoryId: string;
102
+ readonly knownIds: readonly string[];
103
+ constructor(categoryId: string, knownIds: readonly string[]);
104
+ }
105
+ /** One reason a catalogue document failed its schema: the dotted path of the offending value and what is wrong with it. Zod's own issue objects are collapsed into these so the public error surface stays stable across Zod versions. */
106
+ interface SyntaxIssue {
107
+ readonly path: string;
108
+ readonly message: string;
109
+ }
110
+ /** Thrown when an entry or a catalogue manifest fails its schema. Carries every issue found, not just the first, so one failed parse reports the whole document. */
111
+ export declare class CatalogueSyntaxError extends KeepsetError {
112
+ readonly document: "entry" | "manifest";
113
+ readonly issues: readonly SyntaxIssue[];
114
+ constructor(document: "entry" | "manifest", issues: readonly SyntaxIssue[]);
115
+ }
116
+ //#endregion
117
+ //#region src/entry.d.ts
118
+ /** A stable identifier: an entry id, a catalogue id. It starts with a letter or digit, so no id can be `__proto__` or any other name that begins with an underscore. Entry ids stay the same across forks, which is what lets two catalogues be compared entry by entry. */
119
+ export declare const IdSchema: z.ZodString;
120
+ /** A Wayback Machine timestamp, `YYYYMMDD` or `YYYYMMDDhhmmss`: the moment the snapshot is pinned to. */
121
+ export declare const WaybackTimestampSchema: z.ZodString;
122
+ /**
123
+ * The Wayback Machine URL of a snapshot of `url` at `timestamp`: the page form a person follows. The raw bytes a recipe downloads come from the `id_` form, which `fetchDataset` derives from this one.
124
+ * @param timestamp - A Wayback timestamp.
125
+ * @param url - The original URL.
126
+ * @returns The snapshot URL.
127
+ */
128
+ export declare function waybackUrl(timestamp: string, url: string): string;
129
+ /** A pinned Wayback Machine snapshot of a source. The timestamp is part of the recipe's reproducibility, not incidental metadata, so it travels with the URL and the two are checked against each other. */
130
+ export declare const SnapshotSchema: z.ZodObject<{
131
+ url: z.ZodURL;
132
+ timestamp: z.ZodString;
133
+ }, z.core.$strict>;
134
+ type Snapshot = z.infer<typeof SnapshotSchema>;
135
+ /**
136
+ * Where a dataset's bytes come from. A `dead` source no longer serves the bytes the recipe was written against and must carry the snapshot the recipe fetches instead; a `live` source may also carry one, as the pinned record of what it served. The snapshot URL must be exactly `waybackUrl(timestamp, url)`.
137
+ */
138
+ export declare const SourceSchema: z.ZodObject<{
139
+ url: z.ZodURL;
140
+ retrieved: z.ZodISODate;
141
+ status: z.ZodEnum<{
142
+ live: "live";
143
+ dead: "dead";
144
+ }>;
145
+ snapshot: z.ZodOptional<z.ZodObject<{
146
+ url: z.ZodURL;
147
+ timestamp: z.ZodString;
148
+ }, z.core.$strict>>;
149
+ }, z.core.$strict>;
150
+ type Source = z.infer<typeof SourceSchema>;
151
+ /** The licence a dataset is published under: an SPDX identifier, a named licence with the URL of its terms, or a statement that the source records none. A licence the source does not state is recorded as unrecorded rather than guessed. */
152
+ export declare const LicenceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
153
+ kind: z.ZodLiteral<"spdx">;
154
+ id: z.ZodString;
155
+ }, z.core.$strict>, z.ZodObject<{
156
+ kind: z.ZodLiteral<"custom">;
157
+ name: z.ZodString;
158
+ url: z.ZodURL;
159
+ }, z.core.$strict>, z.ZodObject<{
160
+ kind: z.ZodLiteral<"unrecorded">;
161
+ }, z.core.$strict>], "kind">;
162
+ type Licence = z.infer<typeof LicenceSchema>;
163
+ /** A work to cite for the dataset: a formatted reference, with its DOI or URL where the source records one. */
164
+ export declare const CitationSchema: z.ZodObject<{
165
+ text: z.ZodString;
166
+ doi: z.ZodOptional<z.ZodString>;
167
+ url: z.ZodOptional<z.ZodURL>;
168
+ }, z.core.$strict>;
169
+ type Citation = z.infer<typeof CitationSchema>;
170
+ /** A relative path inside an archive or a catalogue: `/`-separated segments, none empty, `.` or `..`, so it can never climb out of the directory it is resolved against. */
171
+ export declare const RelativePathSchema: z.ZodString;
172
+ /** Which file of an archive to read: a member path, matched against a member's whole path or its trailing segments (so `net.gml` finds `net/net.gml`), or the member whose name ends with an extension. Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
173
+ export declare const ArchiveMemberSchema: z.ZodUnion<readonly [z.ZodObject<{
174
+ path: z.ZodString;
175
+ }, z.core.$strict>, z.ZodObject<{
176
+ extension: z.ZodString;
177
+ }, z.core.$strict>]>;
178
+ type ArchiveMember = z.infer<typeof ArchiveMemberSchema>;
179
+ /** How the downloaded bytes unpack into the one file the parser reads. */
180
+ export declare const ArchiveSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
181
+ kind: z.ZodLiteral<"none">;
182
+ }, z.core.$strict>, z.ZodObject<{
183
+ kind: z.ZodLiteral<"gzip">;
184
+ }, z.core.$strict>, z.ZodObject<{
185
+ kind: z.ZodLiteral<"zip">;
186
+ member: z.ZodUnion<readonly [z.ZodObject<{
187
+ path: z.ZodString;
188
+ }, z.core.$strict>, z.ZodObject<{
189
+ extension: z.ZodString;
190
+ }, z.core.$strict>]>;
191
+ }, z.core.$strict>, z.ZodObject<{
192
+ kind: z.ZodLiteral<"tar">;
193
+ member: z.ZodUnion<readonly [z.ZodObject<{
194
+ path: z.ZodString;
195
+ }, z.core.$strict>, z.ZodObject<{
196
+ extension: z.ZodString;
197
+ }, z.core.$strict>]>;
198
+ }, z.core.$strict>, z.ZodObject<{
199
+ kind: z.ZodLiteral<"tar.gz">;
200
+ member: z.ZodUnion<readonly [z.ZodObject<{
201
+ path: z.ZodString;
202
+ }, z.core.$strict>, z.ZodObject<{
203
+ extension: z.ZodString;
204
+ }, z.core.$strict>]>;
205
+ }, z.core.$strict>], "kind">;
206
+ type Archive = z.infer<typeof ArchiveSchema>;
207
+ /** The parser that reads the unpacked file into the dataset's native JSON shape, with the options that format needs. Every option is stated in the entry, so a recipe reads the same way wherever it runs. */
208
+ export declare const ParserSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
209
+ format: z.ZodLiteral<"edge-list">;
210
+ encoding: z.ZodEnum<{
211
+ "utf-8": "utf-8";
212
+ latin1: "latin1";
213
+ }>;
214
+ delimiter: z.ZodEnum<{
215
+ whitespace: "whitespace";
216
+ tab: "tab";
217
+ comma: "comma";
218
+ space: "space";
219
+ }>;
220
+ skipLines: z.ZodInt;
221
+ commentPrefixes: z.ZodArray<z.ZodString>;
222
+ sourceColumn: z.ZodInt;
223
+ targetColumn: z.ZodInt;
224
+ }, z.core.$strict>, z.ZodObject<{
225
+ format: z.ZodLiteral<"matrix-market">;
226
+ encoding: z.ZodEnum<{
227
+ "utf-8": "utf-8";
228
+ latin1: "latin1";
229
+ }>;
230
+ }, z.core.$strict>, z.ZodObject<{
231
+ format: z.ZodLiteral<"gml">;
232
+ encoding: z.ZodEnum<{
233
+ "utf-8": "utf-8";
234
+ latin1: "latin1";
235
+ }>;
236
+ }, z.core.$strict>, z.ZodObject<{
237
+ format: z.ZodLiteral<"pajek">;
238
+ encoding: z.ZodEnum<{
239
+ "utf-8": "utf-8";
240
+ latin1: "latin1";
241
+ }>;
242
+ }, z.core.$strict>], "format">;
243
+ type Parser = z.infer<typeof ParserSchema>;
244
+ /** How to obtain the dataset: a fetchable recipe naming the source (by index into `sources`), the archive layout and the parser, or a statement that no declarative recipe exists yet and why. */
245
+ export declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
246
+ status: z.ZodLiteral<"fetchable">;
247
+ source: z.ZodInt;
248
+ archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
249
+ kind: z.ZodLiteral<"none">;
250
+ }, z.core.$strict>, z.ZodObject<{
251
+ kind: z.ZodLiteral<"gzip">;
252
+ }, z.core.$strict>, z.ZodObject<{
253
+ kind: z.ZodLiteral<"zip">;
254
+ member: z.ZodUnion<readonly [z.ZodObject<{
255
+ path: z.ZodString;
256
+ }, z.core.$strict>, z.ZodObject<{
257
+ extension: z.ZodString;
258
+ }, z.core.$strict>]>;
259
+ }, z.core.$strict>, z.ZodObject<{
260
+ kind: z.ZodLiteral<"tar">;
261
+ member: z.ZodUnion<readonly [z.ZodObject<{
262
+ path: z.ZodString;
263
+ }, z.core.$strict>, z.ZodObject<{
264
+ extension: z.ZodString;
265
+ }, z.core.$strict>]>;
266
+ }, z.core.$strict>, z.ZodObject<{
267
+ kind: z.ZodLiteral<"tar.gz">;
268
+ member: z.ZodUnion<readonly [z.ZodObject<{
269
+ path: z.ZodString;
270
+ }, z.core.$strict>, z.ZodObject<{
271
+ extension: z.ZodString;
272
+ }, z.core.$strict>]>;
273
+ }, z.core.$strict>], "kind">;
274
+ parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
275
+ format: z.ZodLiteral<"edge-list">;
276
+ encoding: z.ZodEnum<{
277
+ "utf-8": "utf-8";
278
+ latin1: "latin1";
279
+ }>;
280
+ delimiter: z.ZodEnum<{
281
+ whitespace: "whitespace";
282
+ tab: "tab";
283
+ comma: "comma";
284
+ space: "space";
285
+ }>;
286
+ skipLines: z.ZodInt;
287
+ commentPrefixes: z.ZodArray<z.ZodString>;
288
+ sourceColumn: z.ZodInt;
289
+ targetColumn: z.ZodInt;
290
+ }, z.core.$strict>, z.ZodObject<{
291
+ format: z.ZodLiteral<"matrix-market">;
292
+ encoding: z.ZodEnum<{
293
+ "utf-8": "utf-8";
294
+ latin1: "latin1";
295
+ }>;
296
+ }, z.core.$strict>, z.ZodObject<{
297
+ format: z.ZodLiteral<"gml">;
298
+ encoding: z.ZodEnum<{
299
+ "utf-8": "utf-8";
300
+ latin1: "latin1";
301
+ }>;
302
+ }, z.core.$strict>, z.ZodObject<{
303
+ format: z.ZodLiteral<"pajek">;
304
+ encoding: z.ZodEnum<{
305
+ "utf-8": "utf-8";
306
+ latin1: "latin1";
307
+ }>;
308
+ }, z.core.$strict>], "format">;
309
+ }, z.core.$strict>, z.ZodObject<{
310
+ status: z.ZodLiteral<"not-fetchable">;
311
+ reason: z.ZodString;
312
+ }, z.core.$strict>], "status">;
313
+ type Recipe = z.infer<typeof RecipeSchema>;
314
+ /** The twindex crosswalk from the native shape to the category target: the document inline, or a `.json` file path relative to the catalogue root, so entries that share a native shape share one crosswalk. */
315
+ export declare const CrosswalkReferenceSchema: z.ZodUnion<readonly [z.ZodType<import("twindex").Crosswalk, unknown, z.core.$ZodTypeInternals<import("twindex").Crosswalk, unknown>> & {
316
+ is: (value: unknown) => value is import("twindex").Crosswalk;
317
+ }, z.ZodObject<{
318
+ file: z.ZodString;
319
+ }, z.core.$strict>]>;
320
+ type CrosswalkReference = z.infer<typeof CrosswalkReferenceSchema>;
321
+ /**
322
+ * One catalogue entry: the descriptor (id, name, description, domain, tags), the category whose target the entry resolves to, where the bytes come from, the licence and citations, the recipe, the crosswalk, the counts a fetched result is expected to have and the dataset's recorded properties. The kind of data is the category's (`kindOf(category)`), never a second field that could disagree with it.
323
+ */
324
+ export declare const EntrySchema: z.ZodObject<{
325
+ id: z.ZodString;
326
+ name: z.ZodString;
327
+ description: z.ZodOptional<z.ZodString>;
328
+ domain: z.ZodOptional<z.ZodString>;
329
+ tags: z.ZodArray<z.ZodString>;
330
+ category: z.ZodEnum<{
331
+ "graph/v1": "graph/v1";
332
+ }>;
333
+ sources: z.ZodArray<z.ZodObject<{
334
+ url: z.ZodURL;
335
+ retrieved: z.ZodISODate;
336
+ status: z.ZodEnum<{
337
+ live: "live";
338
+ dead: "dead";
339
+ }>;
340
+ snapshot: z.ZodOptional<z.ZodObject<{
341
+ url: z.ZodURL;
342
+ timestamp: z.ZodString;
343
+ }, z.core.$strict>>;
344
+ }, z.core.$strict>>;
345
+ licence: z.ZodDiscriminatedUnion<[z.ZodObject<{
346
+ kind: z.ZodLiteral<"spdx">;
347
+ id: z.ZodString;
348
+ }, z.core.$strict>, z.ZodObject<{
349
+ kind: z.ZodLiteral<"custom">;
350
+ name: z.ZodString;
351
+ url: z.ZodURL;
352
+ }, z.core.$strict>, z.ZodObject<{
353
+ kind: z.ZodLiteral<"unrecorded">;
354
+ }, z.core.$strict>], "kind">;
355
+ citations: z.ZodArray<z.ZodObject<{
356
+ text: z.ZodString;
357
+ doi: z.ZodOptional<z.ZodString>;
358
+ url: z.ZodOptional<z.ZodURL>;
359
+ }, z.core.$strict>>;
360
+ recipe: z.ZodDiscriminatedUnion<[z.ZodObject<{
361
+ status: z.ZodLiteral<"fetchable">;
362
+ source: z.ZodInt;
363
+ archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
364
+ kind: z.ZodLiteral<"none">;
365
+ }, z.core.$strict>, z.ZodObject<{
366
+ kind: z.ZodLiteral<"gzip">;
367
+ }, z.core.$strict>, z.ZodObject<{
368
+ kind: z.ZodLiteral<"zip">;
369
+ member: z.ZodUnion<readonly [z.ZodObject<{
370
+ path: z.ZodString;
371
+ }, z.core.$strict>, z.ZodObject<{
372
+ extension: z.ZodString;
373
+ }, z.core.$strict>]>;
374
+ }, z.core.$strict>, z.ZodObject<{
375
+ kind: z.ZodLiteral<"tar">;
376
+ member: z.ZodUnion<readonly [z.ZodObject<{
377
+ path: z.ZodString;
378
+ }, z.core.$strict>, z.ZodObject<{
379
+ extension: z.ZodString;
380
+ }, z.core.$strict>]>;
381
+ }, z.core.$strict>, z.ZodObject<{
382
+ kind: z.ZodLiteral<"tar.gz">;
383
+ member: z.ZodUnion<readonly [z.ZodObject<{
384
+ path: z.ZodString;
385
+ }, z.core.$strict>, z.ZodObject<{
386
+ extension: z.ZodString;
387
+ }, z.core.$strict>]>;
388
+ }, z.core.$strict>], "kind">;
389
+ parser: z.ZodDiscriminatedUnion<[z.ZodObject<{
390
+ format: z.ZodLiteral<"edge-list">;
391
+ encoding: z.ZodEnum<{
392
+ "utf-8": "utf-8";
393
+ latin1: "latin1";
394
+ }>;
395
+ delimiter: z.ZodEnum<{
396
+ whitespace: "whitespace";
397
+ tab: "tab";
398
+ comma: "comma";
399
+ space: "space";
400
+ }>;
401
+ skipLines: z.ZodInt;
402
+ commentPrefixes: z.ZodArray<z.ZodString>;
403
+ sourceColumn: z.ZodInt;
404
+ targetColumn: z.ZodInt;
405
+ }, z.core.$strict>, z.ZodObject<{
406
+ format: z.ZodLiteral<"matrix-market">;
407
+ encoding: z.ZodEnum<{
408
+ "utf-8": "utf-8";
409
+ latin1: "latin1";
410
+ }>;
411
+ }, z.core.$strict>, z.ZodObject<{
412
+ format: z.ZodLiteral<"gml">;
413
+ encoding: z.ZodEnum<{
414
+ "utf-8": "utf-8";
415
+ latin1: "latin1";
416
+ }>;
417
+ }, z.core.$strict>, z.ZodObject<{
418
+ format: z.ZodLiteral<"pajek">;
419
+ encoding: z.ZodEnum<{
420
+ "utf-8": "utf-8";
421
+ latin1: "latin1";
422
+ }>;
423
+ }, z.core.$strict>], "format">;
424
+ }, z.core.$strict>, z.ZodObject<{
425
+ status: z.ZodLiteral<"not-fetchable">;
426
+ reason: z.ZodString;
427
+ }, z.core.$strict>], "status">;
428
+ crosswalk: z.ZodUnion<readonly [z.ZodType<import("twindex").Crosswalk, unknown, z.core.$ZodTypeInternals<import("twindex").Crosswalk, unknown>> & {
429
+ is: (value: unknown) => value is import("twindex").Crosswalk;
430
+ }, z.ZodObject<{
431
+ file: z.ZodString;
432
+ }, z.core.$strict>]>;
433
+ expectedCounts: z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodInt>, unknown>;
434
+ properties: z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>, unknown>;
435
+ }, z.core.$strict> & {
436
+ is: (value: unknown) => value is {
437
+ id: string;
438
+ name: string;
439
+ tags: string[];
440
+ category: "graph/v1";
441
+ sources: {
442
+ url: string;
443
+ retrieved: string;
444
+ status: "live" | "dead";
445
+ snapshot?: {
446
+ url: string;
447
+ timestamp: string;
448
+ } | undefined;
449
+ }[];
450
+ licence: {
451
+ kind: "spdx";
452
+ id: string;
453
+ } | {
454
+ kind: "custom";
455
+ name: string;
456
+ url: string;
457
+ } | {
458
+ kind: "unrecorded";
459
+ };
460
+ citations: {
461
+ text: string;
462
+ doi?: string | undefined;
463
+ url?: string | undefined;
464
+ }[];
465
+ recipe: {
466
+ status: "fetchable";
467
+ source: number;
468
+ archive: {
469
+ kind: "none";
470
+ } | {
471
+ kind: "gzip";
472
+ } | {
473
+ kind: "zip";
474
+ member: {
475
+ path: string;
476
+ } | {
477
+ extension: string;
478
+ };
479
+ } | {
480
+ kind: "tar";
481
+ member: {
482
+ path: string;
483
+ } | {
484
+ extension: string;
485
+ };
486
+ } | {
487
+ kind: "tar.gz";
488
+ member: {
489
+ path: string;
490
+ } | {
491
+ extension: string;
492
+ };
493
+ };
494
+ parser: {
495
+ format: "edge-list";
496
+ encoding: "utf-8" | "latin1";
497
+ delimiter: "whitespace" | "tab" | "comma" | "space";
498
+ skipLines: number;
499
+ commentPrefixes: string[];
500
+ sourceColumn: number;
501
+ targetColumn: number;
502
+ } | {
503
+ format: "matrix-market";
504
+ encoding: "utf-8" | "latin1";
505
+ } | {
506
+ format: "gml";
507
+ encoding: "utf-8" | "latin1";
508
+ } | {
509
+ format: "pajek";
510
+ encoding: "utf-8" | "latin1";
511
+ };
512
+ } | {
513
+ status: "not-fetchable";
514
+ reason: string;
515
+ };
516
+ crosswalk: import("twindex").Crosswalk | {
517
+ file: string;
518
+ };
519
+ expectedCounts: Record<string, number>;
520
+ properties: Record<string, string | number | boolean | null>;
521
+ description?: string | undefined;
522
+ domain?: string | undefined;
523
+ };
524
+ };
525
+ type Entry = z.infer<typeof EntrySchema>;
526
+ /**
527
+ * Parses an unknown value as a catalogue entry.
528
+ * @param doc - The candidate entry document.
529
+ * @returns The validated entry.
530
+ * @throws CatalogueSyntaxError carrying every issue found.
531
+ */
532
+ export declare function parseEntry(doc: unknown): Entry;
533
+ //#endregion
534
+ //#region src/manifest.d.ts
535
+ /** A semantic version, `MAJOR.MINOR.PATCH` with optional pre-release and build parts (https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string). */
536
+ export declare const SemVerSchema: z.ZodString;
537
+ /** Where a forked catalogue came from: the catalogue it was forked from, the version it forked at and the repository that holds it, which together are what lets a fork's entries be compared with, and brought back to, their origin. */
538
+ export declare const LineageSchema: z.ZodObject<{
539
+ catalogue: z.ZodString;
540
+ version: z.ZodString;
541
+ repository: z.ZodURL;
542
+ }, z.core.$strict>;
543
+ type Lineage = z.infer<typeof LineageSchema>;
544
+ /**
545
+ * A catalogue manifest: the catalogue's own id and version, where it lives, and, for a fork, its lineage. A catalogue is a self-contained set of entry documents beside its manifest, and a fork is its own catalogue with its own id, so lineage may never name the catalogue itself.
546
+ */
547
+ export declare const ManifestSchema: z.ZodObject<{
548
+ id: z.ZodString;
549
+ name: z.ZodString;
550
+ description: z.ZodString;
551
+ version: z.ZodString;
552
+ repository: z.ZodURL;
553
+ lineage: z.ZodOptional<z.ZodObject<{
554
+ catalogue: z.ZodString;
555
+ version: z.ZodString;
556
+ repository: z.ZodURL;
557
+ }, z.core.$strict>>;
558
+ }, z.core.$strict> & {
559
+ is: (value: unknown) => value is {
560
+ id: string;
561
+ name: string;
562
+ description: string;
563
+ version: string;
564
+ repository: string;
565
+ lineage?: {
566
+ catalogue: string;
567
+ version: string;
568
+ repository: string;
569
+ } | undefined;
570
+ };
571
+ };
572
+ type Manifest = z.infer<typeof ManifestSchema>;
573
+ /**
574
+ * Parses an unknown value as a catalogue manifest.
575
+ * @param doc - The candidate manifest document.
576
+ * @returns The validated manifest.
577
+ * @throws CatalogueSyntaxError carrying every issue found.
578
+ */
579
+ export declare function parseManifest(doc: unknown): Manifest;
580
+ //#endregion
581
+ //#region src/serialise.d.ts
582
+ /**
583
+ * Serialises a catalogue document as its file holds it: two-space indented JSON in the schema's key order with a trailing newline. A parsed document keeps the schema's key order, so parsing a file written this way and serialising the result reproduces the file byte for byte.
584
+ * @param doc - A parsed entry or manifest.
585
+ * @returns The file text.
586
+ */
587
+ export declare function serialise(doc: Entry | Manifest): string;
588
+ //#endregion
589
+ export type { Archive, ArchiveMember, Category, CategoryId, CategoryTarget, CategoryTargets, Citation, CrosswalkReference, Entry, GraphEdge, GraphNode, GraphV1, JsonScalar, Licence, Lineage, Manifest, Parser, Recipe, Snapshot, Source, SyntaxIssue };
package/dist/index.js CHANGED
@@ -0,0 +1,375 @@
1
+ import { z } from "zod";
2
+ import { CrosswalkSchema } from "twindex";
3
+ //#region src/errors.ts
4
+ /** The root of this package's error taxonomy: every error keepset throws is a `KeepsetError`, so callers can catch the family with one check. */
5
+ var KeepsetError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = new.target.name;
9
+ }
10
+ };
11
+ /** Thrown when a category id names no category in the registry. Carries the ids that do exist, so the message says what would have been accepted. */
12
+ var UnknownCategoryError = class extends KeepsetError {
13
+ categoryId;
14
+ knownIds;
15
+ constructor(categoryId, knownIds) {
16
+ super(`unknown category ${JSON.stringify(categoryId)}: known categories are ${knownIds.join(", ")}`);
17
+ this.categoryId = categoryId;
18
+ this.knownIds = knownIds;
19
+ }
20
+ };
21
+ /** Thrown when an entry or a catalogue manifest fails its schema. Carries every issue found, not just the first, so one failed parse reports the whole document. */
22
+ var CatalogueSyntaxError = class extends KeepsetError {
23
+ document;
24
+ issues;
25
+ constructor(document, issues) {
26
+ super(`${document} syntax error: ${issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
27
+ this.document = document;
28
+ this.issues = issues;
29
+ }
30
+ };
31
+ //#endregion
32
+ //#region src/define.ts
33
+ /**
34
+ * Attaches a type guard to a Zod schema (the schema-attached guard pattern: validation, inference and narrowing share one definition, so the three cannot drift). This module is isolated because it is the one place in the package that augments an object in place with Object.assign, which the house lint rule that eslint.config.ts turns off for this file alone would otherwise reject: object spread is no alternative, as it copies own properties only and drops the Zod prototype's parse/refine methods.
35
+ * @param schema - The schema to augment.
36
+ * @returns The same schema object, now also carrying `is`.
37
+ */
38
+ function define(schema) {
39
+ return Object.assign(schema, { is(value) {
40
+ return schema.safeParse(value).success;
41
+ } });
42
+ }
43
+ //#endregion
44
+ //#region src/record.ts
45
+ /**
46
+ * A record whose every key must satisfy `key`. Zod's own record silently drops an own `__proto__` key (which `JSON.parse` creates as an ordinary property) before the key schema sees it, so this checks the raw input first and rejects such a key loudly instead of losing the value.
47
+ * @param key - The key schema.
48
+ * @param value - The value schema.
49
+ * @returns The record schema.
50
+ */
51
+ function recordOf(key, value) {
52
+ return z.preprocess((input, ctx) => {
53
+ if (typeof input === "object" && input !== null && Object.hasOwn(input, "__proto__")) ctx.issues.push({
54
+ code: "custom",
55
+ input,
56
+ message: "__proto__ is not a valid key"
57
+ });
58
+ return input;
59
+ }, z.record(key, value));
60
+ }
61
+ //#endregion
62
+ //#region src/targets/graph-v1.ts
63
+ /** A JSON scalar: the only values graph/v1 metadata may hold, so `meta` stays a flat record any consumer can tabulate. `z.number()` already rejects NaN and the infinities, which JSON cannot carry. */
64
+ const JsonScalarSchema = z.union([
65
+ z.string(),
66
+ z.number(),
67
+ z.boolean(),
68
+ z.null()
69
+ ]);
70
+ /** A node: an id, an optional type, and whatever else the dataset carries. The target fixes only what every graph consumer needs, so extra fields pass through untouched. */
71
+ const GraphNodeSchema = z.looseObject({
72
+ id: z.string().min(1),
73
+ type: z.string().min(1).optional()
74
+ });
75
+ /** An edge: source and target node ids, an optional type, and whatever else the dataset carries. Direction is the dataset's own: graph/v1 records it in `meta`, never by duplicating edges. */
76
+ const GraphEdgeSchema = z.looseObject({
77
+ source: z.string().min(1),
78
+ target: z.string().min(1),
79
+ type: z.string().min(1).optional()
80
+ });
81
+ /**
82
+ * The graph/v1 normalisation target: `{ meta, nodes, edges }`, the shape the thesis pipeline's benchmark cache already uses. It describes the result `fetchDataset` resolves a graph entry to, not what a source file may contain. Beyond the field shapes it checks the two properties every consumer relies on: node ids are unique, and every edge endpoint names a node.
83
+ */
84
+ const GraphV1Schema = define(z.object({
85
+ meta: recordOf(z.string().min(1), JsonScalarSchema),
86
+ nodes: z.array(GraphNodeSchema),
87
+ edges: z.array(GraphEdgeSchema)
88
+ }).strict().superRefine((graph, ctx) => {
89
+ const ids = /* @__PURE__ */ new Set();
90
+ for (const [index, node] of graph.nodes.entries()) {
91
+ if (ids.has(node.id)) ctx.addIssue({
92
+ code: "custom",
93
+ path: [
94
+ "nodes",
95
+ index,
96
+ "id"
97
+ ],
98
+ message: `duplicate node id ${JSON.stringify(node.id)}`
99
+ });
100
+ ids.add(node.id);
101
+ }
102
+ for (const [index, edge] of graph.edges.entries()) for (const end of ["source", "target"]) if (!ids.has(edge[end])) ctx.addIssue({
103
+ code: "custom",
104
+ path: [
105
+ "edges",
106
+ index,
107
+ end
108
+ ],
109
+ message: `edge ${end} ${JSON.stringify(edge[end])} names no node`
110
+ });
111
+ }));
112
+ //#endregion
113
+ //#region src/categories.ts
114
+ /** Every category id the registry knows. A category id is `<kind>/v<version>`: the kind of data, and the version of its normalisation target, so a breaking change to a target is a new category rather than an edit to an old one. */
115
+ const CategoryIdSchema = z.enum(["graph/v1"]);
116
+ /** The registry. Adding a category is adding its id to `CategoryIdSchema` and its entry here, with its target schema beside graph/v1's in src/targets/: the mapped type makes the compiler reject a registry that misses an id or has one too many. */
117
+ const registry = { "graph/v1": {
118
+ id: "graph/v1",
119
+ description: "A graph as { meta, nodes, edges }: a flat record of scalar metadata, nodes carrying at least an id, and edges carrying at least source and target node ids, with optional node and edge types.",
120
+ target: GraphV1Schema
121
+ } };
122
+ function getCategory(id) {
123
+ const parsed = CategoryIdSchema.safeParse(id);
124
+ if (!parsed.success) throw new UnknownCategoryError(id, CategoryIdSchema.options);
125
+ return registry[parsed.data];
126
+ }
127
+ function getTarget(id) {
128
+ return getCategory(id).target;
129
+ }
130
+ /**
131
+ * The kind of data a category holds: the part of its id before the version.
132
+ * @param id - A category id.
133
+ * @returns The kind, such as `graph`.
134
+ */
135
+ function kindOf(id) {
136
+ return id.slice(0, id.indexOf("/"));
137
+ }
138
+ /** @returns Every registered category, in id order. */
139
+ function listCategories() {
140
+ return CategoryIdSchema.options.map((id) => getCategory(id));
141
+ }
142
+ //#endregion
143
+ //#region src/entry.ts
144
+ /** A stable identifier: an entry id, a catalogue id. It starts with a letter or digit, so no id can be `__proto__` or any other name that begins with an underscore. Entry ids stay the same across forks, which is what lets two catalogues be compared entry by entry. */
145
+ const IdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, { message: "must start with a letter or digit and contain only letters, digits, '.', '_' and '-'" });
146
+ /** A key of an entry's `properties` or `expectedCounts` record: an identifier that starts with a letter, so it can never be `__proto__` or another name Object.prototype reacts to on assignment. */
147
+ const RecordKeySchema = z.string().regex(/^[A-Za-z][A-Za-z0-9]*$/, { message: "must be a letter followed by letters or digits" });
148
+ /** A calendar date, `YYYY-MM-DD`. */
149
+ const IsoDateSchema = z.iso.date();
150
+ /** An http or https URL: the only schemes a recipe may download from. */
151
+ const HttpUrlSchema = z.httpUrl();
152
+ /** A Wayback Machine timestamp, `YYYYMMDD` or `YYYYMMDDhhmmss`: the moment the snapshot is pinned to. */
153
+ const WaybackTimestampSchema = z.string().regex(/^\d{8}(?:\d{6})?$/, { message: "must be a Wayback timestamp, YYYYMMDD or YYYYMMDDhhmmss" });
154
+ /**
155
+ * The Wayback Machine URL of a snapshot of `url` at `timestamp`: the page form a person follows. The raw bytes a recipe downloads come from the `id_` form, which `fetchDataset` derives from this one.
156
+ * @param timestamp - A Wayback timestamp.
157
+ * @param url - The original URL.
158
+ * @returns The snapshot URL.
159
+ */
160
+ function waybackUrl(timestamp, url) {
161
+ return `https://web.archive.org/web/${timestamp}/${url}`;
162
+ }
163
+ /** A pinned Wayback Machine snapshot of a source. The timestamp is part of the recipe's reproducibility, not incidental metadata, so it travels with the URL and the two are checked against each other. */
164
+ const SnapshotSchema = z.object({
165
+ url: HttpUrlSchema,
166
+ timestamp: WaybackTimestampSchema
167
+ }).strict();
168
+ /**
169
+ * Where a dataset's bytes come from. A `dead` source no longer serves the bytes the recipe was written against and must carry the snapshot the recipe fetches instead; a `live` source may also carry one, as the pinned record of what it served. The snapshot URL must be exactly `waybackUrl(timestamp, url)`.
170
+ */
171
+ const SourceSchema = z.object({
172
+ url: HttpUrlSchema,
173
+ retrieved: IsoDateSchema,
174
+ status: z.enum(["live", "dead"]),
175
+ snapshot: SnapshotSchema.optional()
176
+ }).strict().superRefine((source, ctx) => {
177
+ if (source.status === "dead" && source.snapshot === void 0) ctx.addIssue({
178
+ code: "custom",
179
+ path: ["snapshot"],
180
+ message: "a dead source must pin the Wayback snapshot its recipe fetches"
181
+ });
182
+ if (source.snapshot !== void 0 && source.snapshot.url !== waybackUrl(source.snapshot.timestamp, source.url)) ctx.addIssue({
183
+ code: "custom",
184
+ path: ["snapshot", "url"],
185
+ message: `must be ${waybackUrl(source.snapshot.timestamp, source.url)}, the snapshot of this source's url at this timestamp`
186
+ });
187
+ });
188
+ /** The licence a dataset is published under: an SPDX identifier, a named licence with the URL of its terms, or a statement that the source records none. A licence the source does not state is recorded as unrecorded rather than guessed. */
189
+ const LicenceSchema = z.discriminatedUnion("kind", [
190
+ z.object({
191
+ kind: z.literal("spdx"),
192
+ id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9.+-]*$/, { message: "must be an SPDX licence identifier" })
193
+ }).strict(),
194
+ z.object({
195
+ kind: z.literal("custom"),
196
+ name: z.string().min(1),
197
+ url: HttpUrlSchema
198
+ }).strict(),
199
+ z.object({ kind: z.literal("unrecorded") }).strict()
200
+ ]);
201
+ /** A work to cite for the dataset: a formatted reference, with its DOI or URL where the source records one. */
202
+ const CitationSchema = z.object({
203
+ text: z.string().min(1),
204
+ doi: z.string().regex(/^10\.\d{4,9}\/\S+$/, { message: "must be a bare DOI, 10.<registrant>/<suffix>" }).optional(),
205
+ url: HttpUrlSchema.optional()
206
+ }).strict();
207
+ /** A relative path inside an archive or a catalogue: `/`-separated segments, none empty, `.` or `..`, so it can never climb out of the directory it is resolved against. */
208
+ const RelativePathSchema = z.string().refine((path) => path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."), { message: "must be a relative path of non-empty segments with no '.' or '..'" });
209
+ /** Which file of an archive to read: a member path, matched against a member's whole path or its trailing segments (so `net.gml` finds `net/net.gml`), or the member whose name ends with an extension. Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
210
+ const ArchiveMemberSchema = z.union([z.object({ path: RelativePathSchema }).strict(), z.object({ extension: z.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9.]*$/, { message: "must be a file extension such as .mtx" }) }).strict()]);
211
+ /** How the downloaded bytes unpack into the one file the parser reads. */
212
+ const ArchiveSchema = z.discriminatedUnion("kind", [
213
+ z.object({ kind: z.literal("none") }).strict(),
214
+ z.object({ kind: z.literal("gzip") }).strict(),
215
+ z.object({
216
+ kind: z.literal("zip"),
217
+ member: ArchiveMemberSchema
218
+ }).strict(),
219
+ z.object({
220
+ kind: z.literal("tar"),
221
+ member: ArchiveMemberSchema
222
+ }).strict(),
223
+ z.object({
224
+ kind: z.literal("tar.gz"),
225
+ member: ArchiveMemberSchema
226
+ }).strict()
227
+ ]);
228
+ const ColumnSchema = z.int().min(0);
229
+ /** The text encoding of the unpacked file. Decoding is strict: bytes that are not valid in the stated encoding fail the recipe rather than turning into replacement characters. */
230
+ const EncodingSchema = z.enum(["utf-8", "latin1"]);
231
+ /** The parser that reads the unpacked file into the dataset's native JSON shape, with the options that format needs. Every option is stated in the entry, so a recipe reads the same way wherever it runs. */
232
+ const ParserSchema = z.discriminatedUnion("format", [
233
+ z.object({
234
+ format: z.literal("edge-list"),
235
+ encoding: EncodingSchema,
236
+ delimiter: z.enum([
237
+ "whitespace",
238
+ "tab",
239
+ "comma",
240
+ "space"
241
+ ]),
242
+ skipLines: z.int().min(0),
243
+ commentPrefixes: z.array(z.string().min(1)),
244
+ sourceColumn: ColumnSchema,
245
+ targetColumn: ColumnSchema
246
+ }).strict().refine((parser) => parser.sourceColumn !== parser.targetColumn, {
247
+ message: "sourceColumn and targetColumn must differ",
248
+ path: ["targetColumn"]
249
+ }),
250
+ z.object({
251
+ format: z.literal("matrix-market"),
252
+ encoding: EncodingSchema
253
+ }).strict(),
254
+ z.object({
255
+ format: z.literal("gml"),
256
+ encoding: EncodingSchema
257
+ }).strict(),
258
+ z.object({
259
+ format: z.literal("pajek"),
260
+ encoding: EncodingSchema
261
+ }).strict()
262
+ ]);
263
+ /** How to obtain the dataset: a fetchable recipe naming the source (by index into `sources`), the archive layout and the parser, or a statement that no declarative recipe exists yet and why. */
264
+ const RecipeSchema = z.discriminatedUnion("status", [z.object({
265
+ status: z.literal("fetchable"),
266
+ source: z.int().min(0),
267
+ archive: ArchiveSchema,
268
+ parser: ParserSchema
269
+ }).strict(), z.object({
270
+ status: z.literal("not-fetchable"),
271
+ reason: z.string().min(1)
272
+ }).strict()]);
273
+ /** The twindex crosswalk from the native shape to the category target: the document inline, or a `.json` file path relative to the catalogue root, so entries that share a native shape share one crosswalk. */
274
+ const CrosswalkReferenceSchema = z.union([CrosswalkSchema, z.object({ file: RelativePathSchema.refine((path) => path.endsWith(".json"), { message: "must name a .json file" }) }).strict()]);
275
+ /**
276
+ * One catalogue entry: the descriptor (id, name, description, domain, tags), the category whose target the entry resolves to, where the bytes come from, the licence and citations, the recipe, the crosswalk, the counts a fetched result is expected to have and the dataset's recorded properties. The kind of data is the category's (`kindOf(category)`), never a second field that could disagree with it.
277
+ */
278
+ const EntrySchema = define(z.object({
279
+ id: IdSchema,
280
+ name: z.string().min(1),
281
+ description: z.string().min(1).optional(),
282
+ domain: z.string().min(1).optional(),
283
+ tags: z.array(z.string().min(1)).refine((tags) => new Set(tags).size === tags.length, { message: "tags must be unique" }),
284
+ category: CategoryIdSchema,
285
+ sources: z.array(SourceSchema).min(1),
286
+ licence: LicenceSchema,
287
+ citations: z.array(CitationSchema),
288
+ recipe: RecipeSchema,
289
+ crosswalk: CrosswalkReferenceSchema,
290
+ expectedCounts: recordOf(RecordKeySchema, z.int().min(0)),
291
+ properties: recordOf(RecordKeySchema, JsonScalarSchema)
292
+ }).strict().superRefine((entry, ctx) => {
293
+ if (entry.recipe.status === "fetchable" && entry.recipe.source >= entry.sources.length) ctx.addIssue({
294
+ code: "custom",
295
+ path: ["recipe", "source"],
296
+ message: `names source ${String(entry.recipe.source)} but the entry has ${String(entry.sources.length)}`
297
+ });
298
+ if ("target" in entry.crosswalk && entry.crosswalk.target !== entry.category) ctx.addIssue({
299
+ code: "custom",
300
+ path: ["crosswalk", "target"],
301
+ message: `must be the entry's category, ${entry.category}`
302
+ });
303
+ }));
304
+ /**
305
+ * Collapses Zod issues into the package's own issue shape, so the public error surface stays stable across Zod versions.
306
+ * @param error - A Zod error.
307
+ * @returns One issue per Zod issue, with its path written as dotted text.
308
+ */
309
+ function toIssues(error) {
310
+ return error.issues.map((issue) => ({
311
+ path: issue.path.map(String).join(".") || "(root)",
312
+ message: issue.message
313
+ }));
314
+ }
315
+ /**
316
+ * Parses an unknown value as a catalogue entry.
317
+ * @param doc - The candidate entry document.
318
+ * @returns The validated entry.
319
+ * @throws CatalogueSyntaxError carrying every issue found.
320
+ */
321
+ function parseEntry(doc) {
322
+ const result = EntrySchema.safeParse(doc);
323
+ if (!result.success) throw new CatalogueSyntaxError("entry", toIssues(result.error));
324
+ return result.data;
325
+ }
326
+ //#endregion
327
+ //#region src/manifest.ts
328
+ /** A semantic version, `MAJOR.MINOR.PATCH` with optional pre-release and build parts (https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string). */
329
+ const SemVerSchema = z.string().regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/, { message: "must be a semantic version, MAJOR.MINOR.PATCH" });
330
+ /** Where a forked catalogue came from: the catalogue it was forked from, the version it forked at and the repository that holds it, which together are what lets a fork's entries be compared with, and brought back to, their origin. */
331
+ const LineageSchema = z.object({
332
+ catalogue: IdSchema,
333
+ version: SemVerSchema,
334
+ repository: z.httpUrl()
335
+ }).strict();
336
+ /**
337
+ * A catalogue manifest: the catalogue's own id and version, where it lives, and, for a fork, its lineage. A catalogue is a self-contained set of entry documents beside its manifest, and a fork is its own catalogue with its own id, so lineage may never name the catalogue itself.
338
+ */
339
+ const ManifestSchema = define(z.object({
340
+ id: IdSchema,
341
+ name: z.string().min(1),
342
+ description: z.string().min(1),
343
+ version: SemVerSchema,
344
+ repository: z.httpUrl(),
345
+ lineage: LineageSchema.optional()
346
+ }).strict().superRefine((manifest, ctx) => {
347
+ if (manifest.lineage?.catalogue === manifest.id) ctx.addIssue({
348
+ code: "custom",
349
+ path: ["lineage", "catalogue"],
350
+ message: "a fork is its own catalogue: lineage must name the catalogue it was forked from, not itself"
351
+ });
352
+ }));
353
+ /**
354
+ * Parses an unknown value as a catalogue manifest.
355
+ * @param doc - The candidate manifest document.
356
+ * @returns The validated manifest.
357
+ * @throws CatalogueSyntaxError carrying every issue found.
358
+ */
359
+ function parseManifest(doc) {
360
+ const result = ManifestSchema.safeParse(doc);
361
+ if (!result.success) throw new CatalogueSyntaxError("manifest", toIssues(result.error));
362
+ return result.data;
363
+ }
364
+ //#endregion
365
+ //#region src/serialise.ts
366
+ /**
367
+ * Serialises a catalogue document as its file holds it: two-space indented JSON in the schema's key order with a trailing newline. A parsed document keeps the schema's key order, so parsing a file written this way and serialising the result reproduces the file byte for byte.
368
+ * @param doc - A parsed entry or manifest.
369
+ * @returns The file text.
370
+ */
371
+ function serialise(doc) {
372
+ return `${JSON.stringify(doc, void 0, 2)}\n`;
373
+ }
374
+ //#endregion
375
+ export { ArchiveMemberSchema, ArchiveSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, ParserSchema, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, getCategory, getTarget, kindOf, listCategories, parseEntry, parseManifest, serialise, waybackUrl };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.0.4",
4
+ "version": "0.2.0",
5
5
  "dependencies": {
6
- "zod": "4.6.6"
6
+ "twindex": "0.1.1",
7
+ "zod": "4.6.5"
7
8
  },
8
9
  "devDependencies": {
9
10
  "@arethetypeswrong/cli": "0.18.5",
@@ -15,6 +16,8 @@
15
16
  "@semantic-release/git": "11.0.1",
16
17
  "@types/node": "26.6.5",
17
18
  "@vitest/coverage-v8": "5.0.3",
19
+ "ajv": "8.20.0",
20
+ "ajv-formats": "3.0.1",
18
21
  "canonicalize": "5.1.0",
19
22
  "conventional-changelog-conventionalcommits": "10.4.1",
20
23
  "eslint": "10.12.0",
@@ -65,13 +68,13 @@
65
68
  "url": "git+https://github.com/ExaDev/keepset.git"
66
69
  },
67
70
  "scripts": {
68
- "build": "tsdown",
71
+ "build": "tsdown && tsx scripts/generate-json-schema.ts",
69
72
  "lint": "eslint . --fix --cache --max-warnings 0",
70
73
  "lint:check": "eslint . --cache --max-warnings 0",
71
74
  "prepare": "husky",
72
- "prepublishOnly": "pnpm run lint:check && pnpm run typecheck && pnpm run test && pnpm run build && publint && attw --pack --profile esm-only",
75
+ "prepublishOnly": "pnpm run lint:check && pnpm run build && pnpm run typecheck && pnpm run test && publint && attw --pack --profile esm-only",
73
76
  "test": "vitest run",
74
- "typecheck": "tsc -p tsconfig.json"
77
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.node.json"
75
78
  },
76
79
  "sideEffects": false,
77
80
  "type": "module",
@@ -0,0 +1 @@
1
+ {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.1/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"category":{"enum":["graph/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extension":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"}},"required":["extension"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.2.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
@@ -0,0 +1 @@
1
+ {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.2.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}