keepset 0.23.0 → 0.25.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/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 `{ meta, nodes, edges }` with unique node ids and edges whose endpoints name nodes; its entries are graph datasets taken from upstream collections such as SuiteSparse, Netzschleuder, SNAP, KONECT and OGB. `or/v1` is `{ 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 delimited file, a member of an archive, a sheet of an Excel workbook, or an open data portal read a page at a time) and from ArcGIS feature layers by the `geojson` parser. Its entries come from a survey of open operations-research datasets, imported with a curation that records, for each source, what its download gives and how its columns map onto the node; a dataset whose source has no usable table is imported without a recipe and says why. 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 `{ meta, nodes, edges }` with unique node ids and edges whose endpoints name nodes; its entries are graph datasets taken from upstream collections such as SuiteSparse, Netzschleuder, SNAP, KONECT and OGB. `or/v1` is `{ 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 delimited file, a member of an archive, a sheet of an Excel workbook, or an open data portal read a page at a time) and from ArcGIS feature layers by the `geojson` parser. Its entries come from a survey of open operations-research datasets, imported with a curation that records, for each source, what its download gives and how its columns map onto the node; a dataset whose source has no usable table is imported without a recipe and says why. `or-set/v1` is `{ meta, instances }`, a collection of operations-research instances such as a benchmark suite, each instance an `or/v1` document under an id of its own (for a collection read from an archive, the path of the member it came from): a recipe selects every matching archive member with `members`, the parser reads each one, and a member that does not parse fails the fetch and is named. 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
 
@@ -113,14 +113,16 @@ const JsonScalarSchema = z.union([
113
113
  * Reports each node whose id an earlier node already has, as an issue on that node's id.
114
114
  * @param nodes - The nodes of a target document, in order.
115
115
  * @param ctx - The refinement context to report to.
116
+ * @param prefix - The path of the object holding the `nodes` array, for a document that nests nodes inside another.
116
117
  * @returns The set of ids the nodes carry.
117
118
  */
118
- function addDuplicateIdIssues(nodes, ctx) {
119
+ function addDuplicateIdIssues(nodes, ctx, prefix = []) {
119
120
  const ids = /* @__PURE__ */ new Set();
120
121
  for (const [index, node] of nodes.entries()) {
121
122
  if (ids.has(node.id)) ctx.addIssue({
122
123
  code: "custom",
123
124
  path: [
125
+ ...prefix,
124
126
  "nodes",
125
127
  index,
126
128
  "id"
@@ -202,22 +204,65 @@ const OrV1Schema = define(z.object({
202
204
  addDuplicateIdIssues(instance.nodes, ctx);
203
205
  }));
204
206
  //#endregion
207
+ //#region src/targets/or-set-v1.ts
208
+ /** One instance of a set: an or/v1 instance, `{ meta, nodes }`, with the id that names it within the set (for a collection read from an archive, the member's path). */
209
+ const OrInstanceSchema = z.object({
210
+ id: z.string().min(1),
211
+ meta: recordOf(z.string().min(1), JsonScalarSchema),
212
+ nodes: z.array(OrNodeSchema)
213
+ }).strict();
214
+ /**
215
+ * The or-set/v1 normalisation target: `{ meta, instances }`, a collection of operations-research instances such as a benchmark suite, each instance an or/v1 document (`{ meta, nodes }`) under an id of its own. Beyond the shapes it checks that instance ids are unique in the set and node ids unique within each instance. A node id need not be unique across instances, because every instance numbers its own nodes.
216
+ */
217
+ const OrSetV1Schema = define(z.object({
218
+ meta: recordOf(z.string().min(1), JsonScalarSchema),
219
+ instances: z.array(OrInstanceSchema)
220
+ }).strict().superRefine((set, ctx) => {
221
+ const ids = /* @__PURE__ */ new Set();
222
+ for (const [index, instance] of set.instances.entries()) {
223
+ if (ids.has(instance.id)) ctx.addIssue({
224
+ code: "custom",
225
+ path: [
226
+ "instances",
227
+ index,
228
+ "id"
229
+ ],
230
+ message: `duplicate instance id ${JSON.stringify(instance.id)}`
231
+ });
232
+ ids.add(instance.id);
233
+ addDuplicateIdIssues(instance.nodes, ctx, ["instances", index]);
234
+ }
235
+ }));
236
+ //#endregion
205
237
  //#region src/categories.ts
206
238
  /** 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. */
207
- const CategoryIdSchema = z.enum(["graph/v1", "or/v1"]);
239
+ const CategoryIdSchema = z.enum([
240
+ "graph/v1",
241
+ "or/v1",
242
+ "or-set/v1"
243
+ ]);
208
244
  /** 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. */
209
245
  const registry = {
210
246
  "graph/v1": {
211
247
  id: "graph/v1",
212
248
  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.",
249
+ shape: "document",
213
250
  target: GraphV1Schema,
214
251
  counted: ["nodes", "edges"]
215
252
  },
216
253
  "or/v1": {
217
254
  id: "or/v1",
218
255
  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.",
256
+ shape: "document",
219
257
  target: OrV1Schema,
220
258
  counted: ["nodes"]
259
+ },
260
+ "or-set/v1": {
261
+ id: "or-set/v1",
262
+ description: "A collection of operations-research instances as { meta, instances }: a flat record of scalar metadata and instances, each an or/v1 document ({ meta, nodes }) under an id of its own, read from every matching member of an archive.",
263
+ shape: "collection",
264
+ target: OrSetV1Schema,
265
+ counted: ["instances"]
221
266
  }
222
267
  };
223
268
  function getCategory(id) {
@@ -410,22 +455,26 @@ const CitationSchema = z.object({
410
455
  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 '..'" });
411
456
  /** 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 one of a list of extensions (a format can have several, such as Pajek's `.net` and `.paj`). Either must match exactly one member when the recipe runs; macOS resource-fork members (`__MACOSX/` and `._` files) never match. */
412
457
  const ArchiveMemberSchema = z.union([z.object({ path: RelativePathSchema }).strict(), z.object({ extensions: z.array(z.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9.]*$/, { message: "must be a file extension such as .mtx" })).min(1) }).strict()]);
413
- /** How the downloaded bytes unpack into the one file the parser reads. A workbook (`xlsx`) is read as the CSV of its named sheet, with dates as ISO 8601 text, for the `csv-table` parser. */
414
- const ArchiveSchema = z.discriminatedUnion("kind", [
458
+ /** The archive kinds that hold several files, from which a recipe selects one (`member`) or every one a selector matches (`members`). */
459
+ const FileArchiveKindSchema = z.enum([
460
+ "zip",
461
+ "tar",
462
+ "tar.gz"
463
+ ]);
464
+ /** An archive a collection is read from: every file a selector matches, at least one. */
465
+ const CollectionArchiveSchema = z.object({
466
+ kind: FileArchiveKindSchema,
467
+ members: ArchiveMemberSchema
468
+ }).strict();
469
+ /** How the downloaded bytes unpack into the file the parser reads, or for a collection the files it reads one by one. A `members` selector gives every member it matches, at least one, in path order. A workbook (`xlsx`) is read as the CSV of its named sheet, with dates as ISO 8601 text, for the `csv-table` parser. */
470
+ const ArchiveSchema = z.union([
415
471
  z.object({ kind: z.literal("none") }).strict(),
416
472
  z.object({ kind: z.literal("gzip") }).strict(),
417
473
  z.object({
418
- kind: z.literal("zip"),
419
- member: ArchiveMemberSchema
420
- }).strict(),
421
- z.object({
422
- kind: z.literal("tar"),
423
- member: ArchiveMemberSchema
424
- }).strict(),
425
- z.object({
426
- kind: z.literal("tar.gz"),
474
+ kind: FileArchiveKindSchema,
427
475
  member: ArchiveMemberSchema
428
476
  }).strict(),
477
+ CollectionArchiveSchema,
429
478
  z.object({
430
479
  kind: z.literal("xlsx"),
431
480
  sheet: z.string().min(1)
@@ -468,6 +517,18 @@ const ParserSchema = z.discriminatedUnion("format", [
468
517
  format: z.literal("pajek"),
469
518
  encoding: EncodingSchema
470
519
  }).strict(),
520
+ z.object({
521
+ format: z.literal("solomon"),
522
+ encoding: EncodingSchema
523
+ }).strict(),
524
+ z.object({
525
+ format: z.literal("top"),
526
+ encoding: EncodingSchema
527
+ }).strict(),
528
+ z.object({
529
+ format: z.literal("toptw"),
530
+ encoding: EncodingSchema
531
+ }).strict(),
471
532
  z.object({
472
533
  format: z.literal("csv-table"),
473
534
  encoding: EncodingSchema,
@@ -587,6 +648,24 @@ const EntrySchema = define(z.object({
587
648
  path: ["recipe", "source"],
588
649
  message: `names source ${String(entry.recipe.source)} but the entry has ${String(entry.sources.length)}`
589
650
  });
651
+ if (entry.recipe.status === "fetchable") {
652
+ const selectsMembers = "members" in entry.recipe.archive;
653
+ const { shape } = getCategory(entry.category);
654
+ if (shape === "collection" && !selectsMembers) ctx.addIssue({
655
+ code: "custom",
656
+ path: ["recipe", "archive"],
657
+ message: `${entry.category} is a collection, so the archive must select its members with \`members\``
658
+ });
659
+ if (shape === "document" && selectsMembers) ctx.addIssue({
660
+ code: "custom",
661
+ path: [
662
+ "recipe",
663
+ "archive",
664
+ "members"
665
+ ],
666
+ message: `${entry.category} is one document, so the archive selects one file with \`member\``
667
+ });
668
+ }
590
669
  if ("target" in entry.crosswalk && entry.crosswalk.target !== entry.category) ctx.addIssue({
591
670
  code: "custom",
592
671
  path: ["crosswalk", "target"],
@@ -909,7 +988,7 @@ var catalogue_default = {
909
988
  "id": "keepset",
910
989
  "name": "keepset",
911
990
  "description": "The keepset catalogue: datasets with machine-readable recipes that fetch each one and normalise it into its category's target shape.",
912
- "version": "1.11.0",
991
+ "version": "1.13.0",
913
992
  "repository": "https://github.com/Mearman/keepset",
914
993
  "namespaceAliases": {
915
994
  "networkrepository": ["nr"],
@@ -7105,7 +7184,7 @@ var catalogue_default = {
7105
7184
  "duration",
7106
7185
  "travelTime"
7107
7186
  ],
7108
- "category": "or/v1",
7187
+ "category": "or-set/v1",
7109
7188
  "sources": [{
7110
7189
  "url": "https://github.com/psxjpc/MOVRPTW-Generator",
7111
7190
  "retrieved": "2026-10-19",
@@ -7121,12 +7200,95 @@ var catalogue_default = {
7121
7200
  },
7122
7201
  "citations": [],
7123
7202
  "recipe": {
7124
- "status": "not-fetchable",
7125
- "reason": "The download is a collection of Solomon style instance files and distance matrices, and a collection is more than one or/v1 document, so no recipe reads it."
7203
+ "status": "fetchable",
7204
+ "source": 1,
7205
+ "archive": {
7206
+ "kind": "zip",
7207
+ "members": { "extensions": [
7208
+ ".tw0Specs.dat",
7209
+ ".tw1Specs.dat",
7210
+ ".tw2Specs.dat",
7211
+ ".tw3Specs.dat",
7212
+ ".tw4Specs.dat"
7213
+ ] }
7214
+ },
7215
+ "parser": {
7216
+ "format": "solomon",
7217
+ "encoding": "utf-8"
7218
+ }
7126
7219
  },
7127
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
7128
- "expectedCounts": {},
7129
- "properties": { "surveyId": "or-9" }
7220
+ "crosswalk": {
7221
+ "source": "solomon",
7222
+ "target": "or-set/v1",
7223
+ "rules": [
7224
+ {
7225
+ "op": "move",
7226
+ "from": "meta",
7227
+ "to": "meta"
7228
+ },
7229
+ {
7230
+ "op": "move",
7231
+ "from": "members[].id",
7232
+ "to": "instances[].id"
7233
+ },
7234
+ {
7235
+ "op": "move",
7236
+ "from": "members[].meta.name",
7237
+ "to": "instances[].meta.name"
7238
+ },
7239
+ {
7240
+ "op": "move",
7241
+ "from": "members[].meta.vehicles",
7242
+ "to": "instances[].meta.vehicles"
7243
+ },
7244
+ {
7245
+ "op": "move",
7246
+ "from": "members[].meta.capacity",
7247
+ "to": "instances[].meta.capacity"
7248
+ },
7249
+ {
7250
+ "op": "move",
7251
+ "from": "members[].nodes[].id",
7252
+ "to": "instances[].nodes[].id"
7253
+ },
7254
+ {
7255
+ "op": "move",
7256
+ "from": "members[].nodes[].due",
7257
+ "to": "instances[].nodes[].deadline"
7258
+ },
7259
+ {
7260
+ "op": "move",
7261
+ "from": "members[].nodes[].service",
7262
+ "to": "instances[].nodes[].duration"
7263
+ },
7264
+ {
7265
+ "op": "move",
7266
+ "from": "members[].nodes[].x",
7267
+ "to": "instances[].nodes[].lat"
7268
+ },
7269
+ {
7270
+ "op": "move",
7271
+ "from": "members[].nodes[].y",
7272
+ "to": "instances[].nodes[].lon"
7273
+ },
7274
+ {
7275
+ "op": "move",
7276
+ "from": "members[].nodes[].demand",
7277
+ "to": "instances[].nodes[].demand"
7278
+ },
7279
+ {
7280
+ "op": "move",
7281
+ "from": "members[].nodes[].ready",
7282
+ "to": "instances[].nodes[].ready"
7283
+ }
7284
+ ],
7285
+ "oneWay": []
7286
+ },
7287
+ "expectedCounts": { "instances": 45 },
7288
+ "properties": {
7289
+ "surveyId": "or-9",
7290
+ "timeUnit": "seconds"
7291
+ }
7130
7292
  },
7131
7293
  {
7132
7294
  "id": "github-zhangchenhaoseu-jd-cold-chain-nodes",
@@ -8481,7 +8643,7 @@ var catalogue_default = {
8481
8643
  "location",
8482
8644
  "travelTime"
8483
8645
  ],
8484
- "category": "or/v1",
8646
+ "category": "or-set/v1",
8485
8647
  "sources": [{
8486
8648
  "url": "https://www.mech.kuleuven.be/en/mim/op",
8487
8649
  "retrieved": "2026-10-19",
@@ -8494,11 +8656,75 @@ var catalogue_default = {
8494
8656
  "licence": { "kind": "unrecorded" },
8495
8657
  "citations": [],
8496
8658
  "recipe": {
8497
- "status": "not-fetchable",
8498
- "reason": "The download is a collection of 60 text instance files of one 100 node point set under different budgets (planar coordinates and a prize, with no time quantities), and a collection is more than one or/v1 document, so no recipe reads it."
8659
+ "status": "fetchable",
8660
+ "source": 1,
8661
+ "archive": {
8662
+ "kind": "zip",
8663
+ "members": { "extensions": [".txt"] }
8664
+ },
8665
+ "parser": {
8666
+ "format": "top",
8667
+ "encoding": "utf-8"
8668
+ }
8499
8669
  },
8500
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8501
- "expectedCounts": {},
8670
+ "crosswalk": {
8671
+ "source": "top",
8672
+ "target": "or-set/v1",
8673
+ "rules": [
8674
+ {
8675
+ "op": "move",
8676
+ "from": "meta",
8677
+ "to": "meta"
8678
+ },
8679
+ {
8680
+ "op": "move",
8681
+ "from": "members[].id",
8682
+ "to": "instances[].id"
8683
+ },
8684
+ {
8685
+ "op": "move",
8686
+ "from": "members[].meta.points",
8687
+ "to": "instances[].meta.points"
8688
+ },
8689
+ {
8690
+ "op": "move",
8691
+ "from": "members[].meta.paths",
8692
+ "to": "instances[].meta.paths"
8693
+ },
8694
+ {
8695
+ "op": "move",
8696
+ "from": "members[].meta.budget",
8697
+ "to": "instances[].meta.budget"
8698
+ },
8699
+ {
8700
+ "op": "move",
8701
+ "from": "members[].nodes[].id",
8702
+ "to": "instances[].nodes[].id"
8703
+ },
8704
+ {
8705
+ "op": "move",
8706
+ "from": "members[].nodes[].score",
8707
+ "to": "instances[].nodes[].prize"
8708
+ },
8709
+ {
8710
+ "op": "move",
8711
+ "from": "members[].nodes[].x",
8712
+ "to": "instances[].nodes[].x"
8713
+ },
8714
+ {
8715
+ "op": "move",
8716
+ "from": "members[].nodes[].y",
8717
+ "to": "instances[].nodes[].y"
8718
+ },
8719
+ {
8720
+ "op": "move",
8721
+ "from": "members[].nodes[].role",
8722
+ "to": "instances[].nodes[].role"
8723
+ }
8724
+ ],
8725
+ "oneWay": []
8726
+ },
8727
+ "expectedCounts": { "instances": 60 },
8502
8728
  "properties": { "surveyId": "or-0" }
8503
8729
  },
8504
8730
  {
@@ -8515,7 +8741,7 @@ var catalogue_default = {
8515
8741
  "duration",
8516
8742
  "travelTime"
8517
8743
  ],
8518
- "category": "or/v1",
8744
+ "category": "or-set/v1",
8519
8745
  "sources": [{
8520
8746
  "url": "https://www.mech.kuleuven.be/en/mim/op",
8521
8747
  "retrieved": "2026-10-19",
@@ -8528,12 +8754,89 @@ var catalogue_default = {
8528
8754
  "licence": { "kind": "unrecorded" },
8529
8755
  "citations": [],
8530
8756
  "recipe": {
8531
- "status": "not-fetchable",
8532
- "reason": "The download is a collection of Solomon style time window instance files (planar coordinates, service durations, prizes and windows in abstract units, not seconds), and a collection is more than one or/v1 document, so no recipe reads it."
8757
+ "status": "fetchable",
8758
+ "source": 1,
8759
+ "archive": {
8760
+ "kind": "zip",
8761
+ "members": { "extensions": [".txt"] }
8762
+ },
8763
+ "parser": {
8764
+ "format": "toptw",
8765
+ "encoding": "utf-8"
8766
+ }
8533
8767
  },
8534
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8535
- "expectedCounts": {},
8536
- "properties": { "surveyId": "or-1" }
8768
+ "crosswalk": {
8769
+ "source": "toptw",
8770
+ "target": "or-set/v1",
8771
+ "rules": [
8772
+ {
8773
+ "op": "move",
8774
+ "from": "meta",
8775
+ "to": "meta"
8776
+ },
8777
+ {
8778
+ "op": "move",
8779
+ "from": "members[].id",
8780
+ "to": "instances[].id"
8781
+ },
8782
+ {
8783
+ "op": "move",
8784
+ "from": "members[].meta.paths",
8785
+ "to": "instances[].meta.paths"
8786
+ },
8787
+ {
8788
+ "op": "move",
8789
+ "from": "members[].meta.customers",
8790
+ "to": "instances[].meta.customers"
8791
+ },
8792
+ {
8793
+ "op": "move",
8794
+ "from": "members[].meta.capacity",
8795
+ "to": "instances[].meta.capacity"
8796
+ },
8797
+ {
8798
+ "op": "move",
8799
+ "from": "members[].nodes[].id",
8800
+ "to": "instances[].nodes[].id"
8801
+ },
8802
+ {
8803
+ "op": "move",
8804
+ "from": "members[].nodes[].close",
8805
+ "to": "instances[].nodes[].deadline"
8806
+ },
8807
+ {
8808
+ "op": "move",
8809
+ "from": "members[].nodes[].profit",
8810
+ "to": "instances[].nodes[].prize"
8811
+ },
8812
+ {
8813
+ "op": "move",
8814
+ "from": "members[].nodes[].x",
8815
+ "to": "instances[].nodes[].x"
8816
+ },
8817
+ {
8818
+ "op": "move",
8819
+ "from": "members[].nodes[].y",
8820
+ "to": "instances[].nodes[].y"
8821
+ },
8822
+ {
8823
+ "op": "move",
8824
+ "from": "members[].nodes[].service",
8825
+ "to": "instances[].nodes[].service"
8826
+ },
8827
+ {
8828
+ "op": "move",
8829
+ "from": "members[].nodes[].open",
8830
+ "to": "instances[].nodes[].open"
8831
+ }
8832
+ ],
8833
+ "oneWay": []
8834
+ },
8835
+ "expectedCounts": { "instances": 29 },
8836
+ "properties": {
8837
+ "surveyId": "or-1",
8838
+ "timeUnit": "abstract units"
8839
+ }
8537
8840
  },
8538
8841
  {
8539
8842
  "id": "lastfm-hgb",
@@ -8586,21 +8889,102 @@ var catalogue_default = {
8586
8889
  "duration",
8587
8890
  "travelTime"
8588
8891
  ],
8589
- "category": "or/v1",
8892
+ "category": "or-set/v1",
8590
8893
  "sources": [{
8591
8894
  "url": "https://neo.lcc.uma.es/vrp/vrp-instances/capacitated-vrp-with-time-windows-instances/",
8592
8895
  "retrieved": "2026-10-19",
8593
8896
  "status": "live"
8897
+ }, {
8898
+ "url": "https://neo.lcc.uma.es/vrp/wp-content/data/instances/solomon/solomon_100.zip",
8899
+ "retrieved": "2026-10-19",
8900
+ "status": "live"
8594
8901
  }],
8595
8902
  "licence": { "kind": "unrecorded" },
8596
8903
  "citations": [],
8597
8904
  "recipe": {
8598
- "status": "not-fetchable",
8599
- "reason": "The download is a collection of Solomon instance files (planar coordinates, demands, service times and windows in abstract units, not seconds), and a collection is more than one or/v1 document, so no recipe reads it."
8905
+ "status": "fetchable",
8906
+ "source": 1,
8907
+ "archive": {
8908
+ "kind": "zip",
8909
+ "members": { "extensions": [".txt"] }
8910
+ },
8911
+ "parser": {
8912
+ "format": "solomon",
8913
+ "encoding": "utf-8"
8914
+ }
8600
8915
  },
8601
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8602
- "expectedCounts": {},
8603
- "properties": { "surveyId": "or-5" }
8916
+ "crosswalk": {
8917
+ "source": "solomon",
8918
+ "target": "or-set/v1",
8919
+ "rules": [
8920
+ {
8921
+ "op": "move",
8922
+ "from": "meta",
8923
+ "to": "meta"
8924
+ },
8925
+ {
8926
+ "op": "move",
8927
+ "from": "members[].id",
8928
+ "to": "instances[].id"
8929
+ },
8930
+ {
8931
+ "op": "move",
8932
+ "from": "members[].meta.name",
8933
+ "to": "instances[].meta.name"
8934
+ },
8935
+ {
8936
+ "op": "move",
8937
+ "from": "members[].meta.vehicles",
8938
+ "to": "instances[].meta.vehicles"
8939
+ },
8940
+ {
8941
+ "op": "move",
8942
+ "from": "members[].meta.capacity",
8943
+ "to": "instances[].meta.capacity"
8944
+ },
8945
+ {
8946
+ "op": "move",
8947
+ "from": "members[].nodes[].id",
8948
+ "to": "instances[].nodes[].id"
8949
+ },
8950
+ {
8951
+ "op": "move",
8952
+ "from": "members[].nodes[].due",
8953
+ "to": "instances[].nodes[].deadline"
8954
+ },
8955
+ {
8956
+ "op": "move",
8957
+ "from": "members[].nodes[].x",
8958
+ "to": "instances[].nodes[].x"
8959
+ },
8960
+ {
8961
+ "op": "move",
8962
+ "from": "members[].nodes[].y",
8963
+ "to": "instances[].nodes[].y"
8964
+ },
8965
+ {
8966
+ "op": "move",
8967
+ "from": "members[].nodes[].demand",
8968
+ "to": "instances[].nodes[].demand"
8969
+ },
8970
+ {
8971
+ "op": "move",
8972
+ "from": "members[].nodes[].ready",
8973
+ "to": "instances[].nodes[].ready"
8974
+ },
8975
+ {
8976
+ "op": "move",
8977
+ "from": "members[].nodes[].service",
8978
+ "to": "instances[].nodes[].service"
8979
+ }
8980
+ ],
8981
+ "oneWay": []
8982
+ },
8983
+ "expectedCounts": { "instances": 56 },
8984
+ "properties": {
8985
+ "surveyId": "or-5",
8986
+ "timeUnit": "abstract units"
8987
+ }
8604
8988
  },
8605
8989
  {
8606
8990
  "id": "leeds-fixed-penalty-notices",