structured-context 0.11.0 → 0.12.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 (87) hide show
  1. package/README.md +63 -22
  2. package/dist/api.d.ts +7 -5
  3. package/dist/api.js +4 -1
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/dump.d.ts +15 -2
  7. package/dist/commands/dump.js +18 -4
  8. package/dist/commands/render.d.ts +4 -0
  9. package/dist/commands/render.js +64 -9
  10. package/dist/commands/schemas.d.ts +2 -2
  11. package/dist/commands/schemas.js +114 -22
  12. package/dist/commands/show.d.ts +1 -0
  13. package/dist/commands/show.js +7 -1
  14. package/dist/commands/spaces.d.ts +3 -1
  15. package/dist/commands/spaces.js +11 -2
  16. package/dist/commands/validate-file.js +7 -4
  17. package/dist/commands/validate.js +38 -21
  18. package/dist/config.d.ts +18 -1
  19. package/dist/config.js +81 -10
  20. package/dist/filter/augment-nodes.d.ts +3 -1
  21. package/dist/filter/augment-nodes.js +12 -7
  22. package/dist/filter/filter-nodes.js +1 -1
  23. package/dist/index.js +39 -15
  24. package/dist/load-space-graph.d.ts +4 -2
  25. package/dist/load-space-graph.js +15 -4
  26. package/dist/plugins/loader.js +2 -2
  27. package/dist/plugins/markdown/extract-content-links.js +8 -29
  28. package/dist/plugins/markdown/index.js +8 -2
  29. package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
  30. package/dist/plugins/markdown/parse-embedded.js +41 -7
  31. package/dist/plugins/markdown/read-space.js +38 -5
  32. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  33. package/dist/plugins/markdown/render-directory.js +130 -0
  34. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  35. package/dist/plugins/miro/sync.js +7 -6
  36. package/dist/plugins/util.d.ts +39 -1
  37. package/dist/plugins/util.js +139 -0
  38. package/dist/read/resolve-graph-edges.d.ts +1 -1
  39. package/dist/read/resolve-graph-edges.js +46 -27
  40. package/dist/read/wikilink-utils.d.ts +17 -0
  41. package/dist/read/wikilink-utils.js +40 -0
  42. package/dist/render/render.d.ts +3 -1
  43. package/dist/render/render.js +2 -2
  44. package/dist/schema/meta-schemas.d.ts +13 -0
  45. package/dist/schema/meta-schemas.js +50 -0
  46. package/dist/schema/metadata-contract.d.ts +343 -187
  47. package/dist/schema/metadata-contract.js +68 -30
  48. package/dist/schema/schema.d.ts +14 -8
  49. package/dist/schema/schema.js +277 -198
  50. package/dist/schema/validate-graph.d.ts +2 -1
  51. package/dist/schema/validate-graph.js +40 -23
  52. package/dist/schema/validate-rules.d.ts +5 -1
  53. package/dist/schema/validate-rules.js +20 -1
  54. package/dist/schemas/_knowledge_wiki.json +20 -20
  55. package/dist/schemas/_ost_strict.json +2 -2
  56. package/dist/schemas/_sctx_base.json +2 -2
  57. package/dist/schemas/_strategy_general.json +38 -38
  58. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  59. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  60. package/dist/schemas/knowledge_wiki.json +10 -9
  61. package/dist/schemas/strategy_general.json +15 -14
  62. package/dist/schemas/strict_ost.json +17 -16
  63. package/dist/space-context.d.ts +3 -0
  64. package/dist/space-context.js +7 -2
  65. package/dist/space-graph.d.ts +9 -7
  66. package/dist/space-graph.js +7 -5
  67. package/dist/testing.d.ts +33 -0
  68. package/dist/testing.js +72 -0
  69. package/dist/types.d.ts +54 -13
  70. package/dist/validate.d.ts +3 -0
  71. package/dist/validate.js +19 -9
  72. package/docs/concepts.md +14 -4
  73. package/docs/config.md +8 -0
  74. package/docs/rules.md +49 -22
  75. package/docs/schemas.md +110 -35
  76. package/package.json +13 -8
  77. package/schemas/_knowledge_wiki.json +20 -20
  78. package/schemas/_ost_strict.json +2 -2
  79. package/schemas/_sctx_base.json +2 -2
  80. package/schemas/_strategy_general.json +38 -38
  81. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  82. package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  83. package/schemas/knowledge_wiki.json +10 -9
  84. package/schemas/strategy_general.json +15 -14
  85. package/schemas/strict_ost.json +17 -16
  86. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  87. package/schemas/generated/_structured_context_schema_meta.json +0 -191
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
1
4
  import { PLUGIN_PREFIX as _PLUGIN_PREFIX } from '../constants';
2
5
  export const PLUGIN_PREFIX = _PLUGIN_PREFIX;
3
6
  export const CONFIG_PLUGINS_DIR = 'plugins';
@@ -9,3 +12,139 @@ export function normalizePluginName(name) {
9
12
  export function shortenPluginName(name) {
10
13
  return name.startsWith(PLUGIN_PREFIX) ? name.slice(PLUGIN_PREFIX.length) : name;
11
14
  }
15
+ /** Type guard for multi-file render result. */
16
+ export function isRenderMultiFileOutput(result) {
17
+ return typeof result === 'object' && result !== null && Array.isArray(result.files);
18
+ }
19
+ /**
20
+ * Synchronously locate the root directory of an external plugin.
21
+ * Resolution order:
22
+ * 1. Config-adjacent: {configDir}/plugins/{name} or {configDir}/plugins/sctx-{name}
23
+ * 2. Node module: resolve from configDir using standard module resolution
24
+ */
25
+ export function resolvePluginDir(pluginName, configDir) {
26
+ const normalized = normalizePluginName(pluginName);
27
+ const short = shortenPluginName(pluginName);
28
+ for (const candidate of [normalized, short]) {
29
+ const localDir = resolve(configDir, CONFIG_PLUGINS_DIR, candidate);
30
+ if (existsSync(localDir)) {
31
+ try {
32
+ if (statSync(localDir).isDirectory()) {
33
+ return localDir;
34
+ }
35
+ }
36
+ catch { }
37
+ }
38
+ }
39
+ // Node module resolution from configDir
40
+ for (const candidate of [normalized, short]) {
41
+ try {
42
+ const req = createRequire(join(configDir, 'package.json'));
43
+ const pkgPath = req.resolve(`${candidate}/package.json`);
44
+ return dirname(pkgPath);
45
+ }
46
+ catch {
47
+ const nmCandidate = resolve(configDir, 'node_modules', candidate);
48
+ if (existsSync(nmCandidate)) {
49
+ try {
50
+ if (statSync(nmCandidate).isDirectory()) {
51
+ return nmCandidate;
52
+ }
53
+ }
54
+ catch { }
55
+ }
56
+ }
57
+ }
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Synchronously locate the schemas directory for a plugin.
62
+ * Convention:
63
+ * 1. package.json "sctx": { "schemas": "..." }
64
+ * 2. Convention: <pluginDir>/schemas
65
+ * 3. Fallback for test fixtures: {configDir}/plugins/schemas
66
+ */
67
+ export function resolvePluginSchemasDir(pluginName, configDir) {
68
+ const dir = resolvePluginDir(pluginName, configDir);
69
+ if (dir) {
70
+ // 1. package.json sctx.schemas
71
+ const pkgJsonPath = join(dir, 'package.json');
72
+ if (existsSync(pkgJsonPath)) {
73
+ try {
74
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
75
+ if (pkg.sctx?.schemas && typeof pkg.sctx.schemas === 'string') {
76
+ const customSchemas = resolve(dir, pkg.sctx.schemas);
77
+ if (existsSync(customSchemas) && statSync(customSchemas).isDirectory()) {
78
+ return customSchemas;
79
+ }
80
+ }
81
+ }
82
+ catch { }
83
+ }
84
+ // 2. Convention: <pluginDir>/schemas
85
+ const conventionDir = join(dir, 'schemas');
86
+ if (existsSync(conventionDir)) {
87
+ try {
88
+ if (statSync(conventionDir).isDirectory()) {
89
+ return conventionDir;
90
+ }
91
+ }
92
+ catch { }
93
+ }
94
+ }
95
+ // 3. Check config-adjacent plugins/schemas as fallback (e.g. test fixtures)
96
+ const fallbackSchemas = resolve(configDir, CONFIG_PLUGINS_DIR, 'schemas');
97
+ if (existsSync(fallbackSchemas)) {
98
+ try {
99
+ if (statSync(fallbackSchemas).isDirectory()) {
100
+ return fallbackSchemas;
101
+ }
102
+ }
103
+ catch { }
104
+ }
105
+ return undefined;
106
+ }
107
+ /**
108
+ * Synchronously collect all plugin schema sources available to a space.
109
+ */
110
+ export function resolveSpacePluginSchemas(space, config, configDir) {
111
+ const pluginSources = [];
112
+ const seenPlugins = new Set();
113
+ const maybeAddPlugin = (name) => {
114
+ const shortName = shortenPluginName(name);
115
+ if (seenPlugins.has(shortName))
116
+ return;
117
+ seenPlugins.add(shortName);
118
+ const schemasDir = resolvePluginSchemasDir(name, configDir);
119
+ if (schemasDir) {
120
+ pluginSources.push({
121
+ pluginName: shortName,
122
+ schemasDir,
123
+ });
124
+ }
125
+ };
126
+ // 1. Plugin referenced by space.schema prefix if any (e.g. sctx-wardley-mapping/wardley_map.json)
127
+ const rawSchema = space?.schema ?? config.schema;
128
+ if (rawSchema && !isAbsolute(rawSchema) && !rawSchema.startsWith('.')) {
129
+ const normalized = rawSchema.replace(/\\/g, '/');
130
+ const slashIdx = normalized.indexOf('/');
131
+ if (slashIdx !== -1) {
132
+ const prefix = normalized.slice(0, slashIdx);
133
+ maybeAddPlugin(prefix);
134
+ }
135
+ }
136
+ // 2. Plugins configured on space
137
+ if (space?.plugins) {
138
+ for (const name of Object.keys(space.plugins)) {
139
+ maybeAddPlugin(name);
140
+ }
141
+ }
142
+ // 3. Plugins configured globally
143
+ const globalPlugins = config.plugins;
144
+ if (globalPlugins) {
145
+ for (const name of Object.keys(globalPlugins)) {
146
+ maybeAddPlugin(name);
147
+ }
148
+ }
149
+ return pluginSources;
150
+ }
@@ -1,7 +1,7 @@
1
1
  import type { BaseNode, SchemaMetadata, SpaceNode, UnresolvedRef } from '../types';
2
2
  /**
3
3
  * Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
4
- * parent links using the hierarchy levels and relationships from schema metadata.
4
+ * parent links using the levels of every hierarchy and the relationships from schema metadata.
5
5
  *
6
6
  * Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
7
7
  */
@@ -21,19 +21,27 @@ function getRefs(rawField, multiple) {
21
21
  * @param nodesByType Map of node type to nodes
22
22
  * @param targetIndex Map of link targets to nodes
23
23
  * @param edge The edge definition (child type, parent type, field, fieldOn, multiple)
24
- * @param source Whether this edge comes from hierarchy.levels or relationships
24
+ * @param source Whether this edge comes from hierarchy levels or relationships
25
+ * @param hierarchy Name of the hierarchy the edge belongs to (hierarchy edges only)
25
26
  * @param selfRef Whether child and parent are the same node type
26
27
  * @param unresolvedRefs Array to push broken/invalid link entries into
27
28
  * @param typeAliases Optional type aliases for resolution
28
29
  */
29
- function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolvedRefs, typeAliases) {
30
+ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef, unresolvedRefs, typeAliases) {
30
31
  const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
31
32
  const childType = resolveNodeType(rawChildType, typeAliases);
32
33
  const parentType = resolveNodeType(rawParentType, typeAliases);
33
34
  function pushParentRef(childNode, parentTitle) {
34
35
  // Deduplicate by (field, title) — same parent via different fields is intentional
35
36
  if (!childNode.resolvedParents.some((r) => r.field === field && r.title === parentTitle)) {
36
- childNode.resolvedParents.push({ title: parentTitle, field, fieldOn, source, selfRef });
37
+ childNode.resolvedParents.push({
38
+ title: parentTitle,
39
+ field,
40
+ fieldOn,
41
+ source,
42
+ ...(hierarchy !== undefined ? { hierarchy } : {}),
43
+ selfRef,
44
+ });
37
45
  }
38
46
  }
39
47
  function checkShape(ownerNode, rawField) {
@@ -54,6 +62,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
54
62
  field,
55
63
  reason: 'invalid_shape',
56
64
  message: `Field "${field}" must be a wikilink string, got ${typeof rawField}`,
65
+ source: ownerNode.source,
57
66
  });
58
67
  return false;
59
68
  }
@@ -69,6 +78,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
69
78
  field,
70
79
  reason: 'not_found',
71
80
  message: `Link target "${target}" in field "${field}" not found`,
81
+ source: ownerNode.source,
72
82
  });
73
83
  return null;
74
84
  }
@@ -79,6 +89,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
79
89
  field,
80
90
  reason: 'ambiguous',
81
91
  message: `Link target "${target}" in field "${field}" is ambiguous (matches multiple nodes)`,
92
+ source: ownerNode.source,
82
93
  });
83
94
  return null;
84
95
  }
@@ -155,12 +166,12 @@ function classifyContentLink(link, targetIndex) {
155
166
  }
156
167
  /**
157
168
  * Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
158
- * parent links using the hierarchy levels and relationships from schema metadata.
169
+ * parent links using the levels of every hierarchy and the relationships from schema metadata.
159
170
  *
160
171
  * Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
161
172
  */
162
173
  export function resolveGraphEdges(nodes, metadata) {
163
- const levels = metadata.hierarchy?.levels ?? [];
174
+ const hierarchies = metadata.hierarchies ?? [];
164
175
  const relationships = metadata.relationships ?? [];
165
176
  const typeAliases = metadata.typeAliases;
166
177
  const unresolvedRefs = [];
@@ -180,27 +191,35 @@ export function resolveGraphEdges(nodes, metadata) {
180
191
  }
181
192
  nodesByType.get(type).push(node);
182
193
  }
183
- // 1. Process hierarchy levels
184
- for (let i = 0; i < levels.length; i++) {
185
- const level = levels[i];
186
- // Regular relationship (child type → parent type)
187
- if (i > 0) {
188
- const parentLevel = levels[i - 1];
189
- resolveEdge(nodesByType, targetIndex, {
190
- type: level.type,
191
- parent: parentLevel.type,
192
- field: level.field,
193
- fieldOn: level.fieldOn,
194
- multiple: level.multiple,
195
- }, 'hierarchy', false, unresolvedRefs, typeAliases);
196
- }
197
- // Same-type relationship (child type → same type) via primary field
198
- if (level.selfRef) {
199
- resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.field, fieldOn: level.fieldOn, multiple: level.multiple }, 'hierarchy', true, unresolvedRefs, typeAliases);
200
- }
201
- // Same-type relationship via explicit selfRefField
202
- if (level.selfRefField) {
203
- resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', true, unresolvedRefs, typeAliases);
194
+ // 1. Process the levels of each hierarchy
195
+ for (const { name, levels } of hierarchies) {
196
+ for (let i = 0; i < levels.length; i++) {
197
+ const level = levels[i];
198
+ // Regular relationship (child type → parent type)
199
+ if (i > 0) {
200
+ const parentLevel = levels[i - 1];
201
+ resolveEdge(nodesByType, targetIndex, {
202
+ type: level.type,
203
+ parent: parentLevel.type,
204
+ field: level.field,
205
+ fieldOn: level.fieldOn,
206
+ multiple: level.multiple,
207
+ }, 'hierarchy', name, false, unresolvedRefs, typeAliases);
208
+ }
209
+ // Same-type relationship (child type → same type) via primary field
210
+ if (level.selfRef) {
211
+ resolveEdge(nodesByType, targetIndex, {
212
+ type: level.type,
213
+ parent: level.type,
214
+ field: level.field,
215
+ fieldOn: level.fieldOn,
216
+ multiple: level.multiple,
217
+ }, 'hierarchy', name, true, unresolvedRefs, typeAliases);
218
+ }
219
+ // Same-type relationship via explicit selfRefField
220
+ if (level.selfRefField) {
221
+ resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', name, true, unresolvedRefs, typeAliases);
222
+ }
204
223
  }
205
224
  }
206
225
  // 2. Process additional relationships
@@ -212,7 +231,7 @@ export function resolveGraphEdges(nodes, metadata) {
212
231
  fieldOn: rel.fieldOn,
213
232
  multiple: rel.multiple,
214
233
  };
215
- resolveEdge(nodesByType, targetIndex, edge, 'relationship', rel.type === rel.parent, unresolvedRefs, typeAliases);
234
+ resolveEdge(nodesByType, targetIndex, edge, 'relationship', undefined, rel.type === rel.parent, unresolvedRefs, typeAliases);
216
235
  }
217
236
  // 3. Resolve content links — classify each raw ContentLink into a ResolvedContentLink.
218
237
  for (const node of spaceNodes) {
@@ -1,4 +1,21 @@
1
1
  import type { BaseNode } from '../types';
2
+ export interface ParsedWikilink {
3
+ target: string;
4
+ anchor?: string;
5
+ displayText?: string;
6
+ }
7
+ /**
8
+ * Format a wikilink string.
9
+ * formatWikilink('My Goal') -> '[[My Goal]]'
10
+ * formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
11
+ */
12
+ export declare function formatWikilink(target: string, alias?: string): string;
13
+ /**
14
+ * Parse a wikilink or link inner string into target, anchor, and displayText.
15
+ * Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
16
+ * Also handles bare forms without enclosing brackets.
17
+ */
18
+ export declare function parseWikilink(wikilink: string): ParsedWikilink;
2
19
  /**
3
20
  * Extract the lookup key from a wikilink string such as:
4
21
  * [[Personal Vision]] → "Personal Vision"
@@ -1,3 +1,43 @@
1
+ /**
2
+ * Format a wikilink string.
3
+ * formatWikilink('My Goal') -> '[[My Goal]]'
4
+ * formatWikilink('My Goal', 'Custom Display') -> '[[My Goal|Custom Display]]'
5
+ */
6
+ export function formatWikilink(target, alias) {
7
+ const trimmedTarget = target.trim();
8
+ const trimmedAlias = alias?.trim();
9
+ if (trimmedAlias && trimmedAlias !== trimmedTarget) {
10
+ return `[[${trimmedTarget}|${trimmedAlias}]]`;
11
+ }
12
+ return `[[${trimmedTarget}]]`;
13
+ }
14
+ /**
15
+ * Parse a wikilink or link inner string into target, anchor, and displayText.
16
+ * Handles: [[target]], [[target#anchor]], [[target|alias]], [[target#anchor|alias]]
17
+ * Also handles bare forms without enclosing brackets.
18
+ */
19
+ export function parseWikilink(wikilink) {
20
+ const cleaned = wikilink.replace(/^"|"$/g, '').trim();
21
+ const inner = cleaned.startsWith('[[') && cleaned.endsWith(']]') ? cleaned.slice(2, -2).trim() : cleaned;
22
+ // Alias: [[target|alias]] or [[target#anchor|alias]]
23
+ const pipeIdx = inner.indexOf('|');
24
+ let core = inner;
25
+ let displayText;
26
+ if (pipeIdx >= 0) {
27
+ core = inner.slice(0, pipeIdx);
28
+ const alias = inner.slice(pipeIdx + 1).trim();
29
+ if (alias)
30
+ displayText = alias;
31
+ }
32
+ // Anchor: [[target#anchor]] or [[target#^block]]
33
+ const hashIdx = core.indexOf('#');
34
+ if (hashIdx >= 0) {
35
+ const target = core.slice(0, hashIdx).trim();
36
+ const anchor = core.slice(hashIdx + 1).trim() || undefined;
37
+ return { target, anchor, displayText };
38
+ }
39
+ return { target: core.trim(), displayText };
40
+ }
1
41
  /**
2
42
  * Extract the lookup key from a wikilink string such as:
3
43
  * [[Personal Vision]] → "Personal Vision"
@@ -1,5 +1,7 @@
1
+ import { type RenderResult } from '../plugins/util';
1
2
  import type { SpaceContext } from '../types';
2
3
  export declare function executeRender(formatName: string, context: SpaceContext, options: {
3
4
  filter?: string;
5
+ hierarchy?: string;
4
6
  data?: Record<string, unknown>;
5
- }): Promise<string>;
7
+ }): Promise<RenderResult>;
@@ -1,4 +1,4 @@
1
- import { shortenPluginName } from '@/plugins/util';
1
+ import { shortenPluginName } from '../plugins/util';
2
2
  import { updateSpaceField } from '../config';
3
3
  import { assembleSpaceGraph } from '../load-space-graph';
4
4
  import { loadPlugins } from '../plugins/loader';
@@ -12,7 +12,7 @@ export async function executeRender(formatName, context, options) {
12
12
  const available = registry.map((r) => r.qualifiedName).join(', ');
13
13
  throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
14
14
  }
15
- const graph = await assembleSpaceGraph(context, { filter: options.filter });
15
+ const graph = await assembleSpaceGraph(context, { filter: options.filter, hierarchy: options.hierarchy });
16
16
  const shortName = shortenPluginName(entry.plugin.plugin.name);
17
17
  const pluginContext = {
18
18
  ...context,
@@ -0,0 +1,13 @@
1
+ import type { AnySchemaObject } from 'ajv';
2
+ /** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
3
+ export declare function listMetaVersions(generatedDir: string): string[];
4
+ /** The newest released meta-schema and its version folder, if any. */
5
+ export declare function readNewestMetaSchema(generatedDir: string): {
6
+ version: string;
7
+ schema: AnySchemaObject;
8
+ } | undefined;
9
+ /**
10
+ * Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
11
+ * `latest` registered under the `latest` URL so `$schema` references to either form resolve.
12
+ */
13
+ export declare function loadMetaSchemas(schemasDir: string): Map<string, AnySchemaObject>;
@@ -0,0 +1,50 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { SCHEMA_META_FILE, SCHEMA_META_ID } from './metadata-contract';
4
+ const LATEST_FOLDER = 'latest';
5
+ const VERSION_FOLDER = /^\d+\.\d+\.\d+$/;
6
+ function compareVersions(a, b) {
7
+ const [pa, pb] = [a, b].map((v) => v.split('.').map(Number));
8
+ for (let i = 0; i < 3; i++) {
9
+ const diff = (pa?.[i] ?? 0) - (pb?.[i] ?? 0);
10
+ if (diff !== 0)
11
+ return diff;
12
+ }
13
+ return 0;
14
+ }
15
+ /** Version folders under `generatedDir` (named after the release that first shipped the dialect), oldest first. */
16
+ export function listMetaVersions(generatedDir) {
17
+ if (!existsSync(generatedDir))
18
+ return [];
19
+ return readdirSync(generatedDir)
20
+ .filter((name) => VERSION_FOLDER.test(name))
21
+ .sort(compareVersions);
22
+ }
23
+ function readMetaSchema(generatedDir, folder) {
24
+ const path = join(generatedDir, folder, SCHEMA_META_FILE);
25
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf-8')) : undefined;
26
+ }
27
+ /** The newest released meta-schema and its version folder, if any. */
28
+ export function readNewestMetaSchema(generatedDir) {
29
+ const version = listMetaVersions(generatedDir).at(-1);
30
+ const schema = version && readMetaSchema(generatedDir, version);
31
+ return version && schema ? { version, schema } : undefined;
32
+ }
33
+ /**
34
+ * Meta-schemas shipped in `<schemasDir>/generated/`, keyed by `$id`: every versioned folder at its own `$id`, and
35
+ * `latest` registered under the `latest` URL so `$schema` references to either form resolve.
36
+ */
37
+ export function loadMetaSchemas(schemasDir) {
38
+ const generatedDir = join(schemasDir, 'generated');
39
+ const schemas = new Map();
40
+ for (const version of listMetaVersions(generatedDir)) {
41
+ const schema = readMetaSchema(generatedDir, version);
42
+ if (schema && typeof schema.$id === 'string')
43
+ schemas.set(schema.$id, schema);
44
+ }
45
+ // The `latest` copy carries its versioned `$id`, which a version folder may already own; register it under its own URL.
46
+ const latest = readMetaSchema(generatedDir, LATEST_FOLDER);
47
+ if (latest)
48
+ schemas.set(SCHEMA_META_ID, { ...latest, $id: SCHEMA_META_ID });
49
+ return schemas;
50
+ }