@chartcoach/catalog 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +324 -38
  2. package/dist/duckdb-wasm.d.ts +8 -0
  3. package/dist/duckdb-wasm.js +22 -0
  4. package/dist/duckdb.d.ts +8 -0
  5. package/dist/duckdb.js +9 -0
  6. package/dist/index-cache-Cx1gIK6N.js +280 -0
  7. package/dist/index.d.ts +40 -8
  8. package/dist/index.js +22 -7
  9. package/dist/json-CBWjz9b4.js +29 -0
  10. package/dist/model-De0MF-zg.d.ts +255 -0
  11. package/dist/node.d.ts +15 -0
  12. package/dist/node.js +218 -0
  13. package/dist/open-Nnzkr_bE.d.ts +15 -0
  14. package/dist/open-Uu3-r8kp.js +1244 -0
  15. package/dist/registration-CgQGMUIH.d.ts +6 -0
  16. package/dist/registration-DXhS7vgr.js +41 -0
  17. package/dist/tables-DcgKnXwF.js +389 -0
  18. package/package.json +47 -18
  19. package/src/catalog/artifacts.ts +309 -100
  20. package/src/catalog/description.ts +198 -0
  21. package/src/catalog/errors.ts +26 -1
  22. package/src/catalog/identity.ts +69 -0
  23. package/src/catalog/json.ts +32 -0
  24. package/src/catalog/labels.ts +8 -6
  25. package/src/catalog/manifest.ts +81 -20
  26. package/src/catalog/markdown.ts +2 -2
  27. package/src/catalog/model.ts +222 -37
  28. package/src/catalog/open.ts +428 -0
  29. package/src/catalog/parquet.ts +164 -0
  30. package/src/catalog/profile-layout.ts +86 -0
  31. package/src/catalog/profile.ts +345 -0
  32. package/src/catalog/query.ts +125 -0
  33. package/src/catalog/read.ts +101 -0
  34. package/src/catalog/references.ts +336 -0
  35. package/src/catalog/registration.ts +74 -0
  36. package/src/catalog/tables.ts +250 -0
  37. package/src/catalog/wire.ts +38 -65
  38. package/src/duckdb-wasm.ts +33 -0
  39. package/src/duckdb.ts +18 -0
  40. package/src/index.ts +51 -20
  41. package/src/node/index-cache.ts +432 -0
  42. package/src/node.ts +347 -0
  43. package/dist/catalog/artifacts.d.ts +0 -28
  44. package/dist/catalog/artifacts.d.ts.map +0 -1
  45. package/dist/catalog/artifacts.js +0 -89
  46. package/dist/catalog/chartcoach-defaults.d.ts +0 -17
  47. package/dist/catalog/chartcoach-defaults.d.ts.map +0 -1
  48. package/dist/catalog/chartcoach-defaults.js +0 -8
  49. package/dist/catalog/errors.d.ts +0 -4
  50. package/dist/catalog/errors.d.ts.map +0 -1
  51. package/dist/catalog/errors.js +0 -6
  52. package/dist/catalog/labels.d.ts +0 -9
  53. package/dist/catalog/labels.d.ts.map +0 -1
  54. package/dist/catalog/labels.js +0 -23
  55. package/dist/catalog/load-parquet-core.d.ts +0 -15
  56. package/dist/catalog/load-parquet-core.d.ts.map +0 -1
  57. package/dist/catalog/load-parquet-core.js +0 -45
  58. package/dist/catalog/manifest.d.ts +0 -15
  59. package/dist/catalog/manifest.d.ts.map +0 -1
  60. package/dist/catalog/manifest.js +0 -143
  61. package/dist/catalog/markdown.d.ts +0 -3
  62. package/dist/catalog/markdown.d.ts.map +0 -1
  63. package/dist/catalog/markdown.js +0 -13
  64. package/dist/catalog/model.d.ts +0 -33
  65. package/dist/catalog/model.d.ts.map +0 -1
  66. package/dist/catalog/model.js +0 -67
  67. package/dist/catalog/wire.d.ts +0 -18
  68. package/dist/catalog/wire.d.ts.map +0 -1
  69. package/dist/catalog/wire.js +0 -78
  70. package/dist/index.d.ts.map +0 -1
  71. package/src/catalog/chartcoach-defaults.ts +0 -17
  72. package/src/catalog/load-parquet-core.ts +0 -78
@@ -0,0 +1,69 @@
1
+ import { CatalogError } from "./errors";
2
+ import { isJsonObject, type JsonObject, type JsonValue } from "./json";
3
+ import type { Guideline } from "./model";
4
+
5
+ export async function catalogEntriesDigest(guidelines: readonly Guideline[]): Promise<string> {
6
+ const rows = [...guidelines]
7
+ .sort((left, right) => compareUnicode(left.id, right.id))
8
+ .map((guideline): JsonObject => ({
9
+ id: guideline.id,
10
+ title: guideline.title,
11
+ description: guideline.description,
12
+ labels: guideline.labels,
13
+ sections: guideline.sections.map((section) => ({
14
+ role: section.role,
15
+ title: section.title,
16
+ content: section.content,
17
+ })),
18
+ references: guideline.references,
19
+ }));
20
+
21
+ return sha256Text(rows.map(canonicalJson).join(""));
22
+ }
23
+
24
+ export async function manifestDigest(markdown: string): Promise<string> {
25
+ return sha256Text(markdown);
26
+ }
27
+
28
+ export function canonicalJson(value: JsonValue): string {
29
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
30
+
31
+ if (!isJsonObject(value)) return JSON.stringify(value) ?? "null";
32
+
33
+ return `{${Object.keys(value)
34
+ .sort(compareUnicode)
35
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key]!)}`)
36
+ .join(",")}}`;
37
+ }
38
+
39
+ export async function sha256Bytes(value: Uint8Array): Promise<string> {
40
+ const subtle = globalThis.crypto?.subtle;
41
+
42
+ if (!subtle) {
43
+ throw new CatalogError("SHA-256 digest support is unavailable in this runtime.", {
44
+ code: "unavailable_capability",
45
+ });
46
+ }
47
+
48
+ const digest = await subtle.digest("SHA-256", value.slice().buffer);
49
+
50
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
51
+ }
52
+
53
+ export function compareUnicode(left: string, right: string): number {
54
+ const leftPoints = Array.from(left, (character) => character.codePointAt(0)!);
55
+ const rightPoints = Array.from(right, (character) => character.codePointAt(0)!);
56
+ const length = Math.min(leftPoints.length, rightPoints.length);
57
+
58
+ for (let index = 0; index < length; index += 1) {
59
+ const difference = leftPoints[index]! - rightPoints[index]!;
60
+
61
+ if (difference !== 0) return difference;
62
+ }
63
+
64
+ return leftPoints.length - rightPoints.length;
65
+ }
66
+
67
+ async function sha256Text(value: string): Promise<string> {
68
+ return sha256Bytes(new TextEncoder().encode(value));
69
+ }
@@ -0,0 +1,32 @@
1
+ type JsonScalar = boolean | null | number | string;
2
+
3
+ export interface JsonObject {
4
+ [key: string]: JsonValue;
5
+ }
6
+
7
+ export type JsonValue = JsonScalar | JsonObject | readonly JsonValue[];
8
+
9
+ export function parseJson(text: string): JsonValue {
10
+ return JSON.parse(text);
11
+ }
12
+
13
+ export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
14
+ return (
15
+ value !== null &&
16
+ value !== undefined &&
17
+ !Array.isArray(value) &&
18
+ Object.prototype.toString.call(value) === "[object Object]"
19
+ );
20
+ }
21
+
22
+ export function isJsonNumber(value: JsonValue | undefined): value is number {
23
+ return (
24
+ Object.prototype.toString.call(value) === "[object Number]" &&
25
+ value === value?.valueOf() &&
26
+ Number.isFinite(value)
27
+ );
28
+ }
29
+
30
+ export function isJsonString(value: JsonValue | undefined): value is string {
31
+ return Object.prototype.toString.call(value) === "[object String]" && value === value?.valueOf();
32
+ }
@@ -7,20 +7,22 @@ export type CatalogLabel = {
7
7
  modifier?: string;
8
8
  };
9
9
 
10
- export function parseLabel(value: unknown, context = "label"): CatalogLabel {
11
- if (typeof value !== "string") {
12
- throw new CatalogError(`${context} must be a string.`);
13
- }
10
+ export function parseLabel(value: string, context = "label"): CatalogLabel {
14
11
  const parts = value
15
12
  .trim()
16
13
  .split(":")
17
14
  .map((part) => part.trim());
15
+
18
16
  if ((parts.length !== 2 && parts.length !== 3) || parts.some((part) => part.length === 0)) {
19
17
  throw new CatalogError(
20
18
  `${context} must use <family>:<category> or <family>:<category>:<modifier>.`,
21
19
  );
22
20
  }
23
- const [family, category, modifier] = parts as [string, string, string | undefined];
21
+
22
+ const family = parts[0]!;
23
+ const category = parts[1]!;
24
+ const modifier = parts[2];
25
+
24
26
  return {
25
27
  value: modifier === undefined ? `${family}:${category}` : `${family}:${category}:${modifier}`,
26
28
  family,
@@ -29,6 +31,6 @@ export function parseLabel(value: unknown, context = "label"): CatalogLabel {
29
31
  };
30
32
  }
31
33
 
32
- export function normalizeLabel(value: unknown, context = "label"): string {
34
+ export function normalizeLabel(value: string, context = "label"): string {
33
35
  return parseLabel(value, context).value;
34
36
  }
@@ -2,52 +2,68 @@ import { CatalogError } from "./errors";
2
2
  import { parseLabel } from "./labels";
3
3
  import type { Guideline } from "./model";
4
4
 
5
- export const REQUIRED_MANIFEST_HEADINGS = ["Section Roles", "Label Families"] as const;
5
+ const REQUIRED_MANIFEST_HEADINGS = ["Section Roles", "Label Families"] as const;
6
6
 
7
7
  export type ManifestDefinition = {
8
- name: string;
9
- description: string;
10
- examples: string[];
8
+ readonly name: string;
9
+ readonly description: string;
10
+ readonly examples: readonly string[];
11
11
  };
12
12
 
13
13
  export type CatalogManifest = {
14
- markdown: string;
15
- sectionRoles: Record<string, ManifestDefinition>;
16
- labelFamilies: Record<string, ManifestDefinition>;
14
+ readonly markdown: string;
15
+ readonly sectionRoles: Readonly<Record<string, ManifestDefinition>>;
16
+ readonly labelFamilies: Readonly<Record<string, ManifestDefinition>>;
17
+ };
18
+
19
+ type ManifestDefinitions = {
20
+ "Section Roles": Record<string, ManifestDefinition>;
21
+ "Label Families": Record<string, ManifestDefinition>;
17
22
  };
18
23
 
19
24
  const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
25
+
20
26
  const codeSpanPattern = /`([^`\n]+)`/g;
21
27
 
28
+ function manifestDefinitions(): Record<string, ManifestDefinition> {
29
+ const definitions: Record<string, ManifestDefinition> = Object.create(null);
30
+
31
+ return definitions;
32
+ }
33
+
22
34
  export function parseCatalogManifest(markdown: string): CatalogManifest {
23
35
  const requiredSeen = new Set<string>();
24
- const definitions: {
25
- "Section Roles": Record<string, ManifestDefinition>;
26
- "Label Families": Record<string, ManifestDefinition>;
27
- } = {
28
- "Section Roles": {},
29
- "Label Families": {},
36
+
37
+ const definitions: ManifestDefinitions = {
38
+ "Section Roles": manifestDefinitions(),
39
+ "Label Families": manifestDefinitions(),
30
40
  };
31
- let currentHeading: keyof typeof definitions | string | undefined;
41
+
42
+ let currentHeading: string | undefined;
32
43
  let currentName: string | undefined;
33
44
  let currentLines: string[] = [];
34
45
 
35
46
  function flushDefinition() {
36
47
  if (currentHeading !== "Section Roles" && currentHeading !== "Label Families") {
37
48
  currentLines = [];
49
+
38
50
  return;
39
51
  }
52
+
40
53
  if (currentName === undefined) {
41
54
  currentLines = [];
55
+
42
56
  return;
43
57
  }
44
58
 
45
59
  const description = currentLines.join("\n").trim();
60
+
46
61
  if (!description) {
47
62
  throw new CatalogError(
48
63
  `Manifest definition ${currentHeading}/${currentName} must include prose.`,
49
64
  );
50
65
  }
66
+
51
67
  definitions[currentHeading][currentName] = {
52
68
  name: currentName,
53
69
  description,
@@ -59,6 +75,7 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
59
75
 
60
76
  for (const line of markdown.split("\n")) {
61
77
  const match = headingPattern.exec(line);
78
+
62
79
  if (!match) {
63
80
  if (currentName !== undefined) currentLines.push(line);
64
81
  continue;
@@ -72,9 +89,11 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
72
89
  currentHeading = title;
73
90
  currentName = undefined;
74
91
  currentLines = [];
92
+
75
93
  if (title === "Section Roles" || title === "Label Families") {
76
94
  requiredSeen.add(title);
77
95
  }
96
+
78
97
  continue;
79
98
  }
80
99
 
@@ -83,9 +102,11 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
83
102
  (currentHeading === "Section Roles" || currentHeading === "Label Families")
84
103
  ) {
85
104
  flushDefinition();
105
+
86
106
  if (!title) {
87
107
  throw new CatalogError(`Manifest heading ${currentHeading} contains an empty subheading.`);
88
108
  }
109
+
89
110
  currentName = title;
90
111
  currentLines = [];
91
112
  continue;
@@ -97,21 +118,32 @@ export function parseCatalogManifest(markdown: string): CatalogManifest {
97
118
  flushDefinition();
98
119
 
99
120
  const missing = REQUIRED_MANIFEST_HEADINGS.filter((heading) => !requiredSeen.has(heading));
121
+
100
122
  if (missing.length > 0) {
101
123
  throw new CatalogError(`MANIFEST.md is missing required heading(s): ${missing.join(", ")}.`);
102
124
  }
125
+
103
126
  for (const heading of REQUIRED_MANIFEST_HEADINGS) {
104
127
  if (Object.keys(definitions[heading]).length === 0) {
105
- throw new CatalogError(`Manifest heading ${heading} must define entries.`);
128
+ throw new CatalogError(`Manifest heading ${heading} must contain definitions.`);
106
129
  }
107
130
  }
131
+
108
132
  validateLabelFamilyExamples(Object.values(definitions["Label Families"]));
109
133
 
110
- return {
134
+ return copyCatalogManifest({
111
135
  markdown: markdown.endsWith("\n") ? markdown : `${markdown}\n`,
112
136
  sectionRoles: definitions["Section Roles"],
113
137
  labelFamilies: definitions["Label Families"],
114
- };
138
+ });
139
+ }
140
+
141
+ export function copyCatalogManifest(manifest: CatalogManifest): CatalogManifest {
142
+ return Object.freeze({
143
+ markdown: manifest.markdown,
144
+ sectionRoles: copyDefinitions(manifest.sectionRoles),
145
+ labelFamilies: copyDefinitions(manifest.labelFamilies),
146
+ });
115
147
  }
116
148
 
117
149
  export function validateManifestCoverage(
@@ -124,57 +156,70 @@ export function validateManifestCoverage(
124
156
  for (const guideline of guidelines) {
125
157
  for (const section of guideline.sections) {
126
158
  const role = section.role.trim();
159
+
127
160
  if (!role) {
128
161
  throw new CatalogError("Section role values must not be empty.");
129
162
  }
163
+
164
+ if (role === "__dangling__") continue;
130
165
  usedRoles.add(role);
131
166
  }
167
+
132
168
  for (const label of guideline.labels) {
133
169
  usedFamilies.add(parseLabel(label, `label ${JSON.stringify(label)}`).family);
134
170
  }
135
171
  }
136
172
 
137
173
  const missingRoles = Array.from(usedRoles)
138
- .filter((role) => manifest.sectionRoles[role] === undefined)
174
+ .filter((role) => !Object.hasOwn(manifest.sectionRoles, role))
139
175
  .sort();
176
+
140
177
  const missingFamilies = Array.from(usedFamilies)
141
- .filter((family) => manifest.labelFamilies[family] === undefined)
178
+ .filter((family) => !Object.hasOwn(manifest.labelFamilies, family))
142
179
  .sort();
143
180
 
144
181
  const errors: string[] = [];
182
+
145
183
  if (missingRoles.length > 0) {
146
184
  errors.push(`undefined section role(s): ${missingRoles.join(", ")}`);
147
185
  }
186
+
148
187
  if (missingFamilies.length > 0) {
149
188
  errors.push(`undefined label family/families: ${missingFamilies.join(", ")}`);
150
189
  }
190
+
151
191
  if (errors.length > 0) {
152
192
  throw new CatalogError(`Catalog manifest validation failed: ${errors.join("; ")}.`);
153
193
  }
154
194
  }
155
195
 
156
- function validateLabelFamilyExamples(definitions: ManifestDefinition[]) {
196
+ function validateLabelFamilyExamples(definitions: readonly ManifestDefinition[]) {
157
197
  for (const definition of definitions) {
158
198
  const familyExamples: string[] = [];
159
199
  const invalidExamples: string[] = [];
200
+
160
201
  for (const example of definition.examples) {
161
202
  let parsed;
203
+
162
204
  try {
163
205
  parsed = parseLabel(example, `manifest label example ${JSON.stringify(example)}`);
164
206
  } catch {
165
207
  continue;
166
208
  }
209
+
167
210
  if (parsed.family === definition.name) {
168
211
  familyExamples.push(parsed.value);
169
212
  } else if (example.includes(":")) {
170
213
  invalidExamples.push(example);
171
214
  }
172
215
  }
216
+
173
217
  if (invalidExamples.length > 0) {
174
218
  throw new CatalogError(
175
219
  `Label family ${definition.name} has example(s) from another family: ${invalidExamples.join(", ")}.`,
176
220
  );
177
221
  }
222
+
178
223
  if (familyExamples.length === 0) {
179
224
  throw new CatalogError(
180
225
  `Label family ${definition.name} must include at least one label example.`,
@@ -182,3 +227,19 @@ function validateLabelFamilyExamples(definitions: ManifestDefinition[]) {
182
227
  }
183
228
  }
184
229
  }
230
+
231
+ function copyDefinitions(
232
+ definitions: Readonly<Record<string, ManifestDefinition>>,
233
+ ): Readonly<Record<string, ManifestDefinition>> {
234
+ const owned = manifestDefinitions();
235
+
236
+ for (const [name, definition] of Object.entries(definitions)) {
237
+ owned[name] = Object.freeze({
238
+ name: definition.name,
239
+ description: definition.description,
240
+ examples: Object.freeze([...definition.examples]),
241
+ });
242
+ }
243
+
244
+ return Object.freeze(owned);
245
+ }
@@ -3,15 +3,15 @@ import { stringify as stringifyYaml } from "yaml";
3
3
  import type { Guideline } from "./model";
4
4
 
5
5
  export function toMarkdown(
6
- guideline: Pick<Guideline, "id" | "title" | "bibliography" | "description" | "labels" | "body">,
6
+ guideline: Pick<Guideline, "id" | "title" | "description" | "labels" | "body">,
7
7
  ): string {
8
8
  const frontmatter = {
9
9
  id: guideline.id,
10
10
  title: guideline.title,
11
- ...(guideline.bibliography ? { bibliography: guideline.bibliography } : {}),
12
11
  description: guideline.description,
13
12
  labels: [...guideline.labels],
14
13
  };
14
+
15
15
  const frontmatterYaml = stringifyYaml(frontmatter, { sortMapEntries: false }).trim();
16
16
  const body = guideline.body.trim();
17
17