@chartcoach/catalog 0.1.3
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 +71 -0
- package/dist/catalog/artifacts.d.ts +27 -0
- package/dist/catalog/artifacts.d.ts.map +1 -0
- package/dist/catalog/artifacts.js +90 -0
- package/dist/catalog/errors.d.ts +4 -0
- package/dist/catalog/errors.d.ts.map +1 -0
- package/dist/catalog/errors.js +6 -0
- package/dist/catalog/labels.d.ts +9 -0
- package/dist/catalog/labels.d.ts.map +1 -0
- package/dist/catalog/labels.js +23 -0
- package/dist/catalog/load-parquet-core.d.ts +15 -0
- package/dist/catalog/load-parquet-core.d.ts.map +1 -0
- package/dist/catalog/load-parquet-core.js +47 -0
- package/dist/catalog/manifest.d.ts +15 -0
- package/dist/catalog/manifest.d.ts.map +1 -0
- package/dist/catalog/manifest.js +144 -0
- package/dist/catalog/markdown.d.ts +3 -0
- package/dist/catalog/markdown.d.ts.map +1 -0
- package/dist/catalog/markdown.js +13 -0
- package/dist/catalog/model.d.ts +33 -0
- package/dist/catalog/model.d.ts.map +1 -0
- package/dist/catalog/model.js +67 -0
- package/dist/catalog/wire.d.ts +18 -0
- package/dist/catalog/wire.d.ts.map +1 -0
- package/dist/catalog/wire.js +78 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/package.json +49 -0
- package/src/catalog/artifacts.ts +127 -0
- package/src/catalog/errors.ts +6 -0
- package/src/catalog/labels.ts +32 -0
- package/src/catalog/load-parquet-core.ts +81 -0
- package/src/catalog/manifest.ts +186 -0
- package/src/catalog/markdown.ts +22 -0
- package/src/catalog/model.ts +105 -0
- package/src/catalog/wire.ts +93 -0
- package/src/index.ts +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @chartcoach/catalog
|
|
2
|
+
|
|
3
|
+
JavaScript models and parsers for chartcoach catalog artifacts.
|
|
4
|
+
|
|
5
|
+
Read the package docs at [docs.chartcoach.dev/javascript](https://docs.chartcoach.dev/javascript).
|
|
6
|
+
Read the catalog contract at [docs.chartcoach.dev/catalog](https://docs.chartcoach.dev/catalog).
|
|
7
|
+
|
|
8
|
+
Install the package from an application root:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @chartcoach/catalog
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { DEFAULT_CATALOG, loadCatalog } from "@chartcoach/catalog"
|
|
16
|
+
|
|
17
|
+
const entries = await fetch(DEFAULT_CATALOG.entriesUrl).then((response) =>
|
|
18
|
+
response.arrayBuffer(),
|
|
19
|
+
)
|
|
20
|
+
const manifestText = await fetch(DEFAULT_CATALOG.manifestUrl).then((response) =>
|
|
21
|
+
response.text(),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const catalog = await loadCatalog({ entries, manifestText })
|
|
25
|
+
const guideline = catalog.require("compare-percentages-with-bars-not-pies")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`@chartcoach/catalog` accepts bytes that the caller already acquired. Use
|
|
29
|
+
`fetch`, `fs.readFile`, `Bun.file`, `Deno.readFile`, or a test fixture to read
|
|
30
|
+
the artifacts, then pass `entries` and optional manifest data to `loadCatalog`.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { readFile } from "node:fs/promises"
|
|
34
|
+
import { loadCatalog } from "@chartcoach/catalog"
|
|
35
|
+
|
|
36
|
+
const catalog = await loadCatalog({
|
|
37
|
+
entries: await readFile("entries.parquet"),
|
|
38
|
+
manifestText: await readFile("MANIFEST.md", "utf8"),
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Parse release metadata when code needs artifact URLs:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import {
|
|
46
|
+
catalogArtifact,
|
|
47
|
+
catalogArtifactUrl,
|
|
48
|
+
parseCatalogReleaseMetadata,
|
|
49
|
+
} from "@chartcoach/catalog"
|
|
50
|
+
|
|
51
|
+
const metadata = parseCatalogReleaseMetadata(await response.json())
|
|
52
|
+
const entriesUrl = catalogArtifactUrl(
|
|
53
|
+
metadataUrl,
|
|
54
|
+
catalogArtifact(metadata, "entries"),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const entries = await fetch(entriesUrl).then((response) => response.arrayBuffer())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run package checks from the repository root:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pnpm --dir packages/catalog-javascript lint
|
|
64
|
+
pnpm --dir packages/catalog-javascript typecheck
|
|
65
|
+
pnpm --dir packages/catalog-javascript test
|
|
66
|
+
pnpm --dir packages/catalog-javascript build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT. See [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const DEFAULT_CATALOG: {
|
|
2
|
+
readonly version: "0.1.3";
|
|
3
|
+
readonly digest: "7cfd43ee820be252b8ae9058c4c36109a9c8415c6b3a5ff8a9127117b4a10c19";
|
|
4
|
+
readonly releaseRootUrl: string;
|
|
5
|
+
readonly metadataUrl: string;
|
|
6
|
+
readonly entriesUrl: string;
|
|
7
|
+
readonly manifestUrl: string;
|
|
8
|
+
};
|
|
9
|
+
export type ArtifactKind = "manifest" | "entries" | "lancedb-index";
|
|
10
|
+
export type ArtifactDescriptor = {
|
|
11
|
+
kind: ArtifactKind;
|
|
12
|
+
path: string;
|
|
13
|
+
digest: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
format?: string;
|
|
16
|
+
rows?: number;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
export type CatalogReleaseMetadata = {
|
|
20
|
+
version: string;
|
|
21
|
+
digest: string;
|
|
22
|
+
artifacts: ArtifactDescriptor[];
|
|
23
|
+
};
|
|
24
|
+
export declare function parseCatalogReleaseMetadata(value: unknown): CatalogReleaseMetadata;
|
|
25
|
+
export declare function catalogArtifactUrl(baseUrl: string | URL, descriptor: ArtifactDescriptor): string;
|
|
26
|
+
export declare function catalogArtifact(metadata: CatalogReleaseMetadata, kind: ArtifactKind): ArtifactDescriptor;
|
|
27
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../src/catalog/artifacts.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,eAAe;;;;;;;CAOlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,eAAe,CAAC;AAEpE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAgBlF;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,GAAG,EACrB,UAAU,EAAE,kBAAkB,GAC7B,MAAM,CAER;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,sBAAsB,EAChC,IAAI,EAAE,YAAY,GACjB,kBAAkB,CAMpB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CatalogError } from "./errors.js";
|
|
2
|
+
const defaultCatalogArtifactBaseUrl = "https://artifacts.chartcoach.dev";
|
|
3
|
+
const defaultCatalogDigest = "7cfd43ee820be252b8ae9058c4c36109a9c8415c6b3a5ff8a9127117b4a10c19";
|
|
4
|
+
const defaultCatalogVersion = "0.1.3";
|
|
5
|
+
const defaultCatalogReleaseRootUrl = new URL(`catalog/releases/${defaultCatalogVersion}/${defaultCatalogDigest}/`, `${defaultCatalogArtifactBaseUrl}/`).toString();
|
|
6
|
+
export const DEFAULT_CATALOG = {
|
|
7
|
+
version: defaultCatalogVersion,
|
|
8
|
+
digest: defaultCatalogDigest,
|
|
9
|
+
releaseRootUrl: defaultCatalogReleaseRootUrl,
|
|
10
|
+
metadataUrl: new URL("metadata.json", defaultCatalogReleaseRootUrl).toString(),
|
|
11
|
+
entriesUrl: new URL("entries.parquet", defaultCatalogReleaseRootUrl).toString(),
|
|
12
|
+
manifestUrl: new URL("MANIFEST.md", defaultCatalogReleaseRootUrl).toString(),
|
|
13
|
+
};
|
|
14
|
+
export function parseCatalogReleaseMetadata(value) {
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
throw new CatalogError("Catalog release metadata must be an object.");
|
|
17
|
+
}
|
|
18
|
+
const artifacts = value.artifacts;
|
|
19
|
+
if (!Array.isArray(artifacts)) {
|
|
20
|
+
throw new CatalogError("Catalog release metadata artifacts must be a list.");
|
|
21
|
+
}
|
|
22
|
+
const metadata = {
|
|
23
|
+
version: requiredString(value, "version"),
|
|
24
|
+
digest: requiredString(value, "digest"),
|
|
25
|
+
artifacts: artifacts.map(parseArtifactDescriptor),
|
|
26
|
+
};
|
|
27
|
+
catalogArtifact(metadata, "manifest");
|
|
28
|
+
catalogArtifact(metadata, "entries");
|
|
29
|
+
return metadata;
|
|
30
|
+
}
|
|
31
|
+
export function catalogArtifactUrl(baseUrl, descriptor) {
|
|
32
|
+
return new URL(descriptor.path, baseUrl).toString();
|
|
33
|
+
}
|
|
34
|
+
export function catalogArtifact(metadata, kind) {
|
|
35
|
+
const descriptor = metadata.artifacts.find((item) => item.kind === kind);
|
|
36
|
+
if (!descriptor) {
|
|
37
|
+
throw new CatalogError(`Catalog release metadata is missing a ${kind} artifact.`);
|
|
38
|
+
}
|
|
39
|
+
return descriptor;
|
|
40
|
+
}
|
|
41
|
+
function parseArtifactDescriptor(value) {
|
|
42
|
+
if (!isRecord(value)) {
|
|
43
|
+
throw new CatalogError("Catalog artifact descriptor must be an object.");
|
|
44
|
+
}
|
|
45
|
+
const kind = value.kind;
|
|
46
|
+
if (kind !== "manifest" && kind !== "entries" && kind !== "lancedb-index") {
|
|
47
|
+
throw new CatalogError(`Unsupported catalog artifact kind: ${String(kind)}`);
|
|
48
|
+
}
|
|
49
|
+
const path = requiredString(value, "path");
|
|
50
|
+
validateRelativePath(path);
|
|
51
|
+
const descriptor = {
|
|
52
|
+
kind,
|
|
53
|
+
path,
|
|
54
|
+
digest: requiredString(value, "digest"),
|
|
55
|
+
bytes: requiredInteger(value, "bytes"),
|
|
56
|
+
};
|
|
57
|
+
if (typeof value.format === "string")
|
|
58
|
+
descriptor.format = value.format;
|
|
59
|
+
if (typeof value.rows === "number" && Number.isInteger(value.rows)) {
|
|
60
|
+
descriptor.rows = value.rows;
|
|
61
|
+
}
|
|
62
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
63
|
+
if (!["kind", "path", "digest", "bytes", "format", "rows"].includes(key)) {
|
|
64
|
+
descriptor[key] = rawValue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return descriptor;
|
|
68
|
+
}
|
|
69
|
+
function validateRelativePath(path) {
|
|
70
|
+
if (path.startsWith("/") || path.split("/").includes("..")) {
|
|
71
|
+
throw new CatalogError(`Catalog artifact path must be relative: ${path}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function requiredString(value, key) {
|
|
75
|
+
const raw = value[key];
|
|
76
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
77
|
+
throw new CatalogError(`Catalog release metadata ${key} must be a string.`);
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
function requiredInteger(value, key) {
|
|
82
|
+
const raw = value[key];
|
|
83
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
84
|
+
throw new CatalogError(`Catalog release metadata ${key} must be a non-negative integer.`);
|
|
85
|
+
}
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/catalog/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type CatalogLabel = {
|
|
2
|
+
value: string;
|
|
3
|
+
family: string;
|
|
4
|
+
category: string;
|
|
5
|
+
modifier?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseLabel(value: unknown, context?: string): CatalogLabel;
|
|
8
|
+
export declare function normalizeLabel(value: unknown, context?: string): string;
|
|
9
|
+
//# sourceMappingURL=labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../../src/catalog/labels.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,SAAU,GAAG,YAAY,CAkB1E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,SAAU,GAAG,MAAM,CAExE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CatalogError } from "./errors.js";
|
|
2
|
+
export function parseLabel(value, context = "label") {
|
|
3
|
+
if (typeof value !== "string") {
|
|
4
|
+
throw new CatalogError(`${context} must be a string.`);
|
|
5
|
+
}
|
|
6
|
+
const parts = value
|
|
7
|
+
.trim()
|
|
8
|
+
.split(":")
|
|
9
|
+
.map((part) => part.trim());
|
|
10
|
+
if ((parts.length !== 2 && parts.length !== 3) || parts.some((part) => part.length === 0)) {
|
|
11
|
+
throw new CatalogError(`${context} must use <family>:<category> or <family>:<category>:<modifier>.`);
|
|
12
|
+
}
|
|
13
|
+
const [family, category, modifier] = parts;
|
|
14
|
+
return {
|
|
15
|
+
value: modifier === undefined ? `${family}:${category}` : `${family}:${category}:${modifier}`,
|
|
16
|
+
family,
|
|
17
|
+
category,
|
|
18
|
+
modifier,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function normalizeLabel(value, context = "label") {
|
|
22
|
+
return parseLabel(value, context).value;
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Catalog } from "./model.js";
|
|
2
|
+
import type { CatalogManifest } from "./manifest.js";
|
|
3
|
+
export type AsyncBuffer = {
|
|
4
|
+
byteLength: number;
|
|
5
|
+
slice(start: number, end?: number): ArrayBuffer | Promise<ArrayBuffer>;
|
|
6
|
+
};
|
|
7
|
+
export type ParquetBytes = ArrayBuffer | ArrayBufferView;
|
|
8
|
+
export type CatalogBytes = ParquetBytes | AsyncBuffer;
|
|
9
|
+
export type LoadCatalogInput = CatalogBytes | {
|
|
10
|
+
entries: CatalogBytes;
|
|
11
|
+
manifest?: CatalogManifest;
|
|
12
|
+
manifestText?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function loadCatalog(input: LoadCatalogInput): Promise<Catalog>;
|
|
15
|
+
//# sourceMappingURL=load-parquet-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-parquet-core.d.ts","sourceRoot":"","sources":["../../src/catalog/load-parquet-core.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAkB,MAAM,SAAS,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIlD,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,eAAe,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,WAAW,CAAC;AACtD,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG;IAC5C,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAWF,wBAAsB,WAAW,CAC/B,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,OAAO,CAAC,CAiBlB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parquetReadObjects } from "hyparquet";
|
|
2
|
+
import { compressors } from "hyparquet-compressors";
|
|
3
|
+
import { Catalog } from "./model.js";
|
|
4
|
+
import { CatalogError } from "./errors.js";
|
|
5
|
+
import { parseCatalogManifest } from "./manifest.js";
|
|
6
|
+
import { requireGuidelineFromWire } from "./wire.js";
|
|
7
|
+
function normalizeParquetBytes(bytes) {
|
|
8
|
+
if (bytes instanceof ArrayBuffer)
|
|
9
|
+
return bytes;
|
|
10
|
+
// TypedArray/DataView may be a view into a larger ArrayBuffer (or SharedArrayBuffer).
|
|
11
|
+
// Copy to a standalone ArrayBuffer covering exactly the view range.
|
|
12
|
+
const u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
13
|
+
return u8.slice().buffer;
|
|
14
|
+
}
|
|
15
|
+
export async function loadCatalog(input) {
|
|
16
|
+
const { entries, manifest } = resolveLoadCatalogInput(input);
|
|
17
|
+
const normalizedFile = entries instanceof ArrayBuffer || ArrayBuffer.isView(entries)
|
|
18
|
+
? normalizeParquetBytes(entries)
|
|
19
|
+
: entries;
|
|
20
|
+
const rows = (await parquetReadObjects({
|
|
21
|
+
file: normalizedFile,
|
|
22
|
+
compressors,
|
|
23
|
+
}));
|
|
24
|
+
const guidelines = rows.map((row, index) => requireGuidelineFromWire(row, `parquet row ${index}`));
|
|
25
|
+
return new Catalog(guidelines, { manifest });
|
|
26
|
+
}
|
|
27
|
+
function resolveLoadCatalogInput(input) {
|
|
28
|
+
if (isCatalogBytes(input))
|
|
29
|
+
return { entries: input };
|
|
30
|
+
if (input.manifest && input.manifestText !== undefined) {
|
|
31
|
+
throw new CatalogError("Pass manifest or manifestText, not both.");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
entries: input.entries,
|
|
35
|
+
manifest: input.manifestText === undefined
|
|
36
|
+
? input.manifest
|
|
37
|
+
: parseCatalogManifest(input.manifestText),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function isCatalogBytes(value) {
|
|
41
|
+
return (value instanceof ArrayBuffer ||
|
|
42
|
+
ArrayBuffer.isView(value) ||
|
|
43
|
+
(typeof value === "object" &&
|
|
44
|
+
value !== null &&
|
|
45
|
+
typeof value.byteLength === "number" &&
|
|
46
|
+
typeof value.slice === "function"));
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Guideline } from "./model.js";
|
|
2
|
+
export declare const REQUIRED_MANIFEST_HEADINGS: readonly ["Section Roles", "Label Families"];
|
|
3
|
+
export type ManifestDefinition = {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
examples: string[];
|
|
7
|
+
};
|
|
8
|
+
export type CatalogManifest = {
|
|
9
|
+
markdown: string;
|
|
10
|
+
sectionRoles: Record<string, ManifestDefinition>;
|
|
11
|
+
labelFamilies: Record<string, ManifestDefinition>;
|
|
12
|
+
};
|
|
13
|
+
export declare function parseCatalogManifest(markdown: string): CatalogManifest;
|
|
14
|
+
export declare function validateManifestCoverage(guidelines: readonly Guideline[], manifest: CatalogManifest): void;
|
|
15
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/catalog/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,eAAO,MAAM,0BAA0B,8CAA+C,CAAC;AAEvF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACjD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CACnD,CAAC;AAKF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAkGtE;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,EAAE,eAAe,QAkCnG"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { CatalogError } from "./errors.js";
|
|
2
|
+
import { parseLabel } from "./labels.js";
|
|
3
|
+
export const REQUIRED_MANIFEST_HEADINGS = ["Section Roles", "Label Families"];
|
|
4
|
+
const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
5
|
+
const codeSpanPattern = /`([^`\n]+)`/g;
|
|
6
|
+
export function parseCatalogManifest(markdown) {
|
|
7
|
+
const requiredSeen = new Set();
|
|
8
|
+
const definitions = {
|
|
9
|
+
"Section Roles": {},
|
|
10
|
+
"Label Families": {},
|
|
11
|
+
};
|
|
12
|
+
let currentHeading;
|
|
13
|
+
let currentName;
|
|
14
|
+
let currentLines = [];
|
|
15
|
+
function flushDefinition() {
|
|
16
|
+
if (currentHeading !== "Section Roles" &&
|
|
17
|
+
currentHeading !== "Label Families") {
|
|
18
|
+
currentLines = [];
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (currentName === undefined) {
|
|
22
|
+
currentLines = [];
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const description = currentLines.join("\n").trim();
|
|
26
|
+
if (!description) {
|
|
27
|
+
throw new CatalogError(`Manifest definition ${currentHeading}/${currentName} must include prose.`);
|
|
28
|
+
}
|
|
29
|
+
definitions[currentHeading][currentName] = {
|
|
30
|
+
name: currentName,
|
|
31
|
+
description,
|
|
32
|
+
examples: Array.from(description.matchAll(codeSpanPattern), (match) => match[1] ?? ""),
|
|
33
|
+
};
|
|
34
|
+
currentName = undefined;
|
|
35
|
+
currentLines = [];
|
|
36
|
+
}
|
|
37
|
+
for (const line of markdown.split("\n")) {
|
|
38
|
+
const match = headingPattern.exec(line);
|
|
39
|
+
if (!match) {
|
|
40
|
+
if (currentName !== undefined)
|
|
41
|
+
currentLines.push(line);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const level = match[1].length;
|
|
45
|
+
const title = match[2].trim();
|
|
46
|
+
if (level === 2) {
|
|
47
|
+
flushDefinition();
|
|
48
|
+
currentHeading = title;
|
|
49
|
+
currentName = undefined;
|
|
50
|
+
currentLines = [];
|
|
51
|
+
if (title === "Section Roles" || title === "Label Families") {
|
|
52
|
+
requiredSeen.add(title);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (level === 3 &&
|
|
57
|
+
(currentHeading === "Section Roles" || currentHeading === "Label Families")) {
|
|
58
|
+
flushDefinition();
|
|
59
|
+
if (!title) {
|
|
60
|
+
throw new CatalogError(`Manifest heading ${currentHeading} contains an empty subheading.`);
|
|
61
|
+
}
|
|
62
|
+
currentName = title;
|
|
63
|
+
currentLines = [];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (currentName !== undefined)
|
|
67
|
+
currentLines.push(line);
|
|
68
|
+
}
|
|
69
|
+
flushDefinition();
|
|
70
|
+
const missing = REQUIRED_MANIFEST_HEADINGS.filter((heading) => !requiredSeen.has(heading));
|
|
71
|
+
if (missing.length > 0) {
|
|
72
|
+
throw new CatalogError(`MANIFEST.md is missing required heading(s): ${missing.join(", ")}.`);
|
|
73
|
+
}
|
|
74
|
+
for (const heading of REQUIRED_MANIFEST_HEADINGS) {
|
|
75
|
+
if (Object.keys(definitions[heading]).length === 0) {
|
|
76
|
+
throw new CatalogError(`Manifest heading ${heading} must define entries.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
validateLabelFamilyExamples(Object.values(definitions["Label Families"]));
|
|
80
|
+
return {
|
|
81
|
+
markdown: markdown.endsWith("\n") ? markdown : `${markdown}\n`,
|
|
82
|
+
sectionRoles: definitions["Section Roles"],
|
|
83
|
+
labelFamilies: definitions["Label Families"],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function validateManifestCoverage(guidelines, manifest) {
|
|
87
|
+
const usedRoles = new Set();
|
|
88
|
+
const usedFamilies = new Set();
|
|
89
|
+
for (const guideline of guidelines) {
|
|
90
|
+
for (const section of guideline.sections) {
|
|
91
|
+
const role = section.role.trim();
|
|
92
|
+
if (!role) {
|
|
93
|
+
throw new CatalogError("Section role values must not be empty.");
|
|
94
|
+
}
|
|
95
|
+
usedRoles.add(role);
|
|
96
|
+
}
|
|
97
|
+
for (const label of guideline.labels) {
|
|
98
|
+
usedFamilies.add(parseLabel(label, `label ${JSON.stringify(label)}`).family);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const missingRoles = Array.from(usedRoles)
|
|
102
|
+
.filter((role) => manifest.sectionRoles[role] === undefined)
|
|
103
|
+
.sort();
|
|
104
|
+
const missingFamilies = Array.from(usedFamilies)
|
|
105
|
+
.filter((family) => manifest.labelFamilies[family] === undefined)
|
|
106
|
+
.sort();
|
|
107
|
+
const errors = [];
|
|
108
|
+
if (missingRoles.length > 0) {
|
|
109
|
+
errors.push(`undefined section role(s): ${missingRoles.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
if (missingFamilies.length > 0) {
|
|
112
|
+
errors.push(`undefined label family/families: ${missingFamilies.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (errors.length > 0) {
|
|
115
|
+
throw new CatalogError(`Catalog manifest validation failed: ${errors.join("; ")}.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function validateLabelFamilyExamples(definitions) {
|
|
119
|
+
for (const definition of definitions) {
|
|
120
|
+
const familyExamples = [];
|
|
121
|
+
const invalidExamples = [];
|
|
122
|
+
for (const example of definition.examples) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = parseLabel(example, `manifest label example ${JSON.stringify(example)}`);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.family === definition.name) {
|
|
131
|
+
familyExamples.push(parsed.value);
|
|
132
|
+
}
|
|
133
|
+
else if (example.includes(":")) {
|
|
134
|
+
invalidExamples.push(example);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (invalidExamples.length > 0) {
|
|
138
|
+
throw new CatalogError(`Label family ${definition.name} has example(s) from another family: ${invalidExamples.join(", ")}.`);
|
|
139
|
+
}
|
|
140
|
+
if (familyExamples.length === 0) {
|
|
141
|
+
throw new CatalogError(`Label family ${definition.name} must include at least one label example.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/catalog/markdown.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,wBAAgB,UAAU,CACxB,SAAS,EAAE,IAAI,CACb,SAAS,EACT,IAAI,GAAG,OAAO,GAAG,cAAc,GAAG,aAAa,GAAG,QAAQ,GAAG,MAAM,CACpE,GACA,MAAM,CAYR"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
2
|
+
export function toMarkdown(guideline) {
|
|
3
|
+
const frontmatter = {
|
|
4
|
+
id: guideline.id,
|
|
5
|
+
title: guideline.title,
|
|
6
|
+
...(guideline.bibliography ? { bibliography: guideline.bibliography } : {}),
|
|
7
|
+
description: guideline.description,
|
|
8
|
+
labels: [...guideline.labels],
|
|
9
|
+
};
|
|
10
|
+
const frontmatterYaml = stringifyYaml(frontmatter, { sortMapEntries: false }).trim();
|
|
11
|
+
const body = guideline.body.trim();
|
|
12
|
+
return ["---", frontmatterYaml, "---", "", body, ""].join("\n");
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CatalogManifest } from "./manifest.js";
|
|
2
|
+
export type GuidelineSection = {
|
|
3
|
+
role: string;
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
export type Guideline = {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
bibliography?: string;
|
|
11
|
+
description: string;
|
|
12
|
+
labels: readonly string[];
|
|
13
|
+
body: string;
|
|
14
|
+
sections: readonly GuidelineSection[];
|
|
15
|
+
references: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
export type CatalogOptions = {
|
|
18
|
+
manifest?: CatalogManifest;
|
|
19
|
+
};
|
|
20
|
+
export declare class Catalog implements Iterable<Guideline> {
|
|
21
|
+
readonly guidelines: readonly Guideline[];
|
|
22
|
+
readonly manifest?: CatalogManifest;
|
|
23
|
+
private readonly byId;
|
|
24
|
+
constructor(guidelines?: Iterable<Guideline>, options?: CatalogOptions);
|
|
25
|
+
get length(): number;
|
|
26
|
+
get size(): number;
|
|
27
|
+
get(id: string): Guideline | undefined;
|
|
28
|
+
require(id: string): Guideline;
|
|
29
|
+
labels(): string[];
|
|
30
|
+
sectionRoles(): string[];
|
|
31
|
+
[Symbol.iterator](): Iterator<Guideline>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/catalog/model.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B,CAAC;AAEF,qBAAa,OAAQ,YAAW,QAAQ,CAAC,SAAS,CAAC;IACjD,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;gBAElC,UAAU,GAAE,QAAQ,CAAC,SAAS,CAAM,EAAE,OAAO,GAAE,cAAmB;IAkB9E,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAItC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS;IAQ9B,MAAM,IAAI,MAAM,EAAE;IAIlB,YAAY,IAAI,MAAM,EAAE;IAQxB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,SAAS,CAAC;CAGzC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { CatalogError } from "./errors.js";
|
|
2
|
+
import { validateManifestCoverage } from "./manifest.js";
|
|
3
|
+
export class Catalog {
|
|
4
|
+
guidelines;
|
|
5
|
+
manifest;
|
|
6
|
+
byId;
|
|
7
|
+
constructor(guidelines = [], options = {}) {
|
|
8
|
+
const records = Array.from(guidelines, copyGuideline);
|
|
9
|
+
const byId = new Map();
|
|
10
|
+
for (const guideline of records) {
|
|
11
|
+
if (byId.has(guideline.id)) {
|
|
12
|
+
throw new CatalogError(`Catalog contains duplicate guideline id: ${guideline.id}.`);
|
|
13
|
+
}
|
|
14
|
+
byId.set(guideline.id, guideline);
|
|
15
|
+
}
|
|
16
|
+
if (options.manifest) {
|
|
17
|
+
validateManifestCoverage(records, options.manifest);
|
|
18
|
+
}
|
|
19
|
+
this.guidelines = records;
|
|
20
|
+
this.manifest = options.manifest;
|
|
21
|
+
this.byId = byId;
|
|
22
|
+
}
|
|
23
|
+
get length() {
|
|
24
|
+
return this.guidelines.length;
|
|
25
|
+
}
|
|
26
|
+
get size() {
|
|
27
|
+
return this.guidelines.length;
|
|
28
|
+
}
|
|
29
|
+
get(id) {
|
|
30
|
+
return this.byId.get(id);
|
|
31
|
+
}
|
|
32
|
+
require(id) {
|
|
33
|
+
const guideline = this.get(id);
|
|
34
|
+
if (!guideline) {
|
|
35
|
+
throw new CatalogError(`Unknown guideline id: ${id}.`);
|
|
36
|
+
}
|
|
37
|
+
return guideline;
|
|
38
|
+
}
|
|
39
|
+
labels() {
|
|
40
|
+
return sortedUnique(this.guidelines.flatMap((guideline) => [...guideline.labels]));
|
|
41
|
+
}
|
|
42
|
+
sectionRoles() {
|
|
43
|
+
return sortedUnique(this.guidelines.flatMap((guideline) => guideline.sections.map((section) => section.role)));
|
|
44
|
+
}
|
|
45
|
+
[Symbol.iterator]() {
|
|
46
|
+
return this.guidelines[Symbol.iterator]();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function copyGuideline(guideline) {
|
|
50
|
+
return {
|
|
51
|
+
id: guideline.id,
|
|
52
|
+
title: guideline.title,
|
|
53
|
+
bibliography: guideline.bibliography,
|
|
54
|
+
description: guideline.description,
|
|
55
|
+
labels: [...guideline.labels],
|
|
56
|
+
body: guideline.body,
|
|
57
|
+
sections: guideline.sections.map((section) => ({
|
|
58
|
+
role: section.role,
|
|
59
|
+
title: section.title,
|
|
60
|
+
content: section.content,
|
|
61
|
+
})),
|
|
62
|
+
references: [...guideline.references],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function sortedUnique(values) {
|
|
66
|
+
return Array.from(new Set(values)).sort();
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Guideline, GuidelineSection } from "./model.js";
|
|
2
|
+
export type CatalogRowWire = {
|
|
3
|
+
id: string;
|
|
4
|
+
guideline: {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
bibliography?: string | null;
|
|
8
|
+
description: string;
|
|
9
|
+
labels: string[];
|
|
10
|
+
body: string;
|
|
11
|
+
sections: GuidelineSection[];
|
|
12
|
+
};
|
|
13
|
+
references: string[];
|
|
14
|
+
};
|
|
15
|
+
export declare function isCatalogRowWire(value: unknown): value is CatalogRowWire;
|
|
16
|
+
export declare function guidelineFromWire(value: unknown): Guideline | null;
|
|
17
|
+
export declare function requireGuidelineFromWire(value: unknown, context?: string): Guideline;
|
|
18
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/catalog/wire.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;KAC9B,CAAC;IACF,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAgCF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAYxE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAsBlE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAMpF"}
|