@chartcoach/catalog 0.1.6 → 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 (73) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +324 -38
  3. package/dist/duckdb-wasm.d.ts +8 -0
  4. package/dist/duckdb-wasm.js +22 -0
  5. package/dist/duckdb.d.ts +8 -0
  6. package/dist/duckdb.js +9 -0
  7. package/dist/index-cache-Cx1gIK6N.js +280 -0
  8. package/dist/index.d.ts +40 -8
  9. package/dist/index.js +22 -7
  10. package/dist/json-CBWjz9b4.js +29 -0
  11. package/dist/model-De0MF-zg.d.ts +255 -0
  12. package/dist/node.d.ts +15 -0
  13. package/dist/node.js +218 -0
  14. package/dist/open-Nnzkr_bE.d.ts +15 -0
  15. package/dist/open-Uu3-r8kp.js +1244 -0
  16. package/dist/registration-CgQGMUIH.d.ts +6 -0
  17. package/dist/registration-DXhS7vgr.js +41 -0
  18. package/dist/tables-DcgKnXwF.js +389 -0
  19. package/package.json +48 -19
  20. package/src/catalog/artifacts.ts +309 -100
  21. package/src/catalog/description.ts +198 -0
  22. package/src/catalog/errors.ts +26 -1
  23. package/src/catalog/identity.ts +69 -0
  24. package/src/catalog/json.ts +32 -0
  25. package/src/catalog/labels.ts +8 -6
  26. package/src/catalog/manifest.ts +81 -20
  27. package/src/catalog/markdown.ts +2 -2
  28. package/src/catalog/model.ts +222 -37
  29. package/src/catalog/open.ts +428 -0
  30. package/src/catalog/parquet.ts +164 -0
  31. package/src/catalog/profile-layout.ts +86 -0
  32. package/src/catalog/profile.ts +345 -0
  33. package/src/catalog/query.ts +125 -0
  34. package/src/catalog/read.ts +101 -0
  35. package/src/catalog/references.ts +336 -0
  36. package/src/catalog/registration.ts +74 -0
  37. package/src/catalog/tables.ts +250 -0
  38. package/src/catalog/wire.ts +38 -65
  39. package/src/duckdb-wasm.ts +33 -0
  40. package/src/duckdb.ts +18 -0
  41. package/src/index.ts +51 -20
  42. package/src/node/index-cache.ts +432 -0
  43. package/src/node.ts +347 -0
  44. package/dist/catalog/artifacts.d.ts +0 -28
  45. package/dist/catalog/artifacts.d.ts.map +0 -1
  46. package/dist/catalog/artifacts.js +0 -89
  47. package/dist/catalog/chartcoach-defaults.d.ts +0 -17
  48. package/dist/catalog/chartcoach-defaults.d.ts.map +0 -1
  49. package/dist/catalog/chartcoach-defaults.js +0 -8
  50. package/dist/catalog/errors.d.ts +0 -4
  51. package/dist/catalog/errors.d.ts.map +0 -1
  52. package/dist/catalog/errors.js +0 -6
  53. package/dist/catalog/labels.d.ts +0 -9
  54. package/dist/catalog/labels.d.ts.map +0 -1
  55. package/dist/catalog/labels.js +0 -23
  56. package/dist/catalog/load-parquet-core.d.ts +0 -15
  57. package/dist/catalog/load-parquet-core.d.ts.map +0 -1
  58. package/dist/catalog/load-parquet-core.js +0 -45
  59. package/dist/catalog/manifest.d.ts +0 -15
  60. package/dist/catalog/manifest.d.ts.map +0 -1
  61. package/dist/catalog/manifest.js +0 -143
  62. package/dist/catalog/markdown.d.ts +0 -3
  63. package/dist/catalog/markdown.d.ts.map +0 -1
  64. package/dist/catalog/markdown.js +0 -13
  65. package/dist/catalog/model.d.ts +0 -33
  66. package/dist/catalog/model.d.ts.map +0 -1
  67. package/dist/catalog/model.js +0 -67
  68. package/dist/catalog/wire.d.ts +0 -18
  69. package/dist/catalog/wire.d.ts.map +0 -1
  70. package/dist/catalog/wire.js +0 -78
  71. package/dist/index.d.ts.map +0 -1
  72. package/src/catalog/chartcoach-defaults.ts +0 -17
  73. package/src/catalog/load-parquet-core.ts +0 -78
@@ -0,0 +1,336 @@
1
+ import { BibDocument, Document, init, Library, ParseError, Style } from "refkit-js";
2
+ import { CatalogError } from "./errors";
3
+ import { canonicalJson, compareUnicode } from "./identity";
4
+ import { isJsonString, type JsonObject } from "./json";
5
+ import type { Catalog } from "./model";
6
+
7
+ await init();
8
+
9
+ const citationStyle = Style.load("apa");
10
+
11
+ const DEFAULT_GUIDELINE_URL_TEMPLATE = "https://chartcoach.dev/guidelines/{id}";
12
+
13
+ export type MinimalSourceRecord = Readonly<{
14
+ reference_id: string;
15
+ authors_text: string | null;
16
+ year: string | null;
17
+ source_title: string | null;
18
+ doi: string | null;
19
+ url: string | null;
20
+ }>;
21
+
22
+ export type FullSourceRecord = Readonly<{
23
+ guideline_id: string;
24
+ reference_id: string;
25
+ source_type: string | null;
26
+ authors: readonly string[];
27
+ authors_text: string | null;
28
+ year: string | null;
29
+ source_title: string | null;
30
+ journal: string | null;
31
+ booktitle: string | null;
32
+ publisher: string | null;
33
+ url: string | null;
34
+ doi: string | null;
35
+ }>;
36
+
37
+ export type CitationSource = Readonly<{
38
+ reference_id: string;
39
+ source_type: string | null;
40
+ authors_text: string | null;
41
+ year: string | null;
42
+ source_title: string | null;
43
+ journal: string | null;
44
+ booktitle: string | null;
45
+ publisher: string | null;
46
+ doi: string | null;
47
+ url: string | null;
48
+ citation: string;
49
+ }>;
50
+
51
+ export type CiteOptions = Readonly<{
52
+ ids: readonly string[];
53
+ urlTemplate?: string;
54
+ }>;
55
+
56
+ export type CitationRecord = Readonly<{
57
+ id: string;
58
+ title: string;
59
+ url: string;
60
+ guideline_citation: string;
61
+ sources: readonly CitationSource[];
62
+ }>;
63
+
64
+ type ParsedReference = Readonly<{
65
+ id: string;
66
+ bibtex: string;
67
+ sourceType: string;
68
+ authors: readonly string[];
69
+ authorsText: string | null;
70
+ year: string | null;
71
+ title: string | null;
72
+ journal: string | null;
73
+ booktitle: string | null;
74
+ publisher: string | null;
75
+ url: string | null;
76
+ doi: string | null;
77
+ fingerprint: string;
78
+ citation: string;
79
+ }>;
80
+
81
+ type ReferenceIndex = Readonly<{
82
+ byId: ReadonlyMap<string, ParsedReference>;
83
+ idsByGuideline: ReadonlyMap<string, readonly string[]>;
84
+ }>;
85
+
86
+ const indexes = new WeakMap<Catalog, ReferenceIndex>();
87
+
88
+ export function sourceRecords(
89
+ catalog: Catalog,
90
+ guidelineId: string,
91
+ detail: "minimal" | "full",
92
+ ): readonly (MinimalSourceRecord | FullSourceRecord)[] {
93
+ const index = referenceIndex(catalog);
94
+ const ids = index.idsByGuideline.get(guidelineId) ?? [];
95
+
96
+ return Object.freeze(
97
+ ids.map((id) => {
98
+ const reference = index.byId.get(id)!;
99
+
100
+ return detail === "minimal" ? minimalSource(reference) : fullSource(guidelineId, reference);
101
+ }),
102
+ );
103
+ }
104
+
105
+ export function citationRecords(catalog: Catalog, options: CiteOptions): readonly CitationRecord[] {
106
+ if (Object.prototype.toString.call(options) !== "[object Object]") {
107
+ throw new CatalogError("Citation options must be an object.");
108
+ }
109
+
110
+ if (!Array.isArray(options.ids) || !options.ids.every(isJsonString)) {
111
+ throw new CatalogError("ids must be an array of strings.");
112
+ }
113
+
114
+ const urlTemplate = options.urlTemplate ?? DEFAULT_GUIDELINE_URL_TEMPLATE;
115
+
116
+ if (!isJsonString(urlTemplate) || !urlTemplate.includes("{id}")) {
117
+ throw new CatalogError("Guideline URL template must include `{id}`.", {
118
+ hints: [`Use a template such as \`${DEFAULT_GUIDELINE_URL_TEMPLATE}\`.`],
119
+ });
120
+ }
121
+
122
+ if (options.ids.length === 0) return Object.freeze([]);
123
+
124
+ const index = referenceIndex(catalog);
125
+
126
+ return Object.freeze(
127
+ options.ids.map((id) => {
128
+ const guideline = catalog.require(id);
129
+ const url = urlTemplate.replace("{id}", id);
130
+
131
+ const sources = Object.freeze(
132
+ (index.idsByGuideline.get(id) ?? []).map((referenceId) =>
133
+ citationSource(index.byId.get(referenceId)!),
134
+ ),
135
+ );
136
+
137
+ return Object.freeze({
138
+ id,
139
+ title: guideline.title,
140
+ url,
141
+ guideline_citation: `[${guideline.title}](${url}) (\`${id}\`)`,
142
+ sources,
143
+ });
144
+ }),
145
+ );
146
+ }
147
+
148
+ export function referenceIndex(catalog: Catalog): ReferenceIndex {
149
+ const cached = indexes.get(catalog);
150
+
151
+ if (cached) return cached;
152
+
153
+ const byId = new Map<string, ParsedReference>();
154
+ const parsed = new Map<string, ParsedReference>();
155
+ const idsByGuideline = new Map<string, readonly string[]>();
156
+
157
+ for (const guideline of catalog.guidelines) {
158
+ const ids = new Set<string>();
159
+
160
+ for (const bibtex of guideline.references) {
161
+ let reference = parsed.get(bibtex);
162
+
163
+ if (!reference) {
164
+ reference = parseReference(bibtex);
165
+ parsed.set(bibtex, reference);
166
+ }
167
+
168
+ const previous = byId.get(reference.id);
169
+
170
+ if (previous && previous.fingerprint !== reference.fingerprint) {
171
+ throw new CatalogError(
172
+ `Conflicting BibTeX definitions for reference id: ${reference.id}.`,
173
+ {
174
+ details: { reference_id: reference.id },
175
+ },
176
+ );
177
+ }
178
+
179
+ byId.set(
180
+ reference.id,
181
+ previous && compareUnicode(previous.bibtex, reference.bibtex) <= 0 ? previous : reference,
182
+ );
183
+ ids.add(reference.id);
184
+ }
185
+
186
+ idsByGuideline.set(guideline.id, Object.freeze([...ids].sort(compareUnicode)));
187
+ }
188
+
189
+ const created = Object.freeze({ byId, idsByGuideline });
190
+ indexes.set(catalog, created);
191
+
192
+ return created;
193
+ }
194
+
195
+ function parseReference(bibtex: string): ParsedReference {
196
+ try {
197
+ return parsedReference(bibtex);
198
+ } catch (error) {
199
+ if (error instanceof CatalogError) throw error;
200
+
201
+ const details: JsonObject = {
202
+ exception_type: error instanceof Error ? error.name : "Error",
203
+ };
204
+
205
+ if (error instanceof ParseError) {
206
+ details.diagnostics = error.diagnostics.map((diagnostic) => ({ ...diagnostic }));
207
+ }
208
+
209
+ throw new CatalogError("BibTeX reference could not be parsed.", {
210
+ details,
211
+ });
212
+ }
213
+ }
214
+
215
+ function parsedReference(bibtex: string): ParsedReference {
216
+ const document = BibDocument.parse(bibtex);
217
+ const entries = document.resolve();
218
+
219
+ if (entries.length !== 1) {
220
+ throw new CatalogError(`Expected one BibTeX entry, found ${entries.length}.`);
221
+ }
222
+
223
+ const entry = entries[0]!;
224
+ const properties = entry.fields;
225
+ const authorsText = textOrNull(properties.author);
226
+
227
+ const authors = Object.freeze(
228
+ authorsText === null
229
+ ? []
230
+ : authorsText
231
+ .split(" and ")
232
+ .map((author) => author.trim())
233
+ .filter(Boolean),
234
+ );
235
+
236
+ return Object.freeze({
237
+ id: entry.key,
238
+ bibtex,
239
+ sourceType: entry.entryType,
240
+ authors,
241
+ authorsText,
242
+ year: textOrNull(properties.year),
243
+ title: textOrNull(properties.title),
244
+ journal: textOrNull(properties.journal),
245
+ booktitle: textOrNull(properties.booktitle),
246
+ publisher: textOrNull(properties.publisher),
247
+ url: referenceUrl(properties),
248
+ doi: textOrNull(properties.doi),
249
+ citation: new Document(
250
+ Library.parseBibtex(document.tidy().bibtex, { recovery: "report" }),
251
+ citationStyle,
252
+ {
253
+ locale: "en-US",
254
+ },
255
+ ).fullBibliography().text,
256
+ fingerprint: canonicalJson({
257
+ type: entry.entryType,
258
+ label: entry.key,
259
+ properties: { ...properties },
260
+ }),
261
+ });
262
+ }
263
+
264
+ function minimalSource(reference: ParsedReference): MinimalSourceRecord {
265
+ return Object.freeze({
266
+ reference_id: reference.id,
267
+ authors_text: reference.authorsText,
268
+ year: reference.year,
269
+ source_title: reference.title,
270
+ doi: reference.doi,
271
+ url: reference.url,
272
+ });
273
+ }
274
+
275
+ function fullSource(guidelineId: string, reference: ParsedReference): FullSourceRecord {
276
+ return Object.freeze({
277
+ guideline_id: guidelineId,
278
+ reference_id: reference.id,
279
+ source_type: reference.sourceType,
280
+ authors: reference.authors,
281
+ authors_text: reference.authorsText,
282
+ year: reference.year,
283
+ source_title: reference.title,
284
+ journal: reference.journal,
285
+ booktitle: reference.booktitle,
286
+ publisher: reference.publisher,
287
+ url: reference.url,
288
+ doi: reference.doi,
289
+ });
290
+ }
291
+
292
+ function citationSource(reference: ParsedReference): CitationSource {
293
+ const source = {
294
+ reference_id: reference.id,
295
+ source_type: reference.sourceType,
296
+ authors_text: reference.authorsText,
297
+ year: reference.year,
298
+ source_title: reference.title,
299
+ journal: reference.journal,
300
+ booktitle: reference.booktitle,
301
+ publisher: reference.publisher,
302
+ doi: reference.doi,
303
+ url: reference.url,
304
+ };
305
+
306
+ const locators = [source.doi ? doiUrl(source.doi) : null, source.url].filter(
307
+ (value): value is string => value !== null && !reference.citation.includes(value),
308
+ );
309
+
310
+ return Object.freeze({
311
+ ...source,
312
+ citation: [reference.citation, ...new Set(locators)].join(" "),
313
+ });
314
+ }
315
+
316
+ function textOrNull(value: string | undefined): string | null {
317
+ if (value === undefined) return null;
318
+ const text = value.trim();
319
+
320
+ return text || null;
321
+ }
322
+
323
+ function referenceUrl(properties: Readonly<Record<string, string>>): string | null {
324
+ const explicit = textOrNull(properties.url);
325
+ const value = explicit ?? textOrNull(properties.howpublished);
326
+
327
+ if (value === null) return null;
328
+ const wrapped = /^\\url\{([^{}]*)\}$/.exec(value);
329
+ const candidate = wrapped?.[1] ?? value;
330
+
331
+ return /^https?:\/\/[^\s{}\\/?#]+[^\s{}\\]*$/i.test(candidate) ? candidate : explicit;
332
+ }
333
+
334
+ function doiUrl(value: string): string {
335
+ return `https://doi.org/${value.replace(/^https?:\/\/doi\.org\//, "")}`;
336
+ }
@@ -0,0 +1,74 @@
1
+ import type { Catalog } from "./model";
2
+ import { CatalogError } from "./errors";
3
+ import { isJsonString } from "./json";
4
+ import { tableNames, tableSchemas, type CatalogTables, type TableColumnInfo } from "./tables";
5
+
6
+ export type RegisterCatalogOptions = Readonly<{ ids?: readonly string[] }>;
7
+
8
+ export function registrationPlan(
9
+ catalog: Catalog,
10
+ options: RegisterCatalogOptions,
11
+ source: "parameter" | "file",
12
+ ): readonly Readonly<{ sql: string; rows: string }>[] {
13
+ if (Object.prototype.toString.call(options) !== "[object Object]")
14
+ throw new CatalogError("Registration options must be an object.");
15
+
16
+ if (
17
+ options.ids !== undefined &&
18
+ (!Array.isArray(options.ids) || !options.ids.every(isJsonString))
19
+ )
20
+ throw new CatalogError("ids must be an array of strings.");
21
+ const ids = options.ids === undefined ? undefined : new Set(options.ids);
22
+
23
+ for (const id of ids ?? []) catalog.require(id);
24
+ const tables = selectedTables(catalog, ids);
25
+ const json = source === "file" ? "(SELECT content FROM read_text(?))" : "?";
26
+
27
+ return tableNames.map((name) => {
28
+ const columns = tableSchemas[name]
29
+ .map(({ name, type }) => `"${name}" ${sqlType(type)}`)
30
+ .join(", ");
31
+
32
+ return {
33
+ sql: `CREATE OR REPLACE TABLE "${name}" AS SELECT record.*
34
+ FROM UNNEST(CAST(CAST(${json} AS JSON) AS STRUCT(${columns})[])) AS records(record)`,
35
+ rows: JSON.stringify(tables[name]),
36
+ };
37
+ });
38
+ }
39
+
40
+ function selectedTables(catalog: Catalog, ids: ReadonlySet<string> | undefined): CatalogTables {
41
+ const links = catalog.table("guideline_references");
42
+ const selectedLinks = ids ? links.filter(({ guideline_id }) => ids.has(guideline_id)) : links;
43
+ const references = new Set(selectedLinks.map(({ reference_id }) => reference_id));
44
+
45
+ return {
46
+ guidelines: ids
47
+ ? catalog.table("guidelines").filter(({ id }) => ids.has(id))
48
+ : catalog.table("guidelines"),
49
+ sections: ids
50
+ ? catalog.table("sections").filter(({ guideline_id }) => ids.has(guideline_id))
51
+ : catalog.table("sections"),
52
+ guideline_labels: ids
53
+ ? catalog.table("guideline_labels").filter(({ guideline_id }) => ids.has(guideline_id))
54
+ : catalog.table("guideline_labels"),
55
+ references: ids
56
+ ? catalog.table("references").filter(({ id }) => references.has(id))
57
+ : catalog.table("references"),
58
+ guideline_references: selectedLinks,
59
+ guideline_sources: ids
60
+ ? catalog.table("guideline_sources").filter(({ guideline_id }) => ids.has(guideline_id))
61
+ : catalog.table("guideline_sources"),
62
+ };
63
+ }
64
+
65
+ function sqlType(type: TableColumnInfo["type"]): string {
66
+ switch (type) {
67
+ case "String":
68
+ return "VARCHAR";
69
+ case "List(String)":
70
+ return "VARCHAR[]";
71
+ case "List(Struct({'role': String, 'title': String, 'content': String}))":
72
+ return "STRUCT(role VARCHAR, title VARCHAR, content VARCHAR)[]";
73
+ }
74
+ }
@@ -0,0 +1,250 @@
1
+ import { CatalogError } from "./errors";
2
+ import { compareUnicode } from "./identity";
3
+ import { parseLabel } from "./labels";
4
+ import type { Catalog, Guideline, GuidelineSection } from "./model";
5
+ import { referenceIndex } from "./references";
6
+
7
+ type ReferenceRow = Readonly<{
8
+ id: string;
9
+ source_type: string | null;
10
+ authors: readonly string[];
11
+ authors_text: string | null;
12
+ year: string | null;
13
+ title: string | null;
14
+ journal: string | null;
15
+ booktitle: string | null;
16
+ publisher: string | null;
17
+ url: string | null;
18
+ doi: string | null;
19
+ bibtex: string;
20
+ }>;
21
+
22
+ type GuidelineReferenceRow = Readonly<{
23
+ guideline_id: string;
24
+ reference_id: string;
25
+ }>;
26
+
27
+ export type CatalogTables = Readonly<{
28
+ guidelines: readonly Omit<Guideline, "references">[];
29
+ sections: readonly Readonly<GuidelineSection & { guideline_id: string }>[];
30
+ guideline_labels: readonly Readonly<{
31
+ guideline_id: string;
32
+ label: string;
33
+ family: string;
34
+ category: string;
35
+ modifier: string | null;
36
+ }>[];
37
+ references: readonly ReferenceRow[];
38
+ guideline_references: readonly GuidelineReferenceRow[];
39
+ guideline_sources: readonly Readonly<
40
+ Omit<ReferenceRow, "id" | "title"> & GuidelineReferenceRow & { source_title: string | null }
41
+ >[];
42
+ }>;
43
+
44
+ export type TableName = keyof CatalogTables;
45
+
46
+ export type TableColumnInfo = Readonly<{
47
+ name: string;
48
+ type:
49
+ | "String"
50
+ | "List(String)"
51
+ | "List(Struct({'role': String, 'title': String, 'content': String}))";
52
+ }>;
53
+
54
+ export type TableInfo = Readonly<{
55
+ name: TableName;
56
+ rows: number;
57
+ columns: readonly TableColumnInfo[];
58
+ }>;
59
+
60
+ function columns(
61
+ schema: Readonly<Record<string, TableColumnInfo["type"]>>,
62
+ ): readonly TableColumnInfo[] {
63
+ return Object.freeze(Object.entries(schema).map(([name, type]) => Object.freeze({ name, type })));
64
+ }
65
+
66
+ export const tableSchemas: Readonly<Record<TableName, readonly TableColumnInfo[]>> = Object.freeze({
67
+ guidelines: columns({
68
+ id: "String",
69
+ title: "String",
70
+ description: "String",
71
+ labels: "List(String)",
72
+ body: "String",
73
+ sections: "List(Struct({'role': String, 'title': String, 'content': String}))",
74
+ }),
75
+ sections: columns({ guideline_id: "String", role: "String", title: "String", content: "String" }),
76
+ guideline_labels: columns({
77
+ guideline_id: "String",
78
+ label: "String",
79
+ family: "String",
80
+ category: "String",
81
+ modifier: "String",
82
+ }),
83
+ references: columns({
84
+ id: "String",
85
+ source_type: "String",
86
+ authors: "List(String)",
87
+ authors_text: "String",
88
+ year: "String",
89
+ title: "String",
90
+ journal: "String",
91
+ booktitle: "String",
92
+ publisher: "String",
93
+ url: "String",
94
+ doi: "String",
95
+ bibtex: "String",
96
+ }),
97
+ guideline_references: columns({ guideline_id: "String", reference_id: "String" }),
98
+ guideline_sources: columns({
99
+ guideline_id: "String",
100
+ reference_id: "String",
101
+ source_type: "String",
102
+ authors: "List(String)",
103
+ authors_text: "String",
104
+ year: "String",
105
+ source_title: "String",
106
+ journal: "String",
107
+ booktitle: "String",
108
+ publisher: "String",
109
+ url: "String",
110
+ doi: "String",
111
+ bibtex: "String",
112
+ }),
113
+ });
114
+
115
+ export const tableNames: readonly TableName[] = Object.freeze<TableName[]>([
116
+ "guidelines",
117
+ "sections",
118
+ "guideline_labels",
119
+ "references",
120
+ "guideline_references",
121
+ "guideline_sources",
122
+ ]);
123
+
124
+ const tablesByCatalog = new WeakMap<Catalog, Map<TableName, CatalogTables[TableName]>>();
125
+
126
+ export function catalogTable<Name extends TableName>(
127
+ catalog: Catalog,
128
+ name: Name,
129
+ ): CatalogTables[Name] {
130
+ if (!Object.hasOwn(tableSchemas, name)) {
131
+ throw new CatalogError(`Unknown table: ${name}`, {
132
+ code: "lookup",
133
+ details: { table: name, available: tableNames },
134
+ hints: ["Call catalog.describe() to inspect catalog tables."],
135
+ });
136
+ }
137
+
138
+ let tables = tablesByCatalog.get(catalog);
139
+
140
+ if (!tables) {
141
+ tables = new Map();
142
+ tablesByCatalog.set(catalog, tables);
143
+ }
144
+
145
+ let rows = tables.get(name);
146
+
147
+ if (!rows) {
148
+ rows = tableBuilders[name](catalog);
149
+ Object.freeze(rows);
150
+ tables.set(name, rows);
151
+ }
152
+
153
+ // SAFETY: Each cached value comes from the builder indexed by the same validated table name.
154
+ return rows as CatalogTables[Name];
155
+ }
156
+
157
+ export function catalogTableInfo(catalog: Catalog): readonly TableInfo[] {
158
+ return Object.freeze(
159
+ tableNames.map((name) =>
160
+ Object.freeze({
161
+ name,
162
+ rows: catalog.table(name).length,
163
+ columns: tableSchemas[name],
164
+ }),
165
+ ),
166
+ );
167
+ }
168
+
169
+ const tableBuilders: { [Name in TableName]: (catalog: Catalog) => CatalogTables[Name] } = {
170
+ guidelines: (catalog) =>
171
+ catalog.guidelines.map(({ id, title, description, labels, body, sections }) =>
172
+ Object.freeze({ id, title, description, labels, body, sections }),
173
+ ),
174
+ sections: (catalog) =>
175
+ catalog.guidelines.flatMap((guideline) =>
176
+ guideline.sections.map((section) =>
177
+ Object.freeze({ guideline_id: guideline.id, ...section }),
178
+ ),
179
+ ),
180
+ guideline_labels: (catalog) =>
181
+ catalog.guidelines
182
+ .flatMap((guideline) =>
183
+ [...new Set(guideline.labels)].map((label) => {
184
+ const parsed = parseLabel(label);
185
+
186
+ return Object.freeze({
187
+ guideline_id: guideline.id,
188
+ label,
189
+ family: parsed.family,
190
+ category: parsed.category,
191
+ modifier: parsed.modifier ?? null,
192
+ });
193
+ }),
194
+ )
195
+ .sort(
196
+ (left, right) =>
197
+ compareUnicode(left.guideline_id, right.guideline_id) ||
198
+ compareUnicode(left.label, right.label),
199
+ ),
200
+ references: (catalog) =>
201
+ [...referenceIndex(catalog).byId.values()]
202
+ .sort((left, right) => compareUnicode(left.id, right.id))
203
+ .map((reference) =>
204
+ Object.freeze({
205
+ id: reference.id,
206
+ source_type: reference.sourceType,
207
+ authors: reference.authors,
208
+ authors_text: reference.authorsText,
209
+ year: reference.year,
210
+ title: reference.title,
211
+ journal: reference.journal,
212
+ booktitle: reference.booktitle,
213
+ publisher: reference.publisher,
214
+ url: reference.url,
215
+ doi: reference.doi,
216
+ bibtex: reference.bibtex,
217
+ }),
218
+ ),
219
+ guideline_references: (catalog) =>
220
+ [...referenceIndex(catalog).idsByGuideline]
221
+ .sort(([left], [right]) => compareUnicode(left, right))
222
+ .flatMap(([guideline_id, ids]) =>
223
+ ids.map((reference_id) => Object.freeze({ guideline_id, reference_id })),
224
+ ),
225
+ guideline_sources: (catalog) => {
226
+ const references = new Map(
227
+ catalog.table("references").map((reference) => [reference.id, reference]),
228
+ );
229
+
230
+ return catalog.table("guideline_references").map(({ guideline_id, reference_id }) => {
231
+ const reference = references.get(reference_id)!;
232
+
233
+ return Object.freeze({
234
+ guideline_id,
235
+ reference_id,
236
+ source_type: reference.source_type,
237
+ authors: reference.authors,
238
+ authors_text: reference.authors_text,
239
+ year: reference.year,
240
+ source_title: reference.title,
241
+ journal: reference.journal,
242
+ booktitle: reference.booktitle,
243
+ publisher: reference.publisher,
244
+ url: reference.url,
245
+ doi: reference.doi,
246
+ bibtex: reference.bibtex,
247
+ });
248
+ });
249
+ },
250
+ };