structured-context 0.11.0 → 0.13.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.
- package/README.md +52 -198
- package/dist/api.d.ts +17 -15
- package/dist/api.js +12 -9
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +133 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +24 -6
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +79 -23
- package/dist/config.d.ts +25 -1
- package/dist/config.js +106 -12
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +7 -5
- package/dist/load-space-graph.js +19 -8
- package/dist/plugins/loader.js +3 -3
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
- package/dist/plugins/markdown/parse-embedded.js +195 -24
- package/dist/plugins/markdown/read-space.js +42 -14
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +952 -183
- package/dist/schema/metadata-contract.js +177 -33
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +308 -199
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +16 -3
- package/dist/schema/validate-rules.js +91 -29
- package/dist/schemas/_knowledge_wiki.json +24 -24
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +17 -8
- package/dist/schemas/_sctx_base.json +3 -3
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/okf.json +8 -0
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +5 -2
- package/dist/space-context.js +9 -4
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +64 -16
- package/dist/validate.d.ts +62 -9
- package/dist/validate.js +229 -114
- package/docs/concepts.md +65 -9
- package/docs/config.md +78 -1
- package/docs/rules.md +168 -50
- package/docs/schemas.md +152 -52
- package/package.json +17 -11
- package/schemas/_knowledge_wiki.json +24 -24
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +17 -8
- package/schemas/_sctx_base.json +3 -3
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/okf.json +8 -0
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/dist/schema/schema.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import
|
|
5
|
-
import Ajv, {} from 'ajv';
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
6
5
|
import JSON5 from 'json5';
|
|
7
|
-
import {
|
|
6
|
+
import { SCHEMA_URI_SCHEME } from '../constants';
|
|
7
|
+
import { shortenPluginName } from '../plugins/util';
|
|
8
|
+
import { loadMetaSchemas } from './meta-schemas';
|
|
9
|
+
import { METADATA_SCHEMA, } from './metadata-contract';
|
|
8
10
|
import { isObject, mergeVariantProperties, resolveJsonPointer } from './schema-refs';
|
|
9
11
|
const packageDir = import.meta.url ? dirname(fileURLToPath(import.meta.url)) : '';
|
|
10
12
|
export let bundledSchemasDir = packageDir ? join(packageDir, '..', '..', 'schemas') : '';
|
|
@@ -12,9 +14,95 @@ export let bundledSchemasDir = packageDir ? join(packageDir, '..', '..', 'schema
|
|
|
12
14
|
export function setBundledSchemasDir(dir) {
|
|
13
15
|
bundledSchemasDir = dir;
|
|
14
16
|
}
|
|
15
|
-
const validateMetadataContract = new
|
|
17
|
+
const validateMetadataContract = new Ajv2020().compile(METADATA_SCHEMA);
|
|
18
|
+
/** File stem of each schema read from disk — the default name of the hierarchy it declares. */
|
|
19
|
+
const schemaFileStems = new WeakMap();
|
|
16
20
|
export function readRawSchema(schemaPath) {
|
|
17
|
-
|
|
21
|
+
const schema = JSON5.parse(readFileSync(resolve(schemaPath), 'utf-8'));
|
|
22
|
+
rewriteLegacyCoreRefs(schema, schemaPath);
|
|
23
|
+
schemaFileStems.set(schema, basename(schemaPath, extname(schemaPath)));
|
|
24
|
+
return schema;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Schema `$id` namespaces. Every `$id` uses the `sctx://` scheme, and its shape says where the schema comes from:
|
|
28
|
+
* - `sctx://core/<name>` — bundled with structured-context
|
|
29
|
+
* - `sctx://<plugin>/<name>` — contributed by a plugin, prefixed with the plugin's short name
|
|
30
|
+
* - `sctx://<name>` — local to a space's schema directory (a single segment, no path)
|
|
31
|
+
*/
|
|
32
|
+
const SCTX_SCHEME = `${SCHEMA_URI_SCHEME}://`;
|
|
33
|
+
export const CORE_SCHEMA_PREFIX = `${SCTX_SCHEME}core/`;
|
|
34
|
+
/**
|
|
35
|
+
* Bundled schema `$id`s from before the `sctx://core/` namespace. References to them keep resolving
|
|
36
|
+
* (with a deprecation warning) until 1.0, and local schemas may not claim them.
|
|
37
|
+
*/
|
|
38
|
+
const LEGACY_CORE_IDS = new Set([
|
|
39
|
+
'_sctx_base',
|
|
40
|
+
'_ost_strict',
|
|
41
|
+
'_strategy_general',
|
|
42
|
+
'_knowledge_wiki',
|
|
43
|
+
'strict_ost',
|
|
44
|
+
'strategy_general',
|
|
45
|
+
'knowledge_wiki',
|
|
46
|
+
].map((name) => `${SCTX_SCHEME}${name}`));
|
|
47
|
+
const warnedLegacyIds = new Set();
|
|
48
|
+
function upgradeLegacyCoreRef(ref, schemaPath) {
|
|
49
|
+
const hashIndex = ref.indexOf('#');
|
|
50
|
+
const id = hashIndex === -1 ? ref : ref.slice(0, hashIndex);
|
|
51
|
+
if (!LEGACY_CORE_IDS.has(id))
|
|
52
|
+
return ref;
|
|
53
|
+
const upgraded = `${CORE_SCHEMA_PREFIX}${id.slice(SCTX_SCHEME.length)}`;
|
|
54
|
+
if (!warnedLegacyIds.has(id)) {
|
|
55
|
+
warnedLegacyIds.add(id);
|
|
56
|
+
console.warn(`Warning: "${id}" is deprecated and will stop resolving in 1.0; use "${upgraded}" instead (referenced in ${schemaPath}).`);
|
|
57
|
+
}
|
|
58
|
+
return hashIndex === -1 ? upgraded : `${upgraded}${ref.slice(hashIndex)}`;
|
|
59
|
+
}
|
|
60
|
+
/** Rewrite references to legacy bundled `$id`s (in `$ref` and `$metadata.imports`) to their `sctx://core/` form. */
|
|
61
|
+
function rewriteLegacyCoreRefs(node, schemaPath) {
|
|
62
|
+
if (Array.isArray(node)) {
|
|
63
|
+
for (const item of node)
|
|
64
|
+
rewriteLegacyCoreRefs(item, schemaPath);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!isObject(node))
|
|
68
|
+
return;
|
|
69
|
+
if (typeof node.$ref === 'string')
|
|
70
|
+
node.$ref = upgradeLegacyCoreRef(node.$ref, schemaPath);
|
|
71
|
+
if (isObject(node.$metadata) && Array.isArray(node.$metadata.imports)) {
|
|
72
|
+
node.$metadata.imports = node.$metadata.imports.map((entry) => typeof entry === 'string' ? upgradeLegacyCoreRef(entry, schemaPath) : entry);
|
|
73
|
+
}
|
|
74
|
+
for (const value of Object.values(node))
|
|
75
|
+
rewriteLegacyCoreRefs(value, schemaPath);
|
|
76
|
+
}
|
|
77
|
+
/** Throw if `id` doesn't belong to the namespace of the source it was loaded from. */
|
|
78
|
+
function assertIdNamespace(id, source) {
|
|
79
|
+
if (source.kind === 'core') {
|
|
80
|
+
if (!id.startsWith(CORE_SCHEMA_PREFIX)) {
|
|
81
|
+
throw new Error(`Bundled schema $id "${id}" must start with "${CORE_SCHEMA_PREFIX}".`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const where = source.kind === 'plugin' ? `schema in plugin "${source.pluginName}"` : `local schema in "${source.dir}"`;
|
|
86
|
+
const expected = source.kind === 'plugin' ? `"${SCTX_SCHEME}${source.shortName}/<name>"` : `"${SCTX_SCHEME}<name>"`;
|
|
87
|
+
if (!id.startsWith(SCTX_SCHEME)) {
|
|
88
|
+
throw new Error(`Schema namespace violation: ${where} uses $id "${id}". Use the form ${expected}.`);
|
|
89
|
+
}
|
|
90
|
+
const segments = id.slice(SCTX_SCHEME.length).split('/');
|
|
91
|
+
if (source.kind === 'plugin') {
|
|
92
|
+
if (source.shortName === 'core') {
|
|
93
|
+
throw new Error(`Schema namespace violation: plugin "${source.pluginName}" can't contribute schemas; "core" is reserved for bundled schemas.`);
|
|
94
|
+
}
|
|
95
|
+
if (segments.length < 2 || segments[0] !== source.shortName) {
|
|
96
|
+
throw new Error(`Schema namespace violation: ${where} uses $id "${id}". Use the form ${expected}.`);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (segments.length !== 1) {
|
|
101
|
+
throw new Error(`Schema namespace violation: ${where} uses $id "${id}". Local schema $ids are a single name, ${expected}; "${SCTX_SCHEME}<prefix>/..." is reserved for bundled ("core") and plugin schemas.`);
|
|
102
|
+
}
|
|
103
|
+
if (LEGACY_CORE_IDS.has(id)) {
|
|
104
|
+
throw new Error(`Schema namespace violation: ${where} uses $id "${id}", which is reserved as an alias for "${CORE_SCHEMA_PREFIX}${segments[0]}" until 1.0. Choose another name.`);
|
|
105
|
+
}
|
|
18
106
|
}
|
|
19
107
|
/**
|
|
20
108
|
* Build a registry of all schemas in the given directory, keyed by $id.
|
|
@@ -38,37 +126,97 @@ function buildSchemaRegistry(dir, targetFile) {
|
|
|
38
126
|
return schemaRefRegistry;
|
|
39
127
|
}
|
|
40
128
|
/**
|
|
41
|
-
* Build the full
|
|
129
|
+
* Build the full multi-layer registry for a schema path:
|
|
42
130
|
* - Layer 1: bundled schemas/ dir (partials only, as fallback)
|
|
43
|
-
* - Layer 2: schema
|
|
44
|
-
*
|
|
131
|
+
* - Layer 2: plugin schema dirs (partials + target file if schemaPath is in this plugin's schemasDir)
|
|
132
|
+
* - Layer 3: schema's own dir (partials + target file)
|
|
133
|
+
* Throws if a lower-precedence schema collides with a reserved $id or violates namespace rules.
|
|
45
134
|
*/
|
|
46
|
-
export function buildFullRegistry(schemaPath) {
|
|
135
|
+
export function buildFullRegistry(schemaPath, pluginSchemas) {
|
|
47
136
|
const absPath = resolve(schemaPath);
|
|
48
137
|
const targetFile = basename(absPath);
|
|
49
138
|
const targetDir = dirname(absPath);
|
|
50
139
|
const schemaRefRegistry = new Map();
|
|
51
140
|
// Layer 1: bundled schemas/ dir (partials only)
|
|
52
141
|
for (const [id, schema] of buildSchemaRegistry(bundledSchemasDir)) {
|
|
142
|
+
assertIdNamespace(id, { kind: 'core' });
|
|
53
143
|
schemaRefRegistry.set(id, schema);
|
|
54
144
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
145
|
+
const bundledIds = new Set(schemaRefRegistry.keys());
|
|
146
|
+
for (const id of loadMetaSchemas(bundledSchemasDir).keys())
|
|
147
|
+
bundledIds.add(id);
|
|
148
|
+
// Layer 2: plugin schema dirs
|
|
149
|
+
const pluginIds = new Map(); // id -> pluginName
|
|
150
|
+
if (pluginSchemas) {
|
|
151
|
+
for (const { schemasDir, pluginName } of pluginSchemas) {
|
|
152
|
+
if (!existsSync(schemasDir))
|
|
153
|
+
continue;
|
|
154
|
+
const isTargetDir = resolve(schemasDir) === targetDir;
|
|
155
|
+
const shortName = shortenPluginName(pluginName);
|
|
156
|
+
for (const [id, schema] of buildSchemaRegistry(schemasDir, isTargetDir ? targetFile : undefined)) {
|
|
157
|
+
if (bundledIds.has(id)) {
|
|
158
|
+
throw new Error(`Schema collision: schema in plugin "${pluginName}" (${schemasDir}) uses $id "${id}" which is reserved by a default schema. Please use a unique $id for plugin schemas.`);
|
|
159
|
+
}
|
|
160
|
+
const existingPlugin = pluginIds.get(id);
|
|
161
|
+
if (existingPlugin) {
|
|
162
|
+
throw new Error(`Schema collision: schema in plugin "${pluginName}" (${schemasDir}) uses $id "${id}" which is already defined by plugin "${existingPlugin}".`);
|
|
163
|
+
}
|
|
164
|
+
assertIdNamespace(id, { kind: 'plugin', pluginName, shortName });
|
|
165
|
+
schemaRefRegistry.set(id, schema);
|
|
166
|
+
pluginIds.set(id, pluginName);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Layer 3: schema's own dir (partials + target file)
|
|
171
|
+
const isPluginDir = pluginSchemas?.some((p) => resolve(p.schemasDir) === targetDir);
|
|
172
|
+
if (targetDir !== bundledSchemasDir && !isPluginDir) {
|
|
173
|
+
const reservedIds = new Set(schemaRefRegistry.keys());
|
|
174
|
+
for (const id of loadMetaSchemas(bundledSchemasDir).keys())
|
|
175
|
+
reservedIds.add(id);
|
|
59
176
|
for (const [id, schema] of buildSchemaRegistry(targetDir, targetFile)) {
|
|
60
|
-
if (
|
|
61
|
-
throw new Error(`Schema collision:
|
|
177
|
+
if (reservedIds.has(id)) {
|
|
178
|
+
throw new Error(`Schema collision: local schema in ${targetDir} uses $id "${id}", which is already defined by a bundled or plugin schema. Choose another name.`);
|
|
62
179
|
}
|
|
180
|
+
assertIdNamespace(id, { kind: 'local', dir: targetDir });
|
|
63
181
|
schemaRefRegistry.set(id, schema);
|
|
64
182
|
}
|
|
65
183
|
}
|
|
66
184
|
return schemaRefRegistry;
|
|
67
185
|
}
|
|
186
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
187
|
+
const DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?(Z|[+-](\d{2}):(\d{2}))?$/;
|
|
188
|
+
function isCalendarDate(year, month, day) {
|
|
189
|
+
const d = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
190
|
+
return d.getUTCMonth() === Number(month) - 1 && d.getUTCDate() === Number(day);
|
|
191
|
+
}
|
|
192
|
+
/** ISO 8601 date, e.g. 2026-03-31. */
|
|
193
|
+
function isDate(value) {
|
|
194
|
+
const m = DATE_PATTERN.exec(value);
|
|
195
|
+
return m !== null && isCalendarDate(m[1], m[2], m[3]);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* ISO 8601 datetime. `strict` requires seconds and a UTC offset (e.g. 2026-03-31T09:15:00+10:00);
|
|
199
|
+
* otherwise both are optional, as in the local datetimes Obsidian writes (e.g. 2026-03-31T09:15).
|
|
200
|
+
*/
|
|
201
|
+
function isDateTime(value, strict) {
|
|
202
|
+
const m = DATE_TIME_PATTERN.exec(value);
|
|
203
|
+
if (!m || !isCalendarDate(m[1], m[2], m[3]))
|
|
204
|
+
return false;
|
|
205
|
+
const [, , , , hour, minute, second, , offset, offsetHour, offsetMinute] = m;
|
|
206
|
+
if (strict && (second === undefined || offset === undefined))
|
|
207
|
+
return false;
|
|
208
|
+
return (Number(hour) < 24 &&
|
|
209
|
+
Number(minute) < 60 &&
|
|
210
|
+
Number(second ?? 0) < 60 &&
|
|
211
|
+
Number(offsetHour ?? 0) < 24 &&
|
|
212
|
+
Number(offsetMinute ?? 0) < 60);
|
|
213
|
+
}
|
|
68
214
|
function compileValidator(targetSchema, schemaRefRegistry) {
|
|
69
|
-
const ajv = new
|
|
215
|
+
const ajv = new Ajv2020();
|
|
70
216
|
ajv.addFormat('path', (value) => value.length > 0 && !value.includes('\0'));
|
|
71
|
-
ajv.addFormat('date',
|
|
217
|
+
ajv.addFormat('date', isDate);
|
|
218
|
+
ajv.addFormat('date-time', (value) => isDateTime(value, true));
|
|
219
|
+
ajv.addFormat('date-or-date-time', (value) => isDate(value) || isDateTime(value, false));
|
|
72
220
|
ajv.addFormat('wikilink', (value) => /^\[\[.+\]\]$/.test(value));
|
|
73
221
|
ajv.addKeyword({
|
|
74
222
|
keyword: '$metadata',
|
|
@@ -77,37 +225,39 @@ function compileValidator(targetSchema, schemaRefRegistry) {
|
|
|
77
225
|
valid: true,
|
|
78
226
|
errors: false,
|
|
79
227
|
});
|
|
80
|
-
const
|
|
81
|
-
|
|
228
|
+
const metaSchemas = loadMetaSchemas(bundledSchemasDir);
|
|
229
|
+
for (const schema of metaSchemas.values())
|
|
230
|
+
ajv.addSchema(schema);
|
|
82
231
|
// Register all except target schema (AJV compiles targetSchema explicitly)
|
|
83
232
|
for (const [id, schema] of schemaRefRegistry) {
|
|
84
|
-
if (id === targetSchema.$id || id
|
|
233
|
+
if (id === targetSchema.$id || metaSchemas.has(id))
|
|
85
234
|
continue;
|
|
86
235
|
ajv.addSchema(schema);
|
|
87
236
|
}
|
|
88
237
|
return ajv.compile(targetSchema);
|
|
89
238
|
}
|
|
90
|
-
export function createValidator(schemaPath) {
|
|
91
|
-
return compileValidator(readRawSchema(schemaPath), buildFullRegistry(schemaPath));
|
|
239
|
+
export function createValidator(schemaPath, pluginSchemas) {
|
|
240
|
+
return compileValidator(readRawSchema(schemaPath), buildFullRegistry(schemaPath, pluginSchemas));
|
|
92
241
|
}
|
|
93
242
|
export function resolveNodeType(type, typeAliases) {
|
|
94
243
|
return typeAliases?.[type] ?? type;
|
|
95
244
|
}
|
|
96
|
-
const RULE_CATEGORIES = new Set(['validation', 'coherence', 'workflow', 'best-practice']);
|
|
97
|
-
const RULE_ALLOWED_KEYS = new Set(['id', 'category', 'description', 'check', 'type', 'scope', 'override']);
|
|
98
245
|
function readTopLevelMetadata(schema) {
|
|
99
246
|
const metadata = schema.$metadata;
|
|
100
247
|
if (!isObject(metadata))
|
|
101
248
|
return undefined;
|
|
249
|
+
const schemaId = typeof schema.$id === 'string' ? schema.$id : '(unknown schema)';
|
|
250
|
+
if (isObject(metadata.hierarchy) && '$ref' in metadata.hierarchy && Object.keys(metadata.hierarchy).length > 1) {
|
|
251
|
+
throw new Error(`Invalid $metadata in schema "${schemaId}": hierarchy both declares levels and selects a hierarchy with "$ref". Use one or the other.`);
|
|
252
|
+
}
|
|
102
253
|
if (!validateMetadataContract(metadata)) {
|
|
103
|
-
const schemaId = typeof schema.$id === 'string' ? schema.$id : '(unknown schema)';
|
|
104
254
|
const errors = validateMetadataContract.errors?.map((e) => `${e.instancePath || '(root)'} ${e.message}`).join('; ') ??
|
|
105
255
|
'unknown error';
|
|
106
256
|
throw new Error(`Invalid $metadata in schema "${schemaId}": ${errors}`);
|
|
107
257
|
}
|
|
108
258
|
return metadata;
|
|
109
259
|
}
|
|
110
|
-
function
|
|
260
|
+
function resolveMetadataRef(ref, currentRootSchema, schemaRefRegistry) {
|
|
111
261
|
if (ref.startsWith('#')) {
|
|
112
262
|
const pointer = ref.slice(1);
|
|
113
263
|
const rootId = typeof currentRootSchema.$id === 'string' ? currentRootSchema.$id : '(root schema)';
|
|
@@ -130,180 +280,140 @@ function resolveRefTargetForRule(ref, currentRootSchema, schemaRefRegistry) {
|
|
|
130
280
|
refKey: `${baseId}#${pointer}`,
|
|
131
281
|
};
|
|
132
282
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return;
|
|
139
|
-
for (const [key, child] of Object.entries(value)) {
|
|
140
|
-
if (key === '$ref' && typeof child === 'string' && !child.startsWith('#')) {
|
|
141
|
-
const schemaId = child.split('#', 1)[0] ?? child;
|
|
142
|
-
if (!seen.has(schemaId)) {
|
|
143
|
-
seen.add(schemaId);
|
|
144
|
-
refs.push(schemaId);
|
|
145
|
-
}
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
walk(child);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
walk(schema);
|
|
152
|
-
return refs;
|
|
153
|
-
}
|
|
283
|
+
/**
|
|
284
|
+
* Collect metadata providers in merge order: each schema's `$metadata.imports` depth-first, in
|
|
285
|
+
* order, then the schema's own metadata. Each schema contributes at most once. `$ref` does not
|
|
286
|
+
* import metadata.
|
|
287
|
+
*/
|
|
154
288
|
function collectMetadataProviders(rootSchema, schemaRefRegistry) {
|
|
155
289
|
const providers = [];
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!referencedSchema)
|
|
290
|
+
const rootSchemaId = typeof rootSchema.$id === 'string' ? rootSchema.$id : '(root schema)';
|
|
291
|
+
const visitedSchemaIds = new Set([rootSchemaId]);
|
|
292
|
+
const walk = (schema, schemaId) => {
|
|
293
|
+
const metadata = readTopLevelMetadata(schema);
|
|
294
|
+
if (!metadata)
|
|
295
|
+
return;
|
|
296
|
+
for (const importId of metadata.imports ?? []) {
|
|
297
|
+
if (visitedSchemaIds.has(importId))
|
|
165
298
|
continue;
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
299
|
+
visitedSchemaIds.add(importId);
|
|
300
|
+
const importedSchema = schemaRefRegistry.get(importId);
|
|
301
|
+
if (!importedSchema) {
|
|
302
|
+
throw new Error(`Cannot resolve import "${importId}" in schema "${schemaId}".`);
|
|
170
303
|
}
|
|
304
|
+
walk(importedSchema, importId);
|
|
171
305
|
}
|
|
306
|
+
providers.push({ schemaId, schema, metadata });
|
|
172
307
|
};
|
|
173
|
-
walk(rootSchema);
|
|
174
|
-
const rootMetadata = readTopLevelMetadata(rootSchema);
|
|
175
|
-
if (rootMetadata) {
|
|
176
|
-
const rootSchemaId = typeof rootSchema.$id === 'string' ? rootSchema.$id : '(root schema)';
|
|
177
|
-
providers.push({ schemaId: rootSchemaId, schema: rootSchema, metadata: rootMetadata });
|
|
178
|
-
}
|
|
308
|
+
walk(rootSchema, rootSchemaId);
|
|
179
309
|
return providers;
|
|
180
310
|
}
|
|
181
|
-
function
|
|
311
|
+
function isRefEntry(value) {
|
|
182
312
|
if (!isObject(value))
|
|
183
313
|
return false;
|
|
184
314
|
return typeof value.$ref === 'string' && value.$ref.length > 0 && Object.keys(value).length === 1;
|
|
185
315
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
if (typeof record.description !== 'string' || record.description.length === 0)
|
|
195
|
-
return false;
|
|
196
|
-
if (typeof record.check !== 'string' || record.check.length === 0)
|
|
197
|
-
return false;
|
|
198
|
-
if ('type' in record && (typeof record.type !== 'string' || record.type.length === 0))
|
|
199
|
-
return false;
|
|
200
|
-
if ('scope' in record && record.scope !== 'global')
|
|
201
|
-
return false;
|
|
202
|
-
if ('override' in record && typeof record.override !== 'boolean')
|
|
203
|
-
return false;
|
|
204
|
-
for (const key of Object.keys(record)) {
|
|
205
|
-
if (!RULE_ALLOWED_KEYS.has(key))
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
function resolveRuleEntries(ruleEntry, provider, schemaRefRegistry, stack) {
|
|
211
|
-
if (isMetadataRule(ruleEntry)) {
|
|
212
|
-
return [ruleEntry];
|
|
213
|
-
}
|
|
214
|
-
if (!isRuleRefEntry(ruleEntry)) {
|
|
215
|
-
throw new Error(`Invalid rule entry in metadata from "${provider.schemaId}".`);
|
|
216
|
-
}
|
|
217
|
-
const target = resolveRefTargetForRule(ruleEntry.$ref, provider.schema, schemaRefRegistry);
|
|
218
|
-
if (stack.has(target.refKey)) {
|
|
219
|
-
throw new Error(`Cyclic rule import detected while loading metadata from "${provider.schemaId}": ${[...stack, target.refKey].join(' -> ')}`);
|
|
220
|
-
}
|
|
221
|
-
stack.add(target.refKey);
|
|
222
|
-
try {
|
|
223
|
-
const value = target.value;
|
|
224
|
-
const resolveArray = (arr) => {
|
|
225
|
-
const resolvedRules = [];
|
|
226
|
-
for (const child of arr) {
|
|
227
|
-
if (!isObject(child)) {
|
|
228
|
-
throw new Error(`Invalid rule import target for "${ruleEntry.$ref}" from "${provider.schemaId}". Rule sets must contain objects.`);
|
|
229
|
-
}
|
|
230
|
-
resolvedRules.push(...resolveRuleEntries(child, { ...provider, schema: target.rootSchema }, schemaRefRegistry, stack));
|
|
231
|
-
}
|
|
232
|
-
return resolvedRules;
|
|
233
|
-
};
|
|
234
|
-
if (Array.isArray(value)) {
|
|
235
|
-
return resolveArray(value);
|
|
236
|
-
}
|
|
237
|
-
if (isObject(value) && 'rules' in value) {
|
|
238
|
-
const nestedRules = value.rules;
|
|
239
|
-
if (!Array.isArray(nestedRules)) {
|
|
240
|
-
throw new Error(`Invalid rule import target for "${ruleEntry.$ref}" from "${provider.schemaId}". "rules" must be an array.`);
|
|
241
|
-
}
|
|
242
|
-
return resolveArray(nestedRules);
|
|
243
|
-
}
|
|
244
|
-
if (isObject(value)) {
|
|
245
|
-
return resolveRuleEntries(value, { ...provider, schema: target.rootSchema }, schemaRefRegistry, stack);
|
|
246
|
-
}
|
|
247
|
-
throw new Error(`Invalid rule import target for "${ruleEntry.$ref}" from "${provider.schemaId}". Expected a rule object or rule set.`);
|
|
248
|
-
}
|
|
249
|
-
finally {
|
|
250
|
-
stack.delete(target.refKey);
|
|
316
|
+
/**
|
|
317
|
+
* The single metadata merge rule: a duplicate key is an error unless the later entry sets
|
|
318
|
+
* `override: true`, in which case it replaces the earlier one (keeping its position).
|
|
319
|
+
*/
|
|
320
|
+
function mergeEntry(merged, key, label, value, override, providerId) {
|
|
321
|
+
const existing = merged.get(key);
|
|
322
|
+
if (existing && override !== true) {
|
|
323
|
+
throw new Error(`Duplicate ${label} found in "${existing.providerId}" and "${providerId}". Set "override": true on the later one to replace it.`);
|
|
251
324
|
}
|
|
325
|
+
merged.set(key, { providerId, value });
|
|
252
326
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
return normalized;
|
|
256
|
-
}
|
|
257
|
-
function areRulesEquivalent(left, right) {
|
|
258
|
-
return isDeepStrictEqual(normalizeRule(left), normalizeRule(right));
|
|
327
|
+
function values(merged) {
|
|
328
|
+
return [...merged.values()].map(({ value }) => value);
|
|
259
329
|
}
|
|
260
330
|
function extractMetadata(schema, schemaRefRegistry) {
|
|
261
331
|
const metadataProviders = collectMetadataProviders(schema, schemaRefRegistry);
|
|
262
|
-
|
|
263
|
-
|
|
332
|
+
// Hierarchies are merged by name; the declaring object is kept so a root `hierarchy: { $ref }`
|
|
333
|
+
// selection can be matched to it.
|
|
334
|
+
const mergedHierarchies = new Map();
|
|
335
|
+
let rootHierarchy;
|
|
336
|
+
let mainSelection;
|
|
337
|
+
const mergedAliases = new Map();
|
|
264
338
|
const mergedRules = new Map();
|
|
265
|
-
const mergedRelationships =
|
|
339
|
+
const mergedRelationships = new Map();
|
|
266
340
|
for (const provider of metadataProviders) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
341
|
+
const { schemaId, metadata } = provider;
|
|
342
|
+
const declaredHierarchy = metadata.hierarchy;
|
|
343
|
+
const isRoot = provider.schema === schema;
|
|
344
|
+
if (isRefEntry(declaredHierarchy)) {
|
|
345
|
+
// A hierarchy selection only applies when its schema is the root; imported schemas contribute
|
|
346
|
+
// their declared hierarchies, not their selections.
|
|
347
|
+
if (isRoot)
|
|
348
|
+
mainSelection = declaredHierarchy;
|
|
271
349
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
350
|
+
else if (declaredHierarchy) {
|
|
351
|
+
const hierarchy = normalizeHierarchy(declaredHierarchy, provider);
|
|
352
|
+
const label = `hierarchy "${hierarchy.name}"`;
|
|
353
|
+
mergeEntry(mergedHierarchies, hierarchy.name, label, { declared: declaredHierarchy, hierarchy }, declaredHierarchy.override, schemaId);
|
|
354
|
+
if (isRoot)
|
|
355
|
+
rootHierarchy = declaredHierarchy;
|
|
274
356
|
}
|
|
275
|
-
|
|
276
|
-
|
|
357
|
+
for (const [alias, target] of Object.entries(metadata.aliases ?? {})) {
|
|
358
|
+
const [type, override] = typeof target === 'string' ? [target, false] : [target.type, target.override];
|
|
359
|
+
mergeEntry(mergedAliases, alias, `alias "${alias}"`, type, override, schemaId);
|
|
277
360
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
361
|
+
for (const rel of metadata.relationships ?? []) {
|
|
362
|
+
const field = rel.field ?? 'parent';
|
|
363
|
+
const label = `relationship "${rel.parent} → ${rel.type}" (field "${field}")`;
|
|
364
|
+
mergeEntry(mergedRelationships, `${rel.parent}\0${rel.type}\0${field}`, label, rel, rel.override, schemaId);
|
|
365
|
+
}
|
|
366
|
+
for (const rule of metadata.rules ?? []) {
|
|
367
|
+
mergeEntry(mergedRules, rule.id, `rule "${rule.id}"`, rule, rule.override, schemaId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const merged = values(mergedHierarchies);
|
|
371
|
+
const allHierarchies = merged.map(({ hierarchy }) => hierarchy);
|
|
372
|
+
validateHierarchyTypes(allHierarchies);
|
|
373
|
+
const findDeclared = (declared) => merged.find((entry) => entry.declared === declared)?.hierarchy;
|
|
374
|
+
let mainHierarchy = rootHierarchy ? findDeclared(rootHierarchy) : undefined;
|
|
375
|
+
if (mainSelection) {
|
|
376
|
+
const target = resolveMetadataRef(mainSelection.$ref, schema, schemaRefRegistry);
|
|
377
|
+
mainHierarchy = findDeclared(target.value);
|
|
378
|
+
if (!mainHierarchy) {
|
|
379
|
+
throw new Error(`Hierarchy $ref "${mainSelection.$ref}" does not point to a hierarchy declared by an imported schema. Expected a ref like "<schema $id>#/$metadata/hierarchy".`);
|
|
296
380
|
}
|
|
297
381
|
}
|
|
382
|
+
else if (!mainHierarchy && allHierarchies.length === 1) {
|
|
383
|
+
mainHierarchy = allHierarchies[0];
|
|
384
|
+
}
|
|
385
|
+
else if (!mainHierarchy && allHierarchies.length > 1) {
|
|
386
|
+
const rootSchemaId = typeof schema.$id === 'string' ? schema.$id : '(root schema)';
|
|
387
|
+
throw new Error(`Schema "${rootSchemaId}" imports multiple hierarchies (${allHierarchies.map((h) => `"${h.name}"`).join(', ')}) but selects none as its main hierarchy. Declare one, or select one with "hierarchy": { "$ref": "<schema $id>#/$metadata/hierarchy" }.`);
|
|
388
|
+
}
|
|
389
|
+
const typeAliases = Object.fromEntries([...mergedAliases].map(([alias, { value }]) => [alias, value]));
|
|
390
|
+
const rules = values(mergedRules).map(({ override, ...rule }) => rule);
|
|
391
|
+
const relationships = values(mergedRelationships);
|
|
298
392
|
// Collect valid type names from the schema's oneOf list for validation
|
|
299
393
|
const validTypeNames = extractSchemaTypeNames(schema, schemaRefRegistry);
|
|
300
|
-
// Validate
|
|
301
|
-
validateMetadataReferences({
|
|
394
|
+
// Validate metadata references against valid type names
|
|
395
|
+
validateMetadataReferences({ allHierarchies, typeAliases, rules }, validTypeNames);
|
|
302
396
|
// Filter relationships to only include those where both parent and child types are valid
|
|
303
|
-
const filteredRelationships =
|
|
397
|
+
const filteredRelationships = relationships.filter((rel) => {
|
|
304
398
|
return validTypeNames.has(rel.parent) && validTypeNames.has(rel.type);
|
|
305
399
|
});
|
|
306
|
-
|
|
400
|
+
return {
|
|
401
|
+
hierarchy: mainHierarchy,
|
|
402
|
+
hierarchies: allHierarchies.length > 0 ? allHierarchies : undefined,
|
|
403
|
+
typeAliases: Object.keys(typeAliases).length > 0 ? typeAliases : undefined,
|
|
404
|
+
rules: rules.length > 0 ? rules : undefined,
|
|
405
|
+
relationships: filteredRelationships.length > 0
|
|
406
|
+
? filteredRelationships.map(({ override, ...rel }) => ({
|
|
407
|
+
...rel,
|
|
408
|
+
field: rel.field ?? 'parent',
|
|
409
|
+
fieldOn: rel.fieldOn === 'parent' ? 'parent' : 'child',
|
|
410
|
+
multiple: rel.multiple ?? false,
|
|
411
|
+
}))
|
|
412
|
+
: undefined,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function normalizeHierarchy(declared, provider) {
|
|
416
|
+
const levels = declared.levels.map((entry) => {
|
|
307
417
|
if (typeof entry === 'string') {
|
|
308
418
|
return { type: entry, field: 'parent', fieldOn: 'child', multiple: false, selfRef: false };
|
|
309
419
|
}
|
|
@@ -322,24 +432,24 @@ function extractMetadata(schema, schemaRefRegistry) {
|
|
|
322
432
|
};
|
|
323
433
|
});
|
|
324
434
|
return {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
allowSkipLevels: mergedHierarchy?.allowSkipLevels,
|
|
329
|
-
}
|
|
330
|
-
: undefined,
|
|
331
|
-
typeAliases: Object.keys(mergedAliases).length > 0 ? mergedAliases : undefined,
|
|
332
|
-
rules: mergedRules.size > 0 ? [...mergedRules.values()].map(({ rule }) => normalizeRule(rule)) : undefined,
|
|
333
|
-
relationships: filteredRelationships.length > 0
|
|
334
|
-
? filteredRelationships.map((rel) => ({
|
|
335
|
-
...rel,
|
|
336
|
-
field: rel.field ?? 'parent',
|
|
337
|
-
fieldOn: rel.fieldOn === 'parent' ? 'parent' : 'child',
|
|
338
|
-
multiple: rel.multiple ?? false,
|
|
339
|
-
}))
|
|
340
|
-
: undefined,
|
|
435
|
+
name: declared.name ?? schemaFileStems.get(provider.schema) ?? provider.schemaId,
|
|
436
|
+
levels,
|
|
437
|
+
allowSkipLevels: declared.allowSkipLevels,
|
|
341
438
|
};
|
|
342
439
|
}
|
|
440
|
+
/** Each type may belong to at most one hierarchy. */
|
|
441
|
+
function validateHierarchyTypes(hierarchies) {
|
|
442
|
+
const typeOwners = new Map();
|
|
443
|
+
for (const hierarchy of hierarchies) {
|
|
444
|
+
for (const { type } of hierarchy.levels) {
|
|
445
|
+
const owner = typeOwners.get(type);
|
|
446
|
+
if (owner !== undefined && owner !== hierarchy.name) {
|
|
447
|
+
throw new Error(`Type "${type}" belongs to both hierarchy "${owner}" and hierarchy "${hierarchy.name}". Each type may belong to at most one hierarchy.`);
|
|
448
|
+
}
|
|
449
|
+
typeOwners.set(type, hierarchy.name);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
343
453
|
function validateMetadataReferences(metadata, validTypes) {
|
|
344
454
|
// Skip validation if no types are defined (e.g., schemas without oneOf or placeholder schemas)
|
|
345
455
|
if (validTypes.size === 0) {
|
|
@@ -347,23 +457,22 @@ function validateMetadataReferences(metadata, validTypes) {
|
|
|
347
457
|
}
|
|
348
458
|
const errors = [];
|
|
349
459
|
// Validate hierarchy level types
|
|
350
|
-
|
|
351
|
-
for (const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
errors.push(`Hierarchy level "${typeName}" is not a valid type in the schema's oneOf list`);
|
|
460
|
+
for (const hierarchy of metadata.allHierarchies) {
|
|
461
|
+
for (const { type } of hierarchy.levels) {
|
|
462
|
+
if (!validTypes.has(type)) {
|
|
463
|
+
errors.push(`Hierarchy "${hierarchy.name}" level "${type}" is not a valid type in the schema's oneOf list`);
|
|
355
464
|
}
|
|
356
465
|
}
|
|
357
466
|
}
|
|
358
467
|
// Validate alias targets (the values)
|
|
359
|
-
for (const [alias, target] of Object.entries(metadata.
|
|
468
|
+
for (const [alias, target] of Object.entries(metadata.typeAliases)) {
|
|
360
469
|
if (!validTypes.has(target)) {
|
|
361
470
|
errors.push(`Alias "${alias}" → "${target}" references type "${target}" which is not in the schema's oneOf list`);
|
|
362
471
|
}
|
|
363
472
|
}
|
|
364
473
|
// Note: relationships are filtered (not validated) - see extractMetadata
|
|
365
474
|
// Validate rule types
|
|
366
|
-
for (const
|
|
475
|
+
for (const rule of metadata.rules) {
|
|
367
476
|
if (rule.type && !validTypes.has(rule.type)) {
|
|
368
477
|
errors.push(`Rule "${rule.id}" has type "${rule.type}" which is not in the schema's oneOf list`);
|
|
369
478
|
}
|
|
@@ -401,12 +510,12 @@ export function extractEntityInfo(schema, schemaRefRegistry) {
|
|
|
401
510
|
export function extractSchemaTypeNames(schema, schemaRefRegistry) {
|
|
402
511
|
return new Set(extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type));
|
|
403
512
|
}
|
|
404
|
-
export function loadMetadata(schemaPath) {
|
|
405
|
-
return extractMetadata(readRawSchema(schemaPath), buildFullRegistry(schemaPath));
|
|
513
|
+
export function loadMetadata(schemaPath, pluginSchemas) {
|
|
514
|
+
return extractMetadata(readRawSchema(schemaPath), buildFullRegistry(schemaPath, pluginSchemas));
|
|
406
515
|
}
|
|
407
|
-
export function loadSchema(schemaPath) {
|
|
516
|
+
export function loadSchema(schemaPath, pluginSchemas) {
|
|
408
517
|
const rawSchema = readRawSchema(schemaPath);
|
|
409
|
-
const schemaRefRegistry = buildFullRegistry(schemaPath);
|
|
518
|
+
const schemaRefRegistry = buildFullRegistry(schemaPath, pluginSchemas);
|
|
410
519
|
const schema = {
|
|
411
520
|
...rawSchema,
|
|
412
521
|
metadata: extractMetadata(rawSchema, schemaRefRegistry),
|