keepset 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,7 +63,7 @@ The catalogue itself is a manifest (`catalogue/manifest.json`: id, version, repo
63
63
 
64
64
  ## Categories and normalisation targets
65
65
 
66
- A category names a normalisation target: the shape every entry in that category resolves to, whatever its native shape. `graph/v1` is the first, `{ meta, nodes, edges }` with unique node ids and edges whose endpoints name nodes, seeded from 823 graph datasets surveyed from about forty upstream collections (SuiteSparse, Netzschleuder, SNAP, KONECT, OGB and others). A new category starts by defining its target schema in `src/targets/` and registering it in the category registry; an entry cannot name a category that does not exist. The statically generated site renders every entry with schema.org Dataset JSON-LD, so the catalogue surfaces in the dataset discovery layers.
66
+ A category names a normalisation target: the shape every entry in that category resolves to, whatever its native shape. `graph/v1` is the first, `{ meta, nodes, edges }` with unique node ids and edges whose endpoints name nodes, seeded from 823 graph datasets surveyed from about forty upstream collections (SuiteSparse, Netzschleuder, SNAP, KONECT, OGB and others). `or/v1` is the second: `{ meta, nodes }`, an operations-research instance as the nodes it offers to visit, each flat with optional coordinates, prize, prize probability, duration, travel time, skill, territory, lifecycle status and times, and deadline, read from tabular sources by the `csv-table` parser. A new category starts by defining its target schema in `src/targets/` and registering it in the category registry; an entry cannot name a category that does not exist. The statically generated site renders every entry with schema.org Dataset JSON-LD, so the catalogue surfaces in the dataset discovery layers.
67
67
 
68
68
  ## Licensing
69
69
 
@@ -99,14 +99,40 @@ function recordOf(key, value) {
99
99
  }, z.record(key, value));
100
100
  }
101
101
  //#endregion
102
- //#region src/targets/graph-v1.ts
103
- /** 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. */
102
+ //#region src/targets/json-scalar.ts
103
+ /** A JSON scalar: the only values a target's 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. */
104
104
  const JsonScalarSchema = z.union([
105
105
  z.string(),
106
106
  z.number(),
107
107
  z.boolean(),
108
108
  z.null()
109
109
  ]);
110
+ //#endregion
111
+ //#region src/targets/node-ids.ts
112
+ /**
113
+ * Reports each node whose id an earlier node already has, as an issue on that node's id.
114
+ * @param nodes - The nodes of a target document, in order.
115
+ * @param ctx - The refinement context to report to.
116
+ * @returns The set of ids the nodes carry.
117
+ */
118
+ function addDuplicateIdIssues(nodes, ctx) {
119
+ const ids = /* @__PURE__ */ new Set();
120
+ for (const [index, node] of nodes.entries()) {
121
+ if (ids.has(node.id)) ctx.addIssue({
122
+ code: "custom",
123
+ path: [
124
+ "nodes",
125
+ index,
126
+ "id"
127
+ ],
128
+ message: `duplicate node id ${JSON.stringify(node.id)}`
129
+ });
130
+ ids.add(node.id);
131
+ }
132
+ return ids;
133
+ }
134
+ //#endregion
135
+ //#region src/targets/graph-v1.ts
110
136
  /** 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. */
111
137
  const GraphNodeSchema = z.looseObject({
112
138
  id: z.string().min(1),
@@ -126,19 +152,7 @@ const GraphV1Schema = define(z.object({
126
152
  nodes: z.array(GraphNodeSchema),
127
153
  edges: z.array(GraphEdgeSchema)
128
154
  }).strict().superRefine((graph, ctx) => {
129
- const ids = /* @__PURE__ */ new Set();
130
- for (const [index, node] of graph.nodes.entries()) {
131
- if (ids.has(node.id)) ctx.addIssue({
132
- code: "custom",
133
- path: [
134
- "nodes",
135
- index,
136
- "id"
137
- ],
138
- message: `duplicate node id ${JSON.stringify(node.id)}`
139
- });
140
- ids.add(node.id);
141
- }
155
+ const ids = addDuplicateIdIssues(graph.nodes, ctx);
142
156
  for (const [index, edge] of graph.edges.entries()) for (const end of ["source", "target"]) if (!ids.has(edge[end])) ctx.addIssue({
143
157
  code: "custom",
144
158
  path: [
@@ -150,16 +164,62 @@ const GraphV1Schema = define(z.object({
150
164
  });
151
165
  }));
152
166
  //#endregion
167
+ //#region src/targets/or-v1.ts
168
+ /** The largest latitude and longitude, in degrees, either side of zero. */
169
+ const LATITUDE_LIMIT = 90;
170
+ const LONGITUDE_LIMIT = 180;
171
+ /** A value a node may not have: every field below is optional and nullable, null being what a source's empty cell becomes. */
172
+ function maybe(schema) {
173
+ return schema.nullable().optional();
174
+ }
175
+ /**
176
+ * A node of an operations-research instance: a place or case a route may visit, flat so a twindex crosswalk can map a table's columns onto it (a wildcard path must keep the same shape on both sides). The target fixes only what the datasets share and lets a crosswalk map any further column through under its own name, as graph/v1 does. `lat` and `lon` are WGS84 degrees and `x` and `y` are planar coordinates in the dataset's own system; `duration` and `travelTime` are seconds, `prize` is the dataset's own reward measure, and `prizeProbability` is the chance the reward is collected. `skill` and `territory` name what is needed to serve the node and where it sits, `status` is where it is in its lifecycle, and `opened`, `closed` and `deadline` are its lifecycle times as the source writes them, a deadline being a number where the source gives a time of day.
177
+ */
178
+ const OrNodeSchema = z.looseObject({
179
+ id: z.string().min(1),
180
+ lat: maybe(z.number().min(-90).max(LATITUDE_LIMIT)),
181
+ lon: maybe(z.number().min(-180).max(LONGITUDE_LIMIT)),
182
+ x: maybe(z.number()),
183
+ y: maybe(z.number()),
184
+ prize: maybe(z.number()),
185
+ prizeProbability: maybe(z.number().min(0).max(1)),
186
+ duration: maybe(z.number().min(0)),
187
+ travelTime: maybe(z.number().min(0)),
188
+ skill: maybe(z.string().min(1)),
189
+ territory: maybe(z.string().min(1)),
190
+ status: maybe(z.string().min(1)),
191
+ opened: maybe(z.string().min(1)),
192
+ closed: maybe(z.string().min(1)),
193
+ deadline: maybe(z.union([z.string().min(1), z.number()]))
194
+ });
195
+ /**
196
+ * The or/v1 normalisation target: `{ meta, nodes }`, an operations-research instance as the nodes it offers to visit. It describes the result `fetchDataset` resolves an entry to, not what a source file may contain. Beyond the field shapes it checks that node ids are unique. It has no arcs: a source that carries a travel matrix or arc list keeps it out of this version.
197
+ */
198
+ const OrV1Schema = define(z.object({
199
+ meta: recordOf(z.string().min(1), JsonScalarSchema),
200
+ nodes: z.array(OrNodeSchema)
201
+ }).strict().superRefine((instance, ctx) => {
202
+ addDuplicateIdIssues(instance.nodes, ctx);
203
+ }));
204
+ //#endregion
153
205
  //#region src/categories.ts
154
206
  /** 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. */
155
- const CategoryIdSchema = z.enum(["graph/v1"]);
207
+ const CategoryIdSchema = z.enum(["graph/v1", "or/v1"]);
156
208
  /** 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. */
157
- const registry = { "graph/v1": {
158
- id: "graph/v1",
159
- 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.",
160
- target: GraphV1Schema,
161
- counted: ["nodes", "edges"]
162
- } };
209
+ const registry = {
210
+ "graph/v1": {
211
+ id: "graph/v1",
212
+ 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.",
213
+ target: GraphV1Schema,
214
+ counted: ["nodes", "edges"]
215
+ },
216
+ "or/v1": {
217
+ id: "or/v1",
218
+ description: "An operations-research instance as { meta, nodes }: a flat record of scalar metadata and nodes carrying at least an id, with optional coordinates, prize, prize probability, duration, travel time, skill, territory, lifecycle status and times, and deadline.",
219
+ target: OrV1Schema,
220
+ counted: ["nodes"]
221
+ }
222
+ };
163
223
  function getCategory(id) {
164
224
  const parsed = CategoryIdSchema.safeParse(id);
165
225
  if (!parsed.success) throw new UnknownCategoryError(id, CategoryIdSchema.options);
@@ -181,6 +241,18 @@ function listCategories() {
181
241
  return CategoryIdSchema.options.map((id) => getCategory(id));
182
242
  }
183
243
  /**
244
+ * The length of one of a target's collections.
245
+ * @param target - A resolved target.
246
+ * @param key - A key its category counts, so one that holds an array.
247
+ * @returns The collection's length.
248
+ * @throws TypeError when the key holds no array, which a registry entry's `counted` never names.
249
+ */
250
+ function countOf(target, key) {
251
+ const collection = Object.entries(target).find(([name]) => name === key)?.[1];
252
+ if (!Array.isArray(collection)) throw new TypeError(`the target has no collection ${key}`);
253
+ return collection.length;
254
+ }
255
+ /**
184
256
  * Counts the collections an entry may record expected counts for, in a resolved target.
185
257
  * @param id - The target's category.
186
258
  * @param target - A resolved target of that category.
@@ -188,7 +260,7 @@ function listCategories() {
188
260
  */
189
261
  function countTarget(id, target) {
190
262
  const category = getCategory(id);
191
- return new Map(category.counted.map((key) => [key, target[key].length]));
263
+ return new Map(category.counted.map((key) => [key, countOf(target, key)]));
192
264
  }
193
265
  //#endregion
194
266
  //#region src/own.ts
@@ -358,6 +430,8 @@ const ArchiveSchema = z.discriminatedUnion("kind", [
358
430
  const ColumnSchema = z.int().min(0);
359
431
  /** 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. */
360
432
  const EncodingSchema = z.enum(["utf-8", "latin1"]);
433
+ /** A column of a table as the table parser names it: the header with every run of characters outside letters, digits and underscore replaced by one underscore, so it is a valid segment of a twindex path. */
434
+ const ColumnNameSchema = z.string().regex(/^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$/, { message: "must be a column name as the parser spells it: letters, digits and underscores, starting with a letter and not ending with an underscore" });
361
435
  /** 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. */
362
436
  const ParserSchema = z.discriminatedUnion("format", [
363
437
  z.object({
@@ -389,7 +463,25 @@ const ParserSchema = z.discriminatedUnion("format", [
389
463
  z.object({
390
464
  format: z.literal("pajek"),
391
465
  encoding: EncodingSchema
392
- }).strict()
466
+ }).strict(),
467
+ z.object({
468
+ format: z.literal("csv-table"),
469
+ encoding: EncodingSchema,
470
+ delimiter: z.enum([
471
+ "comma",
472
+ "tab",
473
+ "semicolon"
474
+ ]),
475
+ skipLines: z.int().min(0),
476
+ idColumn: ColumnNameSchema.nullable(),
477
+ numericColumns: z.array(z.object({
478
+ column: ColumnNameSchema,
479
+ factor: z.number().positive()
480
+ }).strict())
481
+ }).strict().refine((parser) => new Set(parser.numericColumns.map((numeric) => numeric.column)).size === parser.numericColumns.length, {
482
+ message: "a column may be declared numeric once",
483
+ path: ["numericColumns"]
484
+ })
393
485
  ]);
394
486
  /** 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. */
395
487
  const RecipeSchema = z.discriminatedUnion("status", [z.object({
@@ -47817,4 +47909,4 @@ function getManifest() {
47817
47909
  return catalogue().manifest;
47818
47910
  }
47819
47911
  //#endregion
47820
- export { waybackUrl as A, listCategories as B, RecipeSchema as C, WaybackTimestampSchema as D, SourceSchema as E, CategoryIdSchema as F, ArchiveError as G, GraphNodeSchema as H, countTarget as I, DatasetNotFoundError as J, CatalogueSyntaxError as K, getCategory as L, groupsOf as M, namesOf as N, parseEntry as O, getOwn as P, UnknownCategoryError as Q, getTarget as R, RESERVED_META_KEYS as S, SnapshotSchema as T, GraphV1Schema as U, GraphEdgeSchema as V, JsonScalarSchema as W, NativeParseError as X, KeepsetError as Y, RecipeError as Z, HttpUrlSchema as _, CatalogueIndexSchema as a, NamespaceSchema as b, ManifestSchema as c, AliasSchema as d, ArchiveMemberSchema as f, EntrySchema as g, CrosswalkReferenceSchema as h, listNamespaces as i, NAME_SEPARATOR as j, toIssues as k, SemVerSchema as l, CitationSchema as m, getManifest as n, createCatalogue as o, ArchiveSchema as p, DatasetAmbiguousError as q, listDatasets as r, LineageSchema as s, getDataset as t, parseManifest as u, IdSchema as v, RelativePathSchema as w, ParserSchema as x, LicenceSchema as y, kindOf as z };
47912
+ export { RecipeError as $, waybackUrl as A, listCategories as B, RecipeSchema as C, WaybackTimestampSchema as D, SourceSchema as E, CategoryIdSchema as F, GraphV1Schema as G, OrV1Schema as H, countTarget as I, CatalogueSyntaxError as J, JsonScalarSchema as K, getCategory as L, groupsOf as M, namesOf as N, parseEntry as O, getOwn as P, NativeParseError as Q, getTarget as R, RESERVED_META_KEYS as S, SnapshotSchema as T, GraphEdgeSchema as U, OrNodeSchema as V, GraphNodeSchema as W, DatasetNotFoundError as X, DatasetAmbiguousError as Y, KeepsetError as Z, HttpUrlSchema as _, CatalogueIndexSchema as a, NamespaceSchema as b, ManifestSchema as c, AliasSchema as d, UnknownCategoryError as et, ArchiveMemberSchema as f, EntrySchema as g, CrosswalkReferenceSchema as h, listNamespaces as i, NAME_SEPARATOR as j, toIssues as k, SemVerSchema as l, CitationSchema as m, getManifest as n, createCatalogue as o, ArchiveSchema as p, ArchiveError as q, listDatasets as r, LineageSchema as s, getDataset as t, parseManifest as u, IdSchema as v, RelativePathSchema as w, ParserSchema as x, LicenceSchema as y, kindOf as z };
@@ -134,6 +134,23 @@ declare const ParserSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
134
134
  "utf-8": "utf-8";
135
135
  latin1: "latin1";
136
136
  }>;
137
+ }, z.core.$strict>, z.ZodObject<{
138
+ format: z.ZodLiteral<"csv-table">;
139
+ encoding: z.ZodEnum<{
140
+ "utf-8": "utf-8";
141
+ latin1: "latin1";
142
+ }>;
143
+ delimiter: z.ZodEnum<{
144
+ tab: "tab";
145
+ comma: "comma";
146
+ semicolon: "semicolon";
147
+ }>;
148
+ skipLines: z.ZodInt;
149
+ idColumn: z.ZodNullable<z.ZodString>;
150
+ numericColumns: z.ZodArray<z.ZodObject<{
151
+ column: z.ZodString;
152
+ factor: z.ZodNumber;
153
+ }, z.core.$strict>>;
137
154
  }, z.core.$strict>], "format">;
138
155
  type Parser = z.infer<typeof ParserSchema>;
139
156
  /** 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. */
@@ -201,6 +218,23 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
201
218
  "utf-8": "utf-8";
202
219
  latin1: "latin1";
203
220
  }>;
221
+ }, z.core.$strict>, z.ZodObject<{
222
+ format: z.ZodLiteral<"csv-table">;
223
+ encoding: z.ZodEnum<{
224
+ "utf-8": "utf-8";
225
+ latin1: "latin1";
226
+ }>;
227
+ delimiter: z.ZodEnum<{
228
+ tab: "tab";
229
+ comma: "comma";
230
+ semicolon: "semicolon";
231
+ }>;
232
+ skipLines: z.ZodInt;
233
+ idColumn: z.ZodNullable<z.ZodString>;
234
+ numericColumns: z.ZodArray<z.ZodObject<{
235
+ column: z.ZodString;
236
+ factor: z.ZodNumber;
237
+ }, z.core.$strict>>;
204
238
  }, z.core.$strict>], "format">;
205
239
  }, z.core.$strict>, z.ZodObject<{
206
240
  status: z.ZodLiteral<"not-fetchable">;
@@ -229,6 +263,7 @@ declare const EntrySchema: z.ZodObject<{
229
263
  tags: z.ZodArray<z.ZodString>;
230
264
  category: z.ZodEnum<{
231
265
  "graph/v1": "graph/v1";
266
+ "or/v1": "or/v1";
232
267
  }>;
233
268
  sources: z.ZodArray<z.ZodObject<{
234
269
  url: z.ZodURL;
@@ -321,6 +356,23 @@ declare const EntrySchema: z.ZodObject<{
321
356
  "utf-8": "utf-8";
322
357
  latin1: "latin1";
323
358
  }>;
359
+ }, z.core.$strict>, z.ZodObject<{
360
+ format: z.ZodLiteral<"csv-table">;
361
+ encoding: z.ZodEnum<{
362
+ "utf-8": "utf-8";
363
+ latin1: "latin1";
364
+ }>;
365
+ delimiter: z.ZodEnum<{
366
+ tab: "tab";
367
+ comma: "comma";
368
+ semicolon: "semicolon";
369
+ }>;
370
+ skipLines: z.ZodInt;
371
+ idColumn: z.ZodNullable<z.ZodString>;
372
+ numericColumns: z.ZodArray<z.ZodObject<{
373
+ column: z.ZodString;
374
+ factor: z.ZodNumber;
375
+ }, z.core.$strict>>;
324
376
  }, z.core.$strict>], "format">;
325
377
  }, z.core.$strict>, z.ZodObject<{
326
378
  status: z.ZodLiteral<"not-fetchable">;
@@ -338,7 +390,7 @@ declare const EntrySchema: z.ZodObject<{
338
390
  id: string;
339
391
  name: string;
340
392
  tags: string[];
341
- category: "graph/v1";
393
+ category: "graph/v1" | "or/v1";
342
394
  sources: {
343
395
  url: string;
344
396
  retrieved: string;
@@ -400,6 +452,16 @@ declare const EntrySchema: z.ZodObject<{
400
452
  commentPrefixes: string[];
401
453
  sourceColumn: number;
402
454
  targetColumn: number;
455
+ } | {
456
+ format: "csv-table";
457
+ encoding: "utf-8" | "latin1";
458
+ delimiter: "tab" | "comma" | "semicolon";
459
+ skipLines: number;
460
+ idColumn: string | null;
461
+ numericColumns: {
462
+ column: string;
463
+ factor: number;
464
+ }[];
403
465
  } | {
404
466
  format: "matrix-market";
405
467
  encoding: "utf-8" | "latin1";
@@ -528,6 +590,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
528
590
  tags: z.ZodArray<z.ZodString>;
529
591
  category: z.ZodEnum<{
530
592
  "graph/v1": "graph/v1";
593
+ "or/v1": "or/v1";
531
594
  }>;
532
595
  sources: z.ZodArray<z.ZodObject<{
533
596
  url: z.ZodURL;
@@ -620,6 +683,23 @@ declare const CatalogueIndexSchema: z.ZodObject<{
620
683
  "utf-8": "utf-8";
621
684
  latin1: "latin1";
622
685
  }>;
686
+ }, z.core.$strict>, z.ZodObject<{
687
+ format: z.ZodLiteral<"csv-table">;
688
+ encoding: z.ZodEnum<{
689
+ "utf-8": "utf-8";
690
+ latin1: "latin1";
691
+ }>;
692
+ delimiter: z.ZodEnum<{
693
+ tab: "tab";
694
+ comma: "comma";
695
+ semicolon: "semicolon";
696
+ }>;
697
+ skipLines: z.ZodInt;
698
+ idColumn: z.ZodNullable<z.ZodString>;
699
+ numericColumns: z.ZodArray<z.ZodObject<{
700
+ column: z.ZodString;
701
+ factor: z.ZodNumber;
702
+ }, z.core.$strict>>;
623
703
  }, z.core.$strict>], "format">;
624
704
  }, z.core.$strict>, z.ZodObject<{
625
705
  status: z.ZodLiteral<"not-fetchable">;
@@ -637,7 +717,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
637
717
  id: string;
638
718
  name: string;
639
719
  tags: string[];
640
- category: "graph/v1";
720
+ category: "graph/v1" | "or/v1";
641
721
  sources: {
642
722
  url: string;
643
723
  retrieved: string;
@@ -699,6 +779,16 @@ declare const CatalogueIndexSchema: z.ZodObject<{
699
779
  commentPrefixes: string[];
700
780
  sourceColumn: number;
701
781
  targetColumn: number;
782
+ } | {
783
+ format: "csv-table";
784
+ encoding: "utf-8" | "latin1";
785
+ delimiter: "tab" | "comma" | "semicolon";
786
+ skipLines: number;
787
+ idColumn: string | null;
788
+ numericColumns: {
789
+ column: string;
790
+ factor: number;
791
+ }[];
702
792
  } | {
703
793
  format: "matrix-market";
704
794
  encoding: "utf-8" | "latin1";
@@ -744,7 +834,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
744
834
  id: string;
745
835
  name: string;
746
836
  tags: string[];
747
- category: "graph/v1";
837
+ category: "graph/v1" | "or/v1";
748
838
  sources: {
749
839
  url: string;
750
840
  retrieved: string;
@@ -806,6 +896,16 @@ declare const CatalogueIndexSchema: z.ZodObject<{
806
896
  commentPrefixes: string[];
807
897
  sourceColumn: number;
808
898
  targetColumn: number;
899
+ } | {
900
+ format: "csv-table";
901
+ encoding: "utf-8" | "latin1";
902
+ delimiter: "tab" | "comma" | "semicolon";
903
+ skipLines: number;
904
+ idColumn: string | null;
905
+ numericColumns: {
906
+ column: string;
907
+ factor: number;
908
+ }[];
809
909
  } | {
810
910
  format: "matrix-market";
811
911
  encoding: "utf-8" | "latin1";
@@ -892,9 +992,6 @@ interface Catalogue {
892
992
  declare function createCatalogue(index: unknown): Catalogue;
893
993
  //#endregion
894
994
  //#region src/targets/graph-v1.d.ts
895
- /** 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. */
896
- declare const JsonScalarSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
897
- type JsonScalar = z.infer<typeof JsonScalarSchema>;
898
995
  /** 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. */
899
996
  declare const GraphNodeSchema: z.ZodObject<{
900
997
  id: z.ZodString;
@@ -940,27 +1037,100 @@ declare const GraphV1Schema: z.ZodObject<{
940
1037
  };
941
1038
  type GraphV1 = z.infer<typeof GraphV1Schema>;
942
1039
  //#endregion
1040
+ //#region src/targets/or-v1.d.ts
1041
+ /**
1042
+ * A node of an operations-research instance: a place or case a route may visit, flat so a twindex crosswalk can map a table's columns onto it (a wildcard path must keep the same shape on both sides). The target fixes only what the datasets share and lets a crosswalk map any further column through under its own name, as graph/v1 does. `lat` and `lon` are WGS84 degrees and `x` and `y` are planar coordinates in the dataset's own system; `duration` and `travelTime` are seconds, `prize` is the dataset's own reward measure, and `prizeProbability` is the chance the reward is collected. `skill` and `territory` name what is needed to serve the node and where it sits, `status` is where it is in its lifecycle, and `opened`, `closed` and `deadline` are its lifecycle times as the source writes them, a deadline being a number where the source gives a time of day.
1043
+ */
1044
+ declare const OrNodeSchema: z.ZodObject<{
1045
+ id: z.ZodString;
1046
+ lat: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1047
+ lon: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1048
+ x: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1049
+ y: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1050
+ prize: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1051
+ prizeProbability: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1052
+ duration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1053
+ travelTime: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1054
+ skill: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1055
+ territory: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1056
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1057
+ opened: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1058
+ closed: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1059
+ deadline: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
1060
+ }, z.core.$loose>;
1061
+ type OrNode = z.infer<typeof OrNodeSchema>;
1062
+ /**
1063
+ * The or/v1 normalisation target: `{ meta, nodes }`, an operations-research instance as the nodes it offers to visit. It describes the result `fetchDataset` resolves an entry to, not what a source file may contain. Beyond the field shapes it checks that node ids are unique. It has no arcs: a source that carries a travel matrix or arc list keeps it out of this version.
1064
+ */
1065
+ declare const OrV1Schema: z.ZodObject<{
1066
+ meta: z.ZodPreprocess<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>, unknown>;
1067
+ nodes: z.ZodArray<z.ZodObject<{
1068
+ id: z.ZodString;
1069
+ lat: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1070
+ lon: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1071
+ x: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1072
+ y: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1073
+ prize: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1074
+ prizeProbability: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1075
+ duration: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1076
+ travelTime: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
1077
+ skill: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1078
+ territory: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1079
+ status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1080
+ opened: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1081
+ closed: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1082
+ deadline: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
1083
+ }, z.core.$loose>>;
1084
+ }, z.core.$strict> & {
1085
+ is: (value: unknown) => value is {
1086
+ meta: Record<string, string | number | boolean | null>;
1087
+ nodes: {
1088
+ [x: string]: unknown;
1089
+ id: string;
1090
+ lat?: number | null | undefined;
1091
+ lon?: number | null | undefined;
1092
+ x?: number | null | undefined;
1093
+ y?: number | null | undefined;
1094
+ prize?: number | null | undefined;
1095
+ prizeProbability?: number | null | undefined;
1096
+ duration?: number | null | undefined;
1097
+ travelTime?: number | null | undefined;
1098
+ skill?: string | null | undefined;
1099
+ territory?: string | null | undefined;
1100
+ status?: string | null | undefined;
1101
+ opened?: string | null | undefined;
1102
+ closed?: string | null | undefined;
1103
+ deadline?: string | number | null | undefined;
1104
+ }[];
1105
+ };
1106
+ };
1107
+ type OrV1 = z.infer<typeof OrV1Schema>;
1108
+ //#endregion
943
1109
  //#region src/categories.d.ts
944
1110
  /** 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. */
945
1111
  declare const CategoryIdSchema: z.ZodEnum<{
946
1112
  "graph/v1": "graph/v1";
1113
+ "or/v1": "or/v1";
947
1114
  }>;
948
1115
  type CategoryId = z.infer<typeof CategoryIdSchema>;
949
1116
  /** The normalisation target each category resolves to, keyed by category id. */
950
1117
  interface CategoryTargets {
951
1118
  readonly "graph/v1": GraphV1;
1119
+ readonly "or/v1": OrV1;
952
1120
  }
953
1121
  /** The normalised shape of any category: what `fetchDataset` can resolve to. */
954
1122
  type CategoryTarget = CategoryTargets[CategoryId];
955
1123
  /** The keys of a target whose values are arrays: the collections an entry's `expectedCounts` may name. */
956
1124
  type CountedKey<Target> = { readonly [Key in keyof Target]: Target[Key] extends readonly unknown[] ? Key : never; }[keyof Target] & string;
957
1125
  /** One category: its id, what it holds, the schema of its normalisation target, and which of the target's arrays an entry may record expected counts for. */
958
- interface Category<Id extends CategoryId = CategoryId> {
1126
+ interface CategoryOf<Id extends CategoryId> {
959
1127
  readonly id: Id;
960
1128
  readonly description: string;
961
1129
  readonly target: z.ZodType<CategoryTargets[Id]>;
962
1130
  readonly counted: readonly CountedKey<CategoryTargets[Id]>[];
963
1131
  }
1132
+ /** A category, or with no argument any of them: a union over the ids, so each category keeps its own target and counted keys. */
1133
+ type Category<Id extends CategoryId = CategoryId> = Id extends CategoryId ? CategoryOf<Id> : never;
964
1134
  /**
965
1135
  * Looks up a category by id.
966
1136
  * @param id - Any string; category ids usually come from entry documents, so the argument is untrusted.
@@ -993,4 +1163,4 @@ declare function listCategories(): readonly Category[];
993
1163
  */
994
1164
  declare function countTarget(id: CategoryId, target: CategoryTarget): ReadonlyMap<string, number>;
995
1165
  //#endregion
996
- export { Recipe as $, ManifestSchema as A, CrosswalkReference as B, Dataset as C, Lineage as D, createCatalogue as E, ArchiveMember as F, IdSchema as G, Entry as H, ArchiveMemberSchema as I, Namespace as J, Licence as K, ArchiveSchema as L, parseManifest as M, AliasSchema as N, LineageSchema as O, Archive as P, RESERVED_META_KEYS as Q, Citation as R, CatalogueIndexSchema as S, NamespaceNode as T, EntrySchema as U, CrosswalkReferenceSchema as V, HttpUrlSchema as W, Parser as X, NamespaceSchema as Y, ParserSchema as Z, GraphV1Schema as _, CategoryTargets as a, SourceSchema as at, Catalogue as b, getCategory as c, waybackUrl as ct, listCategories as d, RecipeSchema as et, GraphEdge as f, GraphV1 as g, GraphNodeSchema as h, CategoryTarget as i, Source as it, SemVerSchema as j, Manifest as k, getTarget as l, GraphNode as m, CategoryId as n, Snapshot as nt, CountedKey as o, WaybackTimestampSchema as ot, GraphEdgeSchema as p, LicenceSchema as q, CategoryIdSchema as r, SnapshotSchema as rt, countTarget as s, parseEntry as st, Category as t, RelativePathSchema as tt, kindOf as u, JsonScalar as v, DatasetQuery as w, CatalogueIndex as x, JsonScalarSchema as y, CitationSchema as z };
1166
+ export { ParserSchema as $, LineageSchema as A, Citation as B, CatalogueIndex as C, NamespaceNode as D, DatasetQuery as E, AliasSchema as F, EntrySchema as G, CrosswalkReference as H, Archive as I, Licence as J, HttpUrlSchema as K, ArchiveMember as L, ManifestSchema as M, SemVerSchema as N, createCatalogue as O, parseManifest as P, Parser as Q, ArchiveMemberSchema as R, Catalogue as S, Dataset as T, CrosswalkReferenceSchema as U, CitationSchema as V, Entry as W, Namespace as X, LicenceSchema as Y, NamespaceSchema as Z, GraphEdgeSchema as _, CategoryTargets as a, SnapshotSchema as at, GraphV1 as b, getCategory as c, WaybackTimestampSchema as ct, listCategories as d, RESERVED_META_KEYS as et, OrNode as f, GraphEdge as g, OrV1Schema as h, CategoryTarget as i, Snapshot as it, Manifest as j, Lineage as k, getTarget as l, parseEntry as lt, OrV1 as m, CategoryId as n, RecipeSchema as nt, CountedKey as o, Source as ot, OrNodeSchema as p, IdSchema as q, CategoryIdSchema as r, RelativePathSchema as rt, countTarget as s, SourceSchema as st, Category as t, Recipe as tt, kindOf as u, waybackUrl as ut, GraphNode as v, CatalogueIndexSchema as w, GraphV1Schema as x, GraphNodeSchema as y, ArchiveSchema as z };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { $ as Recipe, A as ManifestSchema, B as CrosswalkReference, C as Dataset, D as Lineage, E as createCatalogue, F as ArchiveMember, G as IdSchema, H as Entry, I as ArchiveMemberSchema, J as Namespace, K as Licence, L as ArchiveSchema, M as parseManifest, N as AliasSchema, O as LineageSchema, P as Archive, Q as RESERVED_META_KEYS, R as Citation, S as CatalogueIndexSchema, T as NamespaceNode, U as EntrySchema, V as CrosswalkReferenceSchema, W as HttpUrlSchema, X as Parser, Y as NamespaceSchema, Z as ParserSchema, _ as GraphV1Schema, a as CategoryTargets, at as SourceSchema, b as Catalogue, c as getCategory, ct as waybackUrl, d as listCategories, et as RecipeSchema, f as GraphEdge, g as GraphV1, h as GraphNodeSchema, i as CategoryTarget, it as Source, j as SemVerSchema, k as Manifest, l as getTarget, m as GraphNode, n as CategoryId, nt as Snapshot, o as CountedKey, ot as WaybackTimestampSchema, p as GraphEdgeSchema, q as LicenceSchema, r as CategoryIdSchema, rt as SnapshotSchema, s as countTarget, st as parseEntry, t as Category, tt as RelativePathSchema, u as kindOf, v as JsonScalar, w as DatasetQuery, x as CatalogueIndex, y as JsonScalarSchema, z as CitationSchema } from "./categories-DzjLQ_fp.js";
1
+ import { $ as ParserSchema, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as EntrySchema, H as CrosswalkReference, I as Archive, J as Licence, K as HttpUrlSchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as Parser, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReferenceSchema, V as CitationSchema, W as Entry, X as Namespace, Y as LicenceSchema, Z as NamespaceSchema, _ as GraphEdgeSchema, a as CategoryTargets, at as SnapshotSchema, b as GraphV1, c as getCategory, ct as WaybackTimestampSchema, d as listCategories, et as RESERVED_META_KEYS, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as Snapshot, j as Manifest, k as Lineage, l as getTarget, lt as parseEntry, m as OrV1, n as CategoryId, nt as RecipeSchema, o as CountedKey, ot as Source, p as OrNodeSchema, q as IdSchema, r as CategoryIdSchema, rt as RelativePathSchema, s as countTarget, st as SourceSchema, t as Category, tt as Recipe, u as kindOf, ut as waybackUrl, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-B4hhikbr.js";
2
2
  import { Crosswalk } from "twindex";
3
+ import { z } from "zod";
3
4
  //#region src/errors.d.ts
4
5
  /** 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
6
  export declare class KeepsetError extends Error {
@@ -109,6 +110,11 @@ export declare function namesOf(entry: Addressed, segmentAliases?: SegmentAliase
109
110
  */
110
111
  export declare function serialise(doc: Entry | Manifest | Crosswalk): string;
111
112
  //#endregion
113
+ //#region src/targets/json-scalar.d.ts
114
+ /** A JSON scalar: the only values a target's 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. */
115
+ export declare const JsonScalarSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
116
+ type JsonScalar = z.infer<typeof JsonScalarSchema>;
117
+ //#endregion
112
118
  //#region src/validate.d.ts
113
119
  /**
114
120
  * Checks a dataset as the catalogue holds it: the entry against the entry schema, and its crosswalk against twindex's structural validation and the entry's category. A catalogue index is checked the same way when it is built, so a bundled dataset is valid unless the package itself is broken; the check is what a contributor runs on an entry they are writing.
@@ -117,4 +123,4 @@ export declare function serialise(doc: Entry | Manifest | Crosswalk): string;
117
123
  */
118
124
  export declare function validateDataset(dataset: Dataset): SyntaxIssue[];
119
125
  //#endregion
120
- export { AliasSchema, type Archive, type ArchiveMember, ArchiveMemberSchema, ArchiveSchema, type Catalogue, type CatalogueIndex, CatalogueIndexSchema, type Category, type CategoryId, CategoryIdSchema, type CategoryTarget, type CategoryTargets, type Citation, CitationSchema, type CountedKey, type CrosswalkReference, CrosswalkReferenceSchema, type Dataset, type DatasetQuery, type Entry, type EntryNames, EntrySchema, type GraphEdge, GraphEdgeSchema, type GraphNode, GraphNodeSchema, type GraphV1, GraphV1Schema, HttpUrlSchema, IdSchema, type JsonScalar, JsonScalarSchema, type Licence, LicenceSchema, type Lineage, LineageSchema, type Manifest, ManifestSchema, type Namespace, type NamespaceNode, NamespaceSchema, type Parser, ParserSchema, RESERVED_META_KEYS, type Recipe, RecipeSchema, type RecipeStep, RelativePathSchema, type SegmentAliases, SemVerSchema, type Snapshot, SnapshotSchema, type Source, SourceSchema, type SyntaxIssue, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getTarget, kindOf, listCategories, parseEntry, parseManifest, waybackUrl };
126
+ export { AliasSchema, type Archive, type ArchiveMember, ArchiveMemberSchema, ArchiveSchema, type Catalogue, type CatalogueIndex, CatalogueIndexSchema, type Category, type CategoryId, CategoryIdSchema, type CategoryTarget, type CategoryTargets, type Citation, CitationSchema, type CountedKey, type CrosswalkReference, CrosswalkReferenceSchema, type Dataset, type DatasetQuery, type Entry, type EntryNames, EntrySchema, type GraphEdge, GraphEdgeSchema, type GraphNode, GraphNodeSchema, type GraphV1, GraphV1Schema, HttpUrlSchema, IdSchema, type JsonScalar, type Licence, LicenceSchema, type Lineage, LineageSchema, type Manifest, ManifestSchema, type Namespace, type NamespaceNode, NamespaceSchema, type OrNode, OrNodeSchema, type OrV1, OrV1Schema, type Parser, ParserSchema, RESERVED_META_KEYS, type Recipe, RecipeSchema, type RecipeStep, RelativePathSchema, type SegmentAliases, SemVerSchema, type Snapshot, SnapshotSchema, type Source, SourceSchema, type SyntaxIssue, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getTarget, kindOf, listCategories, parseEntry, parseManifest, waybackUrl };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as waybackUrl, B as listCategories, C as RecipeSchema, D as WaybackTimestampSchema, E as SourceSchema, F as CategoryIdSchema, G as ArchiveError, H as GraphNodeSchema, I as countTarget, J as DatasetNotFoundError, K as CatalogueSyntaxError, L as getCategory, M as groupsOf, N as namesOf, O as parseEntry, Q as UnknownCategoryError, R as getTarget, S as RESERVED_META_KEYS, T as SnapshotSchema, U as GraphV1Schema, V as GraphEdgeSchema, W as JsonScalarSchema, X as NativeParseError, Y as KeepsetError, Z as RecipeError, _ as HttpUrlSchema, a as CatalogueIndexSchema, b as NamespaceSchema, c as ManifestSchema, d as AliasSchema, f as ArchiveMemberSchema, g as EntrySchema, h as CrosswalkReferenceSchema, i as listNamespaces, j as NAME_SEPARATOR, k as toIssues, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as DatasetAmbiguousError, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as IdSchema, w as RelativePathSchema, x as ParserSchema, y as LicenceSchema, z as kindOf } from "./bundled-DNLiwEcd.js";
1
+ import { $ as RecipeError, A as waybackUrl, B as listCategories, C as RecipeSchema, D as WaybackTimestampSchema, E as SourceSchema, F as CategoryIdSchema, G as GraphV1Schema, H as OrV1Schema, I as countTarget, J as CatalogueSyntaxError, K as JsonScalarSchema, L as getCategory, M as groupsOf, N as namesOf, O as parseEntry, Q as NativeParseError, R as getTarget, S as RESERVED_META_KEYS, T as SnapshotSchema, U as GraphEdgeSchema, V as OrNodeSchema, W as GraphNodeSchema, X as DatasetNotFoundError, Y as DatasetAmbiguousError, Z as KeepsetError, _ as HttpUrlSchema, a as CatalogueIndexSchema, b as NamespaceSchema, c as ManifestSchema, d as AliasSchema, et as UnknownCategoryError, f as ArchiveMemberSchema, g as EntrySchema, h as CrosswalkReferenceSchema, i as listNamespaces, j as NAME_SEPARATOR, k as toIssues, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as ArchiveError, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as IdSchema, w as RelativePathSchema, x as ParserSchema, y as LicenceSchema, z as kindOf } from "./bundled-D9eVvy1i.js";
2
2
  import { validateCrosswalk } from "twindex";
3
3
  //#region src/serialise.ts
4
4
  /**
@@ -57,4 +57,4 @@ function validateDataset(dataset) {
57
57
  return issues;
58
58
  }
59
59
  //#endregion
60
- export { AliasSchema, ArchiveError, ArchiveMemberSchema, ArchiveSchema, CatalogueIndexSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, DatasetAmbiguousError, DatasetNotFoundError, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, HttpUrlSchema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, NAME_SEPARATOR, NamespaceSchema, NativeParseError, ParserSchema, RESERVED_META_KEYS, RecipeError, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getDataset, getManifest, getTarget, groupsOf, kindOf, listCategories, listDatasets, listNamespaces, namesOf, parseEntry, parseManifest, serialise, validateDataset, waybackUrl };
60
+ export { AliasSchema, ArchiveError, ArchiveMemberSchema, ArchiveSchema, CatalogueIndexSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, DatasetAmbiguousError, DatasetNotFoundError, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, HttpUrlSchema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, NAME_SEPARATOR, NamespaceSchema, NativeParseError, OrNodeSchema, OrV1Schema, ParserSchema, RESERVED_META_KEYS, RecipeError, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getDataset, getManifest, getTarget, groupsOf, kindOf, listCategories, listDatasets, listNamespaces, namesOf, parseEntry, parseManifest, serialise, validateDataset, waybackUrl };
@@ -1,4 +1,4 @@
1
- import { C as Dataset, H as Entry, i as CategoryTarget, it as Source, nt as Snapshot } from "../categories-DzjLQ_fp.js";
1
+ import { T as Dataset, W as Entry, i as CategoryTarget, it as Snapshot, ot as Source } from "../categories-B4hhikbr.js";
2
2
  //#region src/node/fetch-dataset.d.ts
3
3
  /** The network port a recipe downloads through: `downloadRaw` by default, which returns the bytes exactly as served, or a stand-in under test. */
4
4
  export type FetchPort = (url: string, init: {
@@ -1,4 +1,4 @@
1
- import { G as ArchiveError, I as countTarget, P as getOwn, R as getTarget, X as NativeParseError, Y as KeepsetError, Z as RecipeError, t as getDataset } from "../bundled-DNLiwEcd.js";
1
+ import { $ as RecipeError, I as countTarget, P as getOwn, Q as NativeParseError, R as getTarget, Z as KeepsetError, q as ArchiveError, t as getDataset } from "../bundled-D9eVvy1i.js";
2
2
  import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
3
3
  import { createHash } from "node:crypto";
4
4
  import { gunzipSync } from "node:zlib";
@@ -347,6 +347,151 @@ function parseMatrixMarket(text) {
347
347
  };
348
348
  }
349
349
  //#endregion
350
+ //#region src/parsers/csv-table.ts
351
+ /** The character each delimiter option splits fields on. */
352
+ const DELIMITERS = {
353
+ comma: ",",
354
+ tab: " ",
355
+ semicolon: ";"
356
+ };
357
+ /** A number as a decimal literal, with an optional sign and exponent. `Number` alone would also accept hex, binary, `Infinity` and the empty string. */
358
+ const DECIMAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
359
+ /** The byte order mark some exporters put before the first header. */
360
+ const BYTE_ORDER_MARK = "";
361
+ /**
362
+ * The name the parser gives a column: the header with every run of characters outside letters, digits and underscore replaced by one underscore, leading and trailing underscores dropped, and a name starting with a digit prefixed with `c_`. The rule depends on the header alone, so a crosswalk written against a file's headers reads the same wherever it runs.
363
+ * @param header - A header cell.
364
+ * @returns The column name, or undefined when nothing of the header is left.
365
+ */
366
+ function columnName(header) {
367
+ const name = header.trim().replaceAll(/[^A-Za-z0-9_]+/g, "_").replaceAll(/^_+|_+$/g, "");
368
+ if (name === "") return;
369
+ return /^\d/.test(name) ? `c_${name}` : name;
370
+ }
371
+ /**
372
+ * Splits text into records and fields by RFC 4180: a field may be quoted, a quoted field may hold the delimiter, line breaks and doubled quotes, and a record ends at an unquoted LF, CRLF or CR.
373
+ * @param text - The file text, after the lines the recipe skips.
374
+ * @param delimiter - The field delimiter.
375
+ * @param firstLine - The 1-based line of the file the text starts on.
376
+ * @returns The records, blank lines included as a record of one empty field.
377
+ * @throws NativeParseError when a quoted field is never closed or a quote opens inside an unquoted field.
378
+ */
379
+ function records(text, delimiter, firstLine) {
380
+ const out = [];
381
+ let fields = [];
382
+ let field = "";
383
+ let quoted = false;
384
+ let started = firstLine;
385
+ let line = firstLine;
386
+ let index = 0;
387
+ const endRecord = () => {
388
+ fields.push(field);
389
+ out.push({
390
+ line: started,
391
+ fields
392
+ });
393
+ fields = [];
394
+ field = "";
395
+ started = line;
396
+ };
397
+ while (index < text.length) {
398
+ const char = text.charAt(index);
399
+ if (quoted) {
400
+ if (char === "\"") {
401
+ if (text.charAt(index + 1) === "\"") {
402
+ field += "\"";
403
+ index += 1;
404
+ } else quoted = false;
405
+ } else {
406
+ if (char === "\n") line += 1;
407
+ field += char;
408
+ }
409
+ } else if (char === "\"") {
410
+ if (field !== "") throw new NativeParseError("csv-table", line, "a quote opens inside an unquoted field");
411
+ quoted = true;
412
+ } else if (char === delimiter) {
413
+ fields.push(field);
414
+ field = "";
415
+ } else if (char === "\n" || char === "\r") {
416
+ if (char === "\r" && text.charAt(index + 1) === "\n") index += 1;
417
+ line += 1;
418
+ endRecord();
419
+ } else field += char;
420
+ index += 1;
421
+ }
422
+ if (quoted) throw new NativeParseError("csv-table", started, "a quoted field is never closed");
423
+ if (field !== "" || fields.length > 0) endRecord();
424
+ return out;
425
+ }
426
+ /**
427
+ * Drops the first lines of a file.
428
+ * @param text - The file text.
429
+ * @param count - How many physical lines to drop.
430
+ * @returns The rest of the text.
431
+ * @throws NativeParseError when the file has fewer lines than that.
432
+ */
433
+ function afterLines(text, count) {
434
+ let index = 0;
435
+ for (let dropped = 0; dropped < count; dropped += 1) {
436
+ const next = text.slice(index).search(/\n/);
437
+ if (next === -1) throw new NativeParseError("csv-table", dropped + 1, `the file has fewer than ${String(count)} lines to skip before the header`);
438
+ index += next + 1;
439
+ }
440
+ return text.slice(index);
441
+ }
442
+ /**
443
+ * Reads a numeric cell.
444
+ * @param cell - The trimmed cell.
445
+ * @param factor - What the recipe multiplies the value by, to put it in the target's unit.
446
+ * @param column - The column, for the error.
447
+ * @param line - The 1-based line, for the error.
448
+ * @returns The number, or null for an empty cell.
449
+ * @throws NativeParseError when the cell is not a decimal number.
450
+ */
451
+ function numeric$1(cell, factor, column, line) {
452
+ if (cell === "") return null;
453
+ if (!DECIMAL.test(cell)) throw new NativeParseError("csv-table", line, `column ${column} is declared numeric but holds ${JSON.stringify(cell)}`);
454
+ return Number(cell) * factor;
455
+ }
456
+ /**
457
+ * Parses a delimited table into rows: the first record after `skipLines` lines is the header, each later record is a row, and every row carries every column but the id column, an empty cell being null. Cells are trimmed. A column the recipe declares numeric is read as a decimal number times its factor and any other column stays a string. A row's id is the cell of the recipe's id column, which is then not also a column, or its 1-based row number when the recipe names none. Blank lines are skipped; a row with a different number of fields than the header, a header cell with no name, two columns with the same name, and an id cell that is empty are errors.
458
+ * @param text - The file text.
459
+ * @param options - The recipe's table parser.
460
+ * @returns The rows, in file order.
461
+ * @throws NativeParseError naming the line of the first problem.
462
+ */
463
+ function parseCsvTable(text, options) {
464
+ const [header, ...rows] = records(afterLines(text.startsWith(BYTE_ORDER_MARK) ? text.slice(1) : text, options.skipLines), DELIMITERS[options.delimiter], options.skipLines + 1).filter((record) => record.fields.length > 1 || record.fields[0]?.trim() !== "");
465
+ if (header === void 0) throw new NativeParseError("csv-table", options.skipLines + 1, "the file has no header row");
466
+ const names = [];
467
+ for (const [position, cell] of header.fields.entries()) {
468
+ const name = columnName(cell);
469
+ if (name === void 0) throw new NativeParseError("csv-table", header.line, `header cell ${String(position + 1)} (${JSON.stringify(cell)}) has no name left once its characters other than letters, digits and underscores are removed`);
470
+ const clash = names.indexOf(name);
471
+ if (clash !== -1) throw new NativeParseError("csv-table", header.line, `header cells ${String(clash + 1)} and ${String(position + 1)} both become the column ${name}`);
472
+ names.push(name);
473
+ }
474
+ const { idColumn, numericColumns } = options;
475
+ for (const wanted of [...idColumn === null ? [] : [idColumn], ...numericColumns.map((entry) => entry.column)]) if (!names.includes(wanted)) throw new NativeParseError("csv-table", header.line, `the recipe names the column ${wanted}, which the header does not have`);
476
+ if (idColumn === null && names.includes("id")) throw new NativeParseError("csv-table", header.line, "the file has a column named id, which would be shadowed by the row number; the recipe names it as its idColumn");
477
+ const factors = new Map(numericColumns.map((entry) => [entry.column, entry.factor]));
478
+ return { nodes: rows.map((record, rowIndex) => {
479
+ if (record.fields.length !== names.length) throw new NativeParseError("csv-table", record.line, `the row has ${String(record.fields.length)} fields, the header has ${String(names.length)}`);
480
+ const cells = new Map(names.map((name, position) => [name, (record.fields[position] ?? "").trim()]));
481
+ const id = idColumn === null ? String(rowIndex + 1) : cells.get(idColumn) ?? "";
482
+ if (id === "") throw new NativeParseError("csv-table", record.line, `the id column ${idColumn ?? ""} is empty`);
483
+ const columns = names.filter((name) => name !== idColumn).map((name) => {
484
+ const cell = cells.get(name) ?? "";
485
+ const factor = factors.get(name);
486
+ return [name, factor === void 0 ? cell === "" ? null : cell : numeric$1(cell, factor, name, record.line)];
487
+ });
488
+ return {
489
+ ...Object.fromEntries(columns),
490
+ id
491
+ };
492
+ }) };
493
+ }
494
+ //#endregion
350
495
  //#region src/parsers/pajek.ts
351
496
  /** Section keywords, lower-cased, mapped to how their lines are read. `*Network` names the network and starts no section. `*Partition` and `*Vector` hold node attributes this version does not carry, so their lines are skipped, as are the `*Vertices` lines inside them. */
352
497
  const sections = /* @__PURE__ */ new Map([
@@ -480,18 +625,12 @@ function decode(bytes, parser) {
480
625
  throw new NativeParseError(parser.format, 1, `the file is not valid utf-8 (${error instanceof Error ? error.message : String(error)}); a recipe for a latin1 file states encoding latin1`);
481
626
  }
482
627
  }
483
- /**
484
- * Reads a file into the native graph shape with the recipe's parser.
485
- * @param bytes - The unpacked file.
486
- * @param parser - The recipe's parser.
487
- * @returns The native graph.
488
- * @throws NativeParseError when the bytes are not valid in the stated encoding or the text is not in the stated format.
489
- */
490
628
  function parseNative(bytes, parser) {
491
629
  const text = decode(bytes, parser);
492
630
  if (parser.format === "edge-list") return parseEdgeList(text, parser);
493
631
  if (parser.format === "matrix-market") return parseMatrixMarket(text);
494
632
  if (parser.format === "gml") return parseGml(text);
633
+ if (parser.format === "csv-table") return parseCsvTable(text, parser);
495
634
  return parsePajek(text);
496
635
  }
497
636
  /** The ustar header layout (POSIX.1-1988, pax(1) "ustar Interchange Format"): each field's byte offset and length. */
@@ -856,8 +995,7 @@ async function runToNative(dataset, options) {
856
995
  return {
857
996
  native: {
858
997
  meta: metaOf(entry),
859
- nodes: parsed.nodes,
860
- edges: parsed.edges
998
+ ...parsed
861
999
  },
862
1000
  provenance: {
863
1001
  entryId: entry.id,
@@ -4,7 +4,7 @@ import { KeepsetError, NAME_SEPARATOR, countTarget, getDataset, listDatasets, li
4
4
  import { fetchDataset, roundTripDataset } from "keepset/node";
5
5
  import { parseArgs } from "node:util";
6
6
  //#region package.json
7
- var version = "0.13.1";
7
+ var version = "0.14.1";
8
8
  const usage = `Usage: keepset <command> [options]
9
9
 
10
10
  Commands:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.13.1",
4
+ "version": "0.14.1",
5
5
  "bin": {
6
6
  "keepset": "./dist-cli/keepset.mjs"
7
7
  },
@@ -28,7 +28,7 @@
28
28
  "@types/react": "^19.3.0",
29
29
  "@types/react-dom": "^19.3.0",
30
30
  "@vanilla-extract/esbuild-plugin": "2.3.22",
31
- "@vitest/coverage-v8": "5.0.3",
31
+ "@vitest/coverage-v8": "5.0.4",
32
32
  "ajv": "8.20.0",
33
33
  "ajv-formats": "3.0.1",
34
34
  "canonicalize": "5.1.0",
@@ -44,7 +44,7 @@
44
44
  "tsx": "4.23.15",
45
45
  "typescript": "6.0.3",
46
46
  "typescript-eslint": "8.71.1",
47
- "vitest": "5.0.3"
47
+ "vitest": "5.0.4"
48
48
  },
49
49
  "engines": {
50
50
  "node": ">=22"
@@ -1 +1 @@
1
- {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"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"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","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.13.1/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/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"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"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":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","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"},{"additionalProperties":false,"properties":{"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"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.14.1/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
@@ -1 +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"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"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.13.1/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
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"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"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.14.1/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}