structured-context 0.18.0 → 0.19.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/dist/commands/dump.d.ts +2 -2
- package/dist/commands/dump.js +2 -2
- package/dist/commands/validate.js +5 -8
- package/dist/load-space-graph.d.ts +7 -7
- package/dist/load-space-graph.js +14 -10
- package/dist/schema/schema.d.ts +6 -0
- package/dist/schema/schema.js +62 -1
- package/dist/validate.d.ts +2 -2
- package/dist/validate.js +17 -28
- package/package.json +1 -1
package/dist/commands/dump.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface DumpOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
|
12
12
|
*
|
|
13
|
-
* Unfiltered dumps include
|
|
14
|
-
* Filtered dumps match show/render: the filter runs over the graph of
|
|
13
|
+
* Unfiltered dumps include nodes of an unknown type, since dump is a debugging aid.
|
|
14
|
+
* Filtered dumps match show/render: the filter runs over the graph of nodes with a known type.
|
|
15
15
|
*/
|
|
16
16
|
export declare function buildDump(context: SpaceContext, options?: DumpOptions): Promise<ReadSpaceResult>;
|
|
17
17
|
export declare function dump(context: SpaceContext, options?: DumpOptions): Promise<void>;
|
package/dist/commands/dump.js
CHANGED
|
@@ -4,8 +4,8 @@ import { exportSpaceGraph } from '../space-graph-data';
|
|
|
4
4
|
/**
|
|
5
5
|
* Build the dump payload: every parsed node, or only nodes matched by a filter.
|
|
6
6
|
*
|
|
7
|
-
* Unfiltered dumps include
|
|
8
|
-
* Filtered dumps match show/render: the filter runs over the graph of
|
|
7
|
+
* Unfiltered dumps include nodes of an unknown type, since dump is a debugging aid.
|
|
8
|
+
* Filtered dumps match show/render: the filter runs over the graph of nodes with a known type.
|
|
9
9
|
*/
|
|
10
10
|
export async function buildDump(context, options = {}) {
|
|
11
11
|
const readResult = await readSpace(context);
|
|
@@ -5,9 +5,8 @@ import { judgeForRun } from '../judge/factory';
|
|
|
5
5
|
import { resolveSpacePluginSchemas } from '../plugins/util';
|
|
6
6
|
import { parseRuleFilter } from '../schema/rule-filter';
|
|
7
7
|
import { bundledSchemasDir } from '../schema/schema';
|
|
8
|
-
import {
|
|
8
|
+
import { formatSource, ruleViolationMessage, validateSpace } from '../validate';
|
|
9
9
|
export async function validate(context, options = {}) {
|
|
10
|
-
const { schema, schemaRefRegistry } = context;
|
|
11
10
|
const result = await validateSpace(context, {
|
|
12
11
|
depth: options.depth,
|
|
13
12
|
includeGated: options.includeGated,
|
|
@@ -25,9 +24,8 @@ export async function validate(context, options = {}) {
|
|
|
25
24
|
errorsByFile[file] = {};
|
|
26
25
|
errorsByFile[file][key] = severity ? { kind, message, severity } : { kind, message };
|
|
27
26
|
};
|
|
28
|
-
for (const { nodeId,
|
|
29
|
-
const
|
|
30
|
-
for (const { message, dedupeKey } of formatted) {
|
|
27
|
+
for (const { nodeId, issues } of result.nodeErrors) {
|
|
28
|
+
for (const { message, dedupeKey } of issues) {
|
|
31
29
|
addError(nodeId, `schema:${dedupeKey}`, 'schema', message);
|
|
32
30
|
}
|
|
33
31
|
}
|
|
@@ -119,10 +117,9 @@ export async function validate(context, options = {}) {
|
|
|
119
117
|
}
|
|
120
118
|
if (result.nodeErrors.length > 0) {
|
|
121
119
|
console.log(`\nSchema validation errors:`);
|
|
122
|
-
result.nodeErrors.forEach(({ nodeId, source,
|
|
120
|
+
result.nodeErrors.forEach(({ nodeId, source, issues }) => {
|
|
123
121
|
console.log(`\n ${source ? formatSource(source) : nodeId}:`);
|
|
124
|
-
|
|
125
|
-
formatted.forEach(({ message }) => {
|
|
122
|
+
issues.forEach(({ message }) => {
|
|
126
123
|
console.log(` ${message}`);
|
|
127
124
|
});
|
|
128
125
|
});
|
|
@@ -10,14 +10,14 @@ export interface AssembleSpaceGraphOptions {
|
|
|
10
10
|
readResult?: ReadSpaceResult;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Assemble a SpaceGraph from a space context: read the space, drop nodes
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes whose type the schema
|
|
14
|
+
* does not define, build the graph for the selected hierarchy (the main hierarchy by default),
|
|
15
|
+
* and apply an optional view/filter expression.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* duplicates. Use validateSpace for a full validation report.
|
|
17
|
+
* A node with schema field errors (for example an invalid enum value) stays in the graph, since
|
|
18
|
+
* it still has a type and a place in the hierarchy. Only nodes that cannot be placed are left
|
|
19
|
+
* out. This is NOT a validation pass — nodes may have field errors, broken references, rule
|
|
20
|
+
* violations, or duplicates. Use validateSpace for a full validation report.
|
|
21
21
|
*
|
|
22
22
|
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
23
23
|
* graph-assembly behaviour cannot drift.
|
package/dist/load-space-graph.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { filterNodes } from './filter/filter-nodes';
|
|
2
2
|
import { readSpace } from './read/read-space';
|
|
3
|
+
import { extractSchemaTypeNames } from './schema/schema';
|
|
3
4
|
import { createSpaceContext } from './space-context';
|
|
4
5
|
import { buildSpaceGraph, selectHierarchy } from './space-graph';
|
|
5
6
|
/**
|
|
6
|
-
* Assemble a SpaceGraph from a space context: read the space, drop nodes
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes whose type the schema
|
|
8
|
+
* does not define, build the graph for the selected hierarchy (the main hierarchy by default),
|
|
9
|
+
* and apply an optional view/filter expression.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* duplicates. Use validateSpace for a full validation report.
|
|
11
|
+
* A node with schema field errors (for example an invalid enum value) stays in the graph, since
|
|
12
|
+
* it still has a type and a place in the hierarchy. Only nodes that cannot be placed are left
|
|
13
|
+
* out. This is NOT a validation pass — nodes may have field errors, broken references, rule
|
|
14
|
+
* violations, or duplicates. Use validateSpace for a full validation report.
|
|
14
15
|
*
|
|
15
16
|
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
16
17
|
* graph-assembly behaviour cannot drift.
|
|
17
18
|
*/
|
|
18
19
|
export async function assembleSpaceGraph(context, options = {}) {
|
|
19
20
|
const { nodes: allNodes } = options.readResult ?? (await readSpace(context));
|
|
20
|
-
const {
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const { schema, schemaRefRegistry } = context;
|
|
22
|
+
const schemaTypes = Array.isArray(schema.oneOf) ? extractSchemaTypeNames(schema, schemaRefRegistry) : undefined;
|
|
23
|
+
const placeableNodes = schemaTypes
|
|
24
|
+
? allNodes.filter((node) => schemaTypes.has(String(node.schemaData.type)))
|
|
25
|
+
: allNodes;
|
|
26
|
+
let graph = buildSpaceGraph(placeableNodes, selectHierarchy(context.schema.metadata, options.hierarchy));
|
|
23
27
|
if (options.filter) {
|
|
24
28
|
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
|
|
25
29
|
graph = await filterNodes(expression, graph);
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export type PluginSchemaSource = {
|
|
|
17
17
|
* Throws if a lower-precedence schema collides with a reserved $id or violates namespace rules.
|
|
18
18
|
*/
|
|
19
19
|
export declare function buildFullRegistry(schemaPath: string, pluginSchemas?: PluginSchemaSource[]): Map<string, AnySchemaObject>;
|
|
20
|
+
/**
|
|
21
|
+
* Validator for one node type: the schema's shared `allOf` plus only that type's `oneOf` branch.
|
|
22
|
+
* Errors then come only from the node's own type, not from every branch that did not match.
|
|
23
|
+
* Returns undefined when the schema has no such branch, so callers fall back to the full validator.
|
|
24
|
+
*/
|
|
25
|
+
export declare function typeValidatorFor(fullValidator: ValidateFunction, type: string): ValidateFunction | undefined;
|
|
20
26
|
export declare function createValidator(schemaPath: string, pluginSchemas?: PluginSchemaSource[]): ValidateFunction;
|
|
21
27
|
export declare function resolveNodeType(type: string, typeAliases: Record<string, string> | undefined): string;
|
|
22
28
|
export interface EntityInfo {
|
package/dist/schema/schema.js
CHANGED
|
@@ -234,7 +234,68 @@ function compileValidator(targetSchema, schemaRefRegistry) {
|
|
|
234
234
|
continue;
|
|
235
235
|
ajv.addSchema(schema);
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
const validator = ajv.compile(targetSchema);
|
|
238
|
+
typeValidators.set(validator, createTypeValidatorLookup(ajv, targetSchema, schemaRefRegistry));
|
|
239
|
+
return validator;
|
|
240
|
+
}
|
|
241
|
+
/** Root keywords that do not constrain a node: a root with any other keyword cannot be split into per-type checks. */
|
|
242
|
+
const NON_CONSTRAINING_ROOT_KEYWORDS = new Set([
|
|
243
|
+
'$schema',
|
|
244
|
+
'$id',
|
|
245
|
+
'$metadata',
|
|
246
|
+
'$defs',
|
|
247
|
+
'title',
|
|
248
|
+
'description',
|
|
249
|
+
'allOf',
|
|
250
|
+
'oneOf',
|
|
251
|
+
]);
|
|
252
|
+
/** Per-type lookups, keyed by the full-schema validator they were compiled alongside. */
|
|
253
|
+
const typeValidators = new WeakMap();
|
|
254
|
+
/**
|
|
255
|
+
* Validator for one node type: the schema's shared `allOf` plus only that type's `oneOf` branch.
|
|
256
|
+
* Errors then come only from the node's own type, not from every branch that did not match.
|
|
257
|
+
* Returns undefined when the schema has no such branch, so callers fall back to the full validator.
|
|
258
|
+
*/
|
|
259
|
+
export function typeValidatorFor(fullValidator, type) {
|
|
260
|
+
return typeValidators.get(fullValidator)?.(type);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Compiled on first use. The wrapper references the branches inside the already-compiled root,
|
|
264
|
+
* so a branch's relative `$ref`s still resolve against the root.
|
|
265
|
+
*/
|
|
266
|
+
function createTypeValidatorLookup(ajv, rootSchema, schemaRefRegistry) {
|
|
267
|
+
const rootId = rootSchema.$id;
|
|
268
|
+
if (typeof rootId !== 'string' ||
|
|
269
|
+
!Array.isArray(rootSchema.oneOf) ||
|
|
270
|
+
!Object.keys(rootSchema).every((k) => NON_CONSTRAINING_ROOT_KEYWORDS.has(k))) {
|
|
271
|
+
return () => undefined;
|
|
272
|
+
}
|
|
273
|
+
const branchByType = new Map();
|
|
274
|
+
rootSchema.oneOf.forEach((branch, index) => {
|
|
275
|
+
const typeDef = mergeVariantProperties(branch, rootSchema, schemaRefRegistry).properties.type;
|
|
276
|
+
const types = typeDef?.const !== undefined ? [typeDef.const] : Array.isArray(typeDef?.enum) ? typeDef.enum : [];
|
|
277
|
+
for (const t of types)
|
|
278
|
+
if (!branchByType.has(String(t)))
|
|
279
|
+
branchByType.set(String(t), index);
|
|
280
|
+
});
|
|
281
|
+
const shared = Array.isArray(rootSchema.allOf)
|
|
282
|
+
? rootSchema.allOf.map((_, i) => ({ $ref: `${rootId}#/allOf/${i}` }))
|
|
283
|
+
: [];
|
|
284
|
+
const compiled = new Map();
|
|
285
|
+
return (type) => {
|
|
286
|
+
const index = branchByType.get(type);
|
|
287
|
+
if (index === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
let validator = compiled.get(index);
|
|
290
|
+
if (!validator) {
|
|
291
|
+
validator = ajv.compile({
|
|
292
|
+
$id: `${rootId}/__type/${index}`,
|
|
293
|
+
allOf: [...shared, { $ref: `${rootId}#/oneOf/${index}` }],
|
|
294
|
+
});
|
|
295
|
+
compiled.set(index, validator);
|
|
296
|
+
}
|
|
297
|
+
return validator;
|
|
298
|
+
};
|
|
238
299
|
}
|
|
239
300
|
export function createValidator(schemaPath, pluginSchemas) {
|
|
240
301
|
return compileValidator(readRawSchema(schemaPath), buildFullRegistry(schemaPath, pluginSchemas));
|
package/dist/validate.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface ValidationResult {
|
|
|
14
14
|
nodeErrors: Array<{
|
|
15
15
|
nodeId: NodeId;
|
|
16
16
|
source?: SourceLocation;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/** Formatted, deduplicated schema problems for the node, one per field problem. */
|
|
18
|
+
issues: FormattedError[];
|
|
19
19
|
}>;
|
|
20
20
|
refErrors: Array<{
|
|
21
21
|
nodeId: NodeId;
|
package/dist/validate.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isAbsolute, relative, resolve } from 'node:path';
|
|
|
2
2
|
import { readSpace } from './read/read-space';
|
|
3
3
|
import { computeHierarchyDepths, evaluateRulesWithGating } from './schema/gating';
|
|
4
4
|
import { filterRules } from './schema/rule-filter';
|
|
5
|
-
import { extractEntityInfo } from './schema/schema';
|
|
5
|
+
import { extractEntityInfo, typeValidatorFor } from './schema/schema';
|
|
6
6
|
import { validateGraph } from './schema/validate-graph';
|
|
7
7
|
import { validateRules } from './schema/validate-rules';
|
|
8
8
|
import { createSpaceContext } from './space-context';
|
|
@@ -177,31 +177,31 @@ async function validateNodes(context, nodes, unresolvedRefs, options = {}) {
|
|
|
177
177
|
contextTrimmed: { nodeCount: 0, ruleIds: [] },
|
|
178
178
|
};
|
|
179
179
|
for (const node of nodes) {
|
|
180
|
-
//
|
|
180
|
+
// Check the type first: an unknown type reports one error instead of one per oneOf branch.
|
|
181
181
|
const nodeType = node.schemaData.type;
|
|
182
182
|
if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
|
|
183
183
|
const sortedTypes = [...validTypes].sort();
|
|
184
184
|
result.nodeFailures.push({
|
|
185
185
|
node,
|
|
186
|
-
|
|
187
|
-
errors: [
|
|
186
|
+
issues: [
|
|
188
187
|
{
|
|
189
|
-
instancePath: '/type',
|
|
190
|
-
keyword: 'enum',
|
|
191
188
|
message: `Invalid type "${nodeType}". Valid types are: ${sortedTypes.join(', ')}`,
|
|
192
|
-
|
|
193
|
-
schemaPath: '#/oneOf',
|
|
194
|
-
data: nodeType,
|
|
189
|
+
dedupeKey: `type:${sortedTypes.join(',')}`,
|
|
195
190
|
},
|
|
196
191
|
],
|
|
197
192
|
});
|
|
198
193
|
continue;
|
|
199
194
|
}
|
|
200
|
-
|
|
195
|
+
// A valid type is checked against its own branch only, so errors come from that type alone.
|
|
196
|
+
const validate = (nodeType !== undefined && typeValidatorFor(schemaValidator, nodeType)) || schemaValidator;
|
|
197
|
+
if (validate(node.schemaData)) {
|
|
201
198
|
result.validCount++;
|
|
202
199
|
}
|
|
203
200
|
else {
|
|
204
|
-
result.nodeFailures.push({
|
|
201
|
+
result.nodeFailures.push({
|
|
202
|
+
node,
|
|
203
|
+
issues: formatErrors(validate.errors || [], schema, schemaRefRegistry, node.schemaData),
|
|
204
|
+
});
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
result.duplicateErrors.push(...findDuplicateIds(nodes));
|
|
@@ -276,11 +276,10 @@ export async function validateSpace(context, options = {}) {
|
|
|
276
276
|
return {
|
|
277
277
|
validCount: result.validCount,
|
|
278
278
|
nodeErrorCount: result.nodeFailures.length,
|
|
279
|
-
nodeErrors: result.nodeFailures.map(({ node,
|
|
279
|
+
nodeErrors: result.nodeFailures.map(({ node, issues }) => ({
|
|
280
280
|
nodeId: node.id,
|
|
281
281
|
...(node.source !== undefined && { source: node.source }),
|
|
282
|
-
|
|
283
|
-
nodeData: node.schemaData,
|
|
282
|
+
issues,
|
|
284
283
|
})),
|
|
285
284
|
refErrors: result.refErrors,
|
|
286
285
|
duplicateErrors: result.duplicateErrors,
|
|
@@ -312,27 +311,17 @@ export function resolveFileSpace(filePath, config) {
|
|
|
312
311
|
* Select one file's entries from a space's validation result: issues keyed and formatted, errors split from warnings.
|
|
313
312
|
* Pure — a file's entries are exactly its slice of the result.
|
|
314
313
|
*/
|
|
315
|
-
function issuesForFile(result, label,
|
|
316
|
-
const { schema, schemaRefRegistry } = context;
|
|
314
|
+
function issuesForFile(result, label, includeGated) {
|
|
317
315
|
const errors = {};
|
|
318
316
|
const warnings = {};
|
|
319
|
-
for (const { node,
|
|
317
|
+
for (const { node, issues } of result.nodeFailures) {
|
|
320
318
|
if (node.id !== label)
|
|
321
319
|
continue;
|
|
322
320
|
const position = {
|
|
323
321
|
...(node.source?.line !== undefined ? { line: node.source.line } : {}),
|
|
324
322
|
...(node.source?.column !== undefined ? { column: node.source.column } : {}),
|
|
325
323
|
};
|
|
326
|
-
|
|
327
|
-
errors[`schema:type:${invalidType.validTypes.join(',')}`] = {
|
|
328
|
-
kind: 'schema',
|
|
329
|
-
message: ajvErrors[0].message,
|
|
330
|
-
...position,
|
|
331
|
-
};
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const formatted = formatErrors(ajvErrors, schema, schemaRefRegistry, node.schemaData);
|
|
335
|
-
for (const { message, dedupeKey } of formatted) {
|
|
324
|
+
for (const { message, dedupeKey } of issues) {
|
|
336
325
|
errors[`schema:${dedupeKey}`] = { kind: 'schema', message, ...position };
|
|
337
326
|
}
|
|
338
327
|
}
|
|
@@ -409,7 +398,7 @@ export async function validateResolvedFile(filePath, label, context, options = {
|
|
|
409
398
|
judge: options.judge,
|
|
410
399
|
judgeTarget: label,
|
|
411
400
|
});
|
|
412
|
-
const { errors, warnings } = issuesForFile(result, label,
|
|
401
|
+
const { errors, warnings } = issuesForFile(result, label, options.includeGated);
|
|
413
402
|
return {
|
|
414
403
|
file: isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath),
|
|
415
404
|
label,
|
package/package.json
CHANGED