keepset 0.4.0 → 0.5.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.
@@ -46189,4 +46189,4 @@ function getManifest() {
46189
46189
  return catalogue().manifest;
46190
46190
  }
46191
46191
  //#endregion
46192
- export { getCategory as A, DatasetNotFoundError as B, SnapshotSchema as C, waybackUrl as D, parseEntry as E, GraphNodeSchema as F, NativeParseError as H, GraphV1Schema as I, JsonScalarSchema as L, kindOf as M, listCategories as N, CategoryIdSchema as O, GraphEdgeSchema as P, ArchiveError as R, RelativePathSchema as S, WaybackTimestampSchema as T, RecipeError as U, KeepsetError as V, UnknownCategoryError as W, IdSchema as _, createCatalogue as a, RESERVED_META_KEYS as b, ManifestSchema as c, ArchiveMemberSchema as d, ArchiveSchema as f, HttpUrlSchema as g, EntrySchema as h, CatalogueIndexSchema as i, getTarget as j, countTarget as k, SemVerSchema as l, CrosswalkReferenceSchema as m, getManifest as n, getOwn as o, CitationSchema as p, listDatasets as r, LineageSchema as s, getDataset as t, parseManifest as u, LicenceSchema as v, SourceSchema as w, RecipeSchema as x, ParserSchema as y, CatalogueSyntaxError as z };
46192
+ export { countTarget as A, CatalogueSyntaxError as B, SnapshotSchema as C, toIssues as D, parseEntry as E, GraphEdgeSchema as F, UnknownCategoryError as G, KeepsetError as H, GraphNodeSchema as I, GraphV1Schema as L, getTarget as M, kindOf as N, waybackUrl as O, listCategories as P, JsonScalarSchema as R, RelativePathSchema as S, WaybackTimestampSchema as T, NativeParseError as U, DatasetNotFoundError as V, RecipeError as W, IdSchema as _, createCatalogue as a, RESERVED_META_KEYS as b, ManifestSchema as c, ArchiveMemberSchema as d, ArchiveSchema as f, HttpUrlSchema as g, EntrySchema as h, CatalogueIndexSchema as i, getCategory as j, CategoryIdSchema as k, SemVerSchema as l, CrosswalkReferenceSchema as m, getManifest as n, getOwn as o, CitationSchema as p, listDatasets as r, LineageSchema as s, getDataset as t, parseManifest as u, LicenceSchema as v, SourceSchema as w, RecipeSchema as x, ParserSchema as y, ArchiveError as z };
package/dist/index.d.ts CHANGED
@@ -69,4 +69,12 @@ export declare function getManifest(): Manifest;
69
69
  */
70
70
  export declare function serialise(doc: Entry | Manifest | Crosswalk): string;
71
71
  //#endregion
72
+ //#region src/validate.d.ts
73
+ /**
74
+ * 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.
75
+ * @param dataset - The dataset.
76
+ * @returns Every issue found, with paths from the dataset's root (`entry.` or `crosswalk.`); empty when the dataset is valid.
77
+ */
78
+ export declare function validateDataset(dataset: Dataset): SyntaxIssue[];
79
+ //#endregion
72
80
  export { 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, 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 Parser, ParserSchema, RESERVED_META_KEYS, type Recipe, RecipeSchema, type RecipeStep, RelativePathSchema, 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,5 @@
1
- import { A as getCategory, B as DatasetNotFoundError, C as SnapshotSchema, D as waybackUrl, E as parseEntry, F as GraphNodeSchema, H as NativeParseError, I as GraphV1Schema, L as JsonScalarSchema, M as kindOf, N as listCategories, O as CategoryIdSchema, P as GraphEdgeSchema, R as ArchiveError, S as RelativePathSchema, T as WaybackTimestampSchema, U as RecipeError, V as KeepsetError, W as UnknownCategoryError, _ as IdSchema, a as createCatalogue, b as RESERVED_META_KEYS, c as ManifestSchema, d as ArchiveMemberSchema, f as ArchiveSchema, g as HttpUrlSchema, h as EntrySchema, i as CatalogueIndexSchema, j as getTarget, k as countTarget, l as SemVerSchema, m as CrosswalkReferenceSchema, n as getManifest, p as CitationSchema, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as LicenceSchema, w as SourceSchema, x as RecipeSchema, y as ParserSchema, z as CatalogueSyntaxError } from "./bundled-DN3sSHfu.js";
1
+ import { A as countTarget, B as CatalogueSyntaxError, C as SnapshotSchema, D as toIssues, E as parseEntry, F as GraphEdgeSchema, G as UnknownCategoryError, H as KeepsetError, I as GraphNodeSchema, L as GraphV1Schema, M as getTarget, N as kindOf, O as waybackUrl, P as listCategories, R as JsonScalarSchema, S as RelativePathSchema, T as WaybackTimestampSchema, U as NativeParseError, V as DatasetNotFoundError, W as RecipeError, _ as IdSchema, a as createCatalogue, b as RESERVED_META_KEYS, c as ManifestSchema, d as ArchiveMemberSchema, f as ArchiveSchema, g as HttpUrlSchema, h as EntrySchema, i as CatalogueIndexSchema, j as getCategory, k as CategoryIdSchema, l as SemVerSchema, m as CrosswalkReferenceSchema, n as getManifest, p as CitationSchema, r as listDatasets, s as LineageSchema, t as getDataset, u as parseManifest, v as LicenceSchema, w as SourceSchema, x as RecipeSchema, y as ParserSchema, z as ArchiveError } from "./bundled-P_132GDn.js";
2
+ import { validateCrosswalk } from "twindex";
2
3
  //#region src/serialise.ts
3
4
  /**
4
5
  * Copies a JSON value with every object's keys in ascending code-unit order, the order the house JSON lint (`json/sort-keys`) requires of committed JSON. `Object.fromEntries` defines each key as an own data property, so a key such as `__proto__` stays a key.
@@ -27,4 +28,33 @@ function serialise(doc) {
27
28
  return canonicalJson(doc);
28
29
  }
29
30
  //#endregion
30
- export { ArchiveError, ArchiveMemberSchema, ArchiveSchema, CatalogueIndexSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, DatasetNotFoundError, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, HttpUrlSchema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, NativeParseError, ParserSchema, RESERVED_META_KEYS, RecipeError, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getDataset, getManifest, getTarget, kindOf, listCategories, listDatasets, parseEntry, parseManifest, serialise, waybackUrl };
31
+ //#region src/validate.ts
32
+ /**
33
+ * 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.
34
+ * @param dataset - The dataset.
35
+ * @returns Every issue found, with paths from the dataset's root (`entry.` or `crosswalk.`); empty when the dataset is valid.
36
+ */
37
+ function validateDataset(dataset) {
38
+ const issues = [];
39
+ const entry = EntrySchema.safeParse(dataset.entry);
40
+ if (!entry.success) issues.push(...toIssues(entry.error).map((issue) => ({
41
+ path: `entry.${issue.path}`,
42
+ message: issue.message
43
+ })));
44
+ const { rules, oneWay } = dataset.crosswalk;
45
+ for (const issue of validateCrosswalk(dataset.crosswalk)) {
46
+ const { ruleIndex } = issue;
47
+ const path = ruleIndex >= 0 && ruleIndex < rules.length ? `crosswalk.rules.${String(ruleIndex)}` : ruleIndex >= rules.length && ruleIndex < rules.length + oneWay.length ? `crosswalk.oneWay.${String(ruleIndex - rules.length)}` : "crosswalk";
48
+ issues.push({
49
+ path,
50
+ message: issue.message
51
+ });
52
+ }
53
+ if (dataset.crosswalk.target !== dataset.entry.category) issues.push({
54
+ path: "crosswalk.target",
55
+ message: `must be the entry's category, ${dataset.entry.category}`
56
+ });
57
+ return issues;
58
+ }
59
+ //#endregion
60
+ export { ArchiveError, ArchiveMemberSchema, ArchiveSchema, CatalogueIndexSchema, CatalogueSyntaxError, CategoryIdSchema, CitationSchema, CrosswalkReferenceSchema, DatasetNotFoundError, EntrySchema, GraphEdgeSchema, GraphNodeSchema, GraphV1Schema, HttpUrlSchema, IdSchema, JsonScalarSchema, KeepsetError, LicenceSchema, LineageSchema, ManifestSchema, NativeParseError, ParserSchema, RESERVED_META_KEYS, RecipeError, RecipeSchema, RelativePathSchema, SemVerSchema, SnapshotSchema, SourceSchema, UnknownCategoryError, WaybackTimestampSchema, countTarget, createCatalogue, getCategory, getDataset, getManifest, getTarget, kindOf, listCategories, listDatasets, parseEntry, parseManifest, serialise, validateDataset, waybackUrl };
@@ -55,4 +55,30 @@ export declare function fetchEntry(dataset: Dataset, options?: FetchOptions): Pr
55
55
  * @throws DatasetNotFoundError when no entry has that id, and RecipeError naming the entry and the failing step when the recipe fails.
56
56
  */
57
57
  export declare function fetchDataset(id: string, options?: FetchOptions): Promise<FetchedDataset>;
58
+ /** Whether decoding a dataset's encoded native document gives the native document back, with the provenance of the bytes it was checked on. */
59
+ export type RoundTripReport = {
60
+ readonly holds: true;
61
+ readonly provenance: Provenance;
62
+ } | {
63
+ readonly holds: false;
64
+ readonly path: string;
65
+ readonly message: string;
66
+ readonly provenance: Provenance;
67
+ };
68
+ /**
69
+ * Checks a dataset's crosswalk on its real data: runs the recipe to the native document, then twindex's `assertRoundTrip`, which encodes it, decodes the result and compares the two over every path the crosswalk promises to preserve.
70
+ * @param dataset - A dataset from any catalogue.
71
+ * @param options - The network port, abort signal and clock.
72
+ * @returns Whether the round trip holds, and where it first differs when it does not.
73
+ * @throws RecipeError naming the entry and the failing step when the recipe fails before the comparison, or when the crosswalk cannot run.
74
+ */
75
+ export declare function roundTripEntry(dataset: Dataset, options?: FetchOptions): Promise<RoundTripReport>;
76
+ /**
77
+ * Runs `roundTripEntry` on a dataset of the bundled catalogue by id.
78
+ * @param id - The entry id.
79
+ * @param options - The network port, abort signal and clock.
80
+ * @returns Whether the round trip holds.
81
+ * @throws DatasetNotFoundError when no entry has that id, and RecipeError as `roundTripEntry` does.
82
+ */
83
+ export declare function roundTripDataset(id: string, options?: FetchOptions): Promise<RoundTripReport>;
58
84
  //#endregion
@@ -1,5 +1,5 @@
1
- import { H as NativeParseError, R as ArchiveError, U as RecipeError, V as KeepsetError, j as getTarget, k as countTarget, o as getOwn, t as getDataset } from "../bundled-DN3sSHfu.js";
2
- import { encode } from "twindex";
1
+ import { A as countTarget, H as KeepsetError, M as getTarget, U as NativeParseError, W as RecipeError, o as getOwn, t as getDataset, z as ArchiveError } from "../bundled-P_132GDn.js";
2
+ import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
3
3
  import { createHash } from "node:crypto";
4
4
  import { gunzipSync } from "node:zlib";
5
5
  import { unzipSync } from "fflate";
@@ -832,14 +832,13 @@ function metaOf(entry) {
832
832
  };
833
833
  }
834
834
  /**
835
- * Runs a dataset's recipe: downloads the source (from its pinned snapshot when the source is dead), unpacks the archive, parses the native shape, applies the twindex crosswalk and validates the result against the category target. The native document the crosswalk receives is the parser's output with `meta` built from the entry.
836
- * @param dataset - A dataset from any catalogue.
835
+ * Runs a recipe's download, extract and parse steps, and builds the native document the crosswalk receives: the parser's output with `meta` built from the entry.
836
+ * @param dataset - The dataset.
837
837
  * @param options - The network port, abort signal and clock.
838
- * @returns The resolved target with its provenance and any count warnings.
839
- * @throws RecipeError naming the entry and the failing step; nothing partial or default-shaped is ever returned.
838
+ * @returns The native document and the provenance of its bytes.
840
839
  */
841
- async function fetchEntry(dataset, options = {}) {
842
- const { entry, crosswalk } = dataset;
840
+ async function runToNative(dataset, options) {
841
+ const { entry } = dataset;
843
842
  const { fetch: fetchPort = downloadRaw, signal, now = () => /* @__PURE__ */ new Date() } = options;
844
843
  const { recipe } = entry;
845
844
  if (recipe.status === "not-fetchable") throw new RecipeError(entry.id, "recipe", `the entry has no fetchable recipe: ${recipe.reason}`);
@@ -853,22 +852,13 @@ async function fetchEntry(dataset, options = {}) {
853
852
  return new Uint8Array(await response.arrayBuffer());
854
853
  });
855
854
  const unpacked = await step(entry.id, "extract", () => unpack(bytes, recipe.archive));
856
- const native = await step(entry.id, "parse", () => parseNative(unpacked.bytes, recipe.parser));
857
- const encoded = await step(entry.id, "crosswalk", () => encode(crosswalk, {
858
- meta: metaOf(entry),
859
- nodes: native.nodes,
860
- edges: native.edges
861
- }));
862
- const data = await step(entry.id, "validate", () => {
863
- const result = getTarget(entry.category).safeParse(encoded);
864
- if (!result.success) throw new KeepsetError(`the result is not ${entry.category}: ${result.error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
865
- return result.data;
866
- });
867
- const counts = countTarget(entry.category, data);
868
- const warnings = Object.entries(entry.expectedCounts).filter(([key, expected]) => counts.get(key) !== expected).map(([key, expected]) => `${key}: the entry records ${String(expected)}, the fetched result has ${String(counts.get(key))}`);
855
+ const parsed = await step(entry.id, "parse", () => parseNative(unpacked.bytes, recipe.parser));
869
856
  return {
870
- dataset,
871
- data,
857
+ native: {
858
+ meta: metaOf(entry),
859
+ nodes: parsed.nodes,
860
+ edges: parsed.edges
861
+ },
872
862
  provenance: {
873
863
  entryId: entry.id,
874
864
  catalogue: dataset.catalogue,
@@ -883,8 +873,31 @@ async function fetchEntry(dataset, options = {}) {
883
873
  sha256: createHash("sha256").update(bytes).digest("hex"),
884
874
  byteLength: bytes.byteLength,
885
875
  ...unpacked.member === void 0 ? {} : { member: unpacked.member }
886
- },
887
- warnings
876
+ }
877
+ };
878
+ }
879
+ /**
880
+ * Runs a dataset's recipe: downloads the source (from its pinned snapshot when the source is dead), unpacks the archive, parses the native shape, applies the twindex crosswalk and validates the result against the category target. The native document the crosswalk receives is the parser's output with `meta` built from the entry.
881
+ * @param dataset - A dataset from any catalogue.
882
+ * @param options - The network port, abort signal and clock.
883
+ * @returns The resolved target with its provenance and any count warnings.
884
+ * @throws RecipeError naming the entry and the failing step; nothing partial or default-shaped is ever returned.
885
+ */
886
+ async function fetchEntry(dataset, options = {}) {
887
+ const { entry, crosswalk } = dataset;
888
+ const { native, provenance } = await runToNative(dataset, options);
889
+ const encoded = await step(entry.id, "crosswalk", () => encode(crosswalk, native));
890
+ const data = await step(entry.id, "validate", () => {
891
+ const result = getTarget(entry.category).safeParse(encoded);
892
+ if (!result.success) throw new KeepsetError(`the result is not ${entry.category}: ${result.error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
893
+ return result.data;
894
+ });
895
+ const counts = countTarget(entry.category, data);
896
+ return {
897
+ dataset,
898
+ data,
899
+ provenance,
900
+ warnings: Object.entries(entry.expectedCounts).filter(([key, expected]) => counts.get(key) !== expected).map(([key, expected]) => `${key}: the entry records ${String(expected)}, the fetched result has ${String(counts.get(key))}`)
888
901
  };
889
902
  }
890
903
  /**
@@ -897,5 +910,40 @@ async function fetchEntry(dataset, options = {}) {
897
910
  async function fetchDataset(id, options = {}) {
898
911
  return fetchEntry(getDataset(id), options);
899
912
  }
913
+ /**
914
+ * Checks a dataset's crosswalk on its real data: runs the recipe to the native document, then twindex's `assertRoundTrip`, which encodes it, decodes the result and compares the two over every path the crosswalk promises to preserve.
915
+ * @param dataset - A dataset from any catalogue.
916
+ * @param options - The network port, abort signal and clock.
917
+ * @returns Whether the round trip holds, and where it first differs when it does not.
918
+ * @throws RecipeError naming the entry and the failing step when the recipe fails before the comparison, or when the crosswalk cannot run.
919
+ */
920
+ async function roundTripEntry(dataset, options = {}) {
921
+ const { native, provenance } = await runToNative(dataset, options);
922
+ try {
923
+ assertRoundTrip(dataset.crosswalk, [native]);
924
+ } catch (error) {
925
+ if (error instanceof RoundTripMismatchError) return {
926
+ holds: false,
927
+ path: error.path,
928
+ message: error.message,
929
+ provenance
930
+ };
931
+ throw new RecipeError(dataset.entry.id, "crosswalk", error instanceof Error ? error.message : String(error), { cause: error });
932
+ }
933
+ return {
934
+ holds: true,
935
+ provenance
936
+ };
937
+ }
938
+ /**
939
+ * Runs `roundTripEntry` on a dataset of the bundled catalogue by id.
940
+ * @param id - The entry id.
941
+ * @param options - The network port, abort signal and clock.
942
+ * @returns Whether the round trip holds.
943
+ * @throws DatasetNotFoundError when no entry has that id, and RecipeError as `roundTripEntry` does.
944
+ */
945
+ async function roundTripDataset(id, options = {}) {
946
+ return roundTripEntry(getDataset(id), options);
947
+ }
900
948
  //#endregion
901
- export { downloadUrl, fetchDataset, fetchEntry };
949
+ export { downloadUrl, fetchDataset, fetchEntry, roundTripDataset, roundTripEntry };
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from "node:fs";
3
+ import { KeepsetError, countTarget, getDataset, listDatasets, validateDataset } from "keepset";
4
+ import { fetchDataset, roundTripDataset } from "keepset/node";
5
+ import { parseArgs } from "node:util";
6
+ //#region package.json
7
+ var version = "0.5.0";
8
+ const usage = `Usage: keepset <command> [options]
9
+
10
+ Commands:
11
+ search [text] [--kind <kind>] [--domain <domain>] [--category <id>] [--json]
12
+ list the datasets whose id, name, description, domain or
13
+ tags contain the text, filtered by kind, domain or category
14
+ show <id> [--json] print a dataset's metadata, recipe and crosswalk
15
+ fetch <id> [--out <file>]
16
+ run the dataset's recipe and write the normalised result
17
+ to <file> (default <id>.json), with its provenance beside
18
+ it in <file without .json>.provenance.json
19
+ validate <id> check the entry against the entry schema and its
20
+ crosswalk against twindex's validation
21
+ roundtrip <id> fetch the dataset and check that decoding the encoded
22
+ native document gives it back
23
+
24
+ Options:
25
+ --help, -h print this help
26
+ --version, -v print the version
27
+
28
+ Exit status: 0 for a result, a valid entry or a round trip that holds; 1 for an
29
+ invalid entry or a round trip that does not hold; 2 when there is no answer:
30
+ bad usage, an unknown dataset or a recipe that failed.
31
+ `;
32
+ function parseCommandLine(args) {
33
+ return parseArgs({
34
+ args: [...args],
35
+ allowPositionals: true,
36
+ strict: true,
37
+ options: {
38
+ kind: { type: "string" },
39
+ domain: { type: "string" },
40
+ category: { type: "string" },
41
+ json: { type: "boolean" },
42
+ out: { type: "string" },
43
+ help: {
44
+ type: "boolean",
45
+ short: "h"
46
+ },
47
+ version: {
48
+ type: "boolean",
49
+ short: "v"
50
+ }
51
+ }
52
+ });
53
+ }
54
+ /**
55
+ * JSON as the command prints it: two-space indented, with a trailing newline.
56
+ * @param value - The value.
57
+ * @returns The text.
58
+ */
59
+ function json(value) {
60
+ return `${JSON.stringify(value, void 0, 2)}\n`;
61
+ }
62
+ /**
63
+ * One line per dataset for `search`: id, name, category and whether its recipe can run, tab-separated so the output pipes into other tools.
64
+ * @param datasets - The datasets.
65
+ * @returns The text.
66
+ */
67
+ function formatSearch(datasets) {
68
+ return datasets.map(({ entry }) => `${entry.id}\t${entry.name}\t${entry.category}\t${entry.recipe.status}\n`).join("");
69
+ }
70
+ /**
71
+ * The human-readable form of `show`: the descriptor, sources, licence, citations, recipe and crosswalk rules, one labelled section after another.
72
+ * @param dataset - The dataset.
73
+ * @returns The text.
74
+ */
75
+ function formatShow(dataset) {
76
+ const { entry, crosswalk } = dataset;
77
+ const lines = [`${entry.name} (${entry.id})`];
78
+ if (entry.description !== void 0) lines.push("", entry.description);
79
+ lines.push("", `category: ${entry.category} (kind ${dataset.kind})`, `catalogue: ${dataset.catalogue.id} ${dataset.catalogue.version}`);
80
+ if (entry.domain !== void 0) lines.push(`domain: ${entry.domain}`);
81
+ if (entry.tags.length > 0) lines.push(`tags: ${entry.tags.join(", ")}`);
82
+ const { licence } = entry;
83
+ lines.push(`licence: ${licence.kind === "spdx" ? licence.id : licence.kind === "custom" ? `${licence.name} <${licence.url}>` : "not recorded by the source"}`);
84
+ lines.push("", "sources:");
85
+ for (const [index, source] of entry.sources.entries()) {
86
+ const snapshot = source.snapshot === void 0 ? "" : `, snapshot ${source.snapshot.timestamp}`;
87
+ lines.push(` [${String(index)}] ${source.url} (${source.status}, retrieved ${source.retrieved}${snapshot})`);
88
+ }
89
+ if (entry.citations.length > 0) {
90
+ lines.push("", "citations:");
91
+ for (const citation of entry.citations) {
92
+ const link = citation.doi === void 0 ? citation.url === void 0 ? "" : ` <${citation.url}>` : ` doi:${citation.doi}`;
93
+ lines.push(` ${citation.text}${link}`);
94
+ }
95
+ }
96
+ const { recipe } = entry;
97
+ lines.push("", "recipe:");
98
+ if (recipe.status === "not-fetchable") lines.push(` not fetchable: ${recipe.reason}`);
99
+ else {
100
+ const { member } = "member" in recipe.archive ? recipe.archive : { member: void 0 };
101
+ const memberText = member === void 0 ? "" : "path" in member ? `, member ${member.path}` : `, member ending ${member.extensions.join(" or ")}`;
102
+ const { format, ...options } = recipe.parser;
103
+ lines.push(` source: [${String(recipe.source)}]`, ` archive: ${recipe.archive.kind}${memberText}`, ` parser: ${format} ${JSON.stringify(options)}`);
104
+ }
105
+ lines.push("", `crosswalk: ${crosswalk.source} -> ${crosswalk.target}`);
106
+ for (const rule of crosswalk.rules) lines.push(` ${rule.op} ${rule.from} -> ${rule.to}`);
107
+ for (const rule of crosswalk.oneWay) lines.push(rule.op === "drop" ? ` drop ${rule.from} (encode)` : ` default ${rule.to} = ${JSON.stringify(rule.value)} (${rule.direction})`);
108
+ const counts = Object.entries(entry.expectedCounts);
109
+ if (counts.length > 0) lines.push("", `expected counts: ${counts.map(([key, count]) => `${key} ${String(count)}`).join(", ")}`);
110
+ const properties = Object.entries(entry.properties);
111
+ if (properties.length > 0) lines.push("", "properties:", ...properties.map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`));
112
+ return `${lines.join("\n")}\n`;
113
+ }
114
+ /**
115
+ * Where `fetch` writes provenance: beside the result, `.json` replaced by `.provenance.json` (or the suffix appended to a path without one).
116
+ * @param out - The result path.
117
+ * @returns The provenance path.
118
+ */
119
+ function provenancePath(out) {
120
+ return out.endsWith(".json") ? `${out.slice(0, -5)}.provenance.json` : `${out}.provenance.json`;
121
+ }
122
+ /**
123
+ * Formats a list of validation issues, one per line.
124
+ * @param issues - The issues.
125
+ * @returns The text.
126
+ */
127
+ function formatIssues(issues) {
128
+ return issues.map((issue) => `${issue.path}: ${issue.message}\n`).join("");
129
+ }
130
+ /**
131
+ * Runs the command. A KeepsetError from the library (an unknown dataset, a failing recipe) is the command's failure to answer and becomes exit status 2 with its message; anything else is a bug and propagates.
132
+ * @param args - The arguments after the command name.
133
+ * @param library - The library calls.
134
+ * @param io - Output.
135
+ * @returns The exit status.
136
+ */
137
+ async function run(args, library, io) {
138
+ let parsed;
139
+ try {
140
+ parsed = parseCommandLine(args);
141
+ } catch (error) {
142
+ io.writeStderr(`${error instanceof Error ? error.message : String(error)}\n\n${usage}`);
143
+ return 2;
144
+ }
145
+ const { values, positionals } = parsed;
146
+ if (values.version === true) {
147
+ io.writeStdout(`${io.version}\n`);
148
+ return 0;
149
+ }
150
+ if (values.help === true) {
151
+ io.writeStdout(usage);
152
+ return 0;
153
+ }
154
+ const [command, ...rest] = positionals;
155
+ const reject = (message) => {
156
+ io.writeStderr(`${message}\n\n${usage}`);
157
+ return 2;
158
+ };
159
+ try {
160
+ if (command === "search") {
161
+ if (rest.length > 1) return reject("search takes at most one text argument; quote a phrase");
162
+ const [text] = rest;
163
+ const query = {
164
+ ...text === void 0 ? {} : { text },
165
+ ...values.kind === void 0 ? {} : { kind: values.kind },
166
+ ...values.domain === void 0 ? {} : { domain: values.domain },
167
+ ...values.category === void 0 ? {} : { category: values.category }
168
+ };
169
+ const datasets = library.listDatasets(query);
170
+ io.writeStdout(values.json === true ? json(datasets.map((dataset) => dataset.entry)) : formatSearch(datasets));
171
+ return 0;
172
+ }
173
+ const [id, ...extra] = rest;
174
+ if (command === void 0) return reject("expected a command");
175
+ if (![
176
+ "show",
177
+ "fetch",
178
+ "validate",
179
+ "roundtrip"
180
+ ].includes(command)) return reject(`unknown command ${command}`);
181
+ if (id === void 0 || extra.length > 0) return reject(`${command} takes exactly one dataset id`);
182
+ if (command === "show") {
183
+ const dataset = library.getDataset(id);
184
+ io.writeStdout(values.json === true ? json(dataset) : formatShow(dataset));
185
+ return 0;
186
+ }
187
+ if (command === "validate") {
188
+ const issues = library.validateDataset(library.getDataset(id));
189
+ if (issues.length > 0) {
190
+ io.writeStdout(formatIssues(issues));
191
+ return 1;
192
+ }
193
+ io.writeStdout(`${id} is valid\n`);
194
+ return 0;
195
+ }
196
+ if (command === "fetch") {
197
+ const fetched = await library.fetchDataset(id);
198
+ const out = values.out ?? `${id}.json`;
199
+ io.writeFile(out, json(fetched.data));
200
+ io.writeFile(provenancePath(out), json(fetched.provenance));
201
+ for (const warning of fetched.warnings) io.writeStderr(`warning: ${warning}\n`);
202
+ const counts = [...library.countTarget(fetched)].map(([key, count]) => `${key} ${String(count)}`).join(", ");
203
+ io.writeStdout(`wrote ${out} (${counts}) and ${provenancePath(out)}\n`);
204
+ return 0;
205
+ }
206
+ const report = await library.roundTripDataset(id);
207
+ if (report.holds) {
208
+ io.writeStdout(`${id}: the round trip holds\n`);
209
+ return 0;
210
+ }
211
+ io.writeStdout(`${id}: the round trip does not hold at ${report.path === "" ? "(root)" : report.path}: ${report.message}\n`);
212
+ return 1;
213
+ } catch (error) {
214
+ if (error instanceof KeepsetError) {
215
+ io.writeStderr(`${error.message}\n`);
216
+ return 2;
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ //#endregion
222
+ //#region cli/keepset.ts
223
+ process.stdout.on("error", (error) => {
224
+ if (error.code !== "EPIPE") throw error;
225
+ });
226
+ process.exitCode = await run(process.argv.slice(2), {
227
+ listDatasets,
228
+ getDataset,
229
+ validateDataset,
230
+ fetchDataset: async (id) => fetchDataset(id),
231
+ roundTripDataset: async (id) => roundTripDataset(id),
232
+ countTarget: (fetched) => countTarget(fetched.dataset.entry.category, fetched.data)
233
+ }, {
234
+ writeStdout: (text) => {
235
+ process.stdout.write(text);
236
+ },
237
+ writeStderr: (text) => {
238
+ process.stderr.write(text);
239
+ },
240
+ writeFile: (path, text) => {
241
+ writeFileSync(path, text, "utf8");
242
+ },
243
+ version
244
+ });
245
+ //#endregion
246
+ export {};
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
+ "bin": {
6
+ "keepset": "./dist-cli/keepset.mjs"
7
+ },
5
8
  "dependencies": {
6
9
  "fflate": "0.8.3",
7
10
  "twindex": "0.1.1",
@@ -49,6 +52,7 @@
49
52
  },
50
53
  "files": [
51
54
  "dist",
55
+ "dist-cli",
52
56
  "schemas"
53
57
  ],
54
58
  "keywords": [
@@ -1 +1 @@
1
- {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.1/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"category":{"enum":["graph/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"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.4.0/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.1/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"category":{"enum":["graph/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"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.5.0/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"},"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.4.0/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"},"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.5.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}