structured-context 0.15.0 → 0.17.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/LICENSE +21 -0
- package/README.md +21 -13
- package/dist/api.d.ts +5 -2
- package/dist/api.js +4 -0
- package/dist/browser.d.ts +14 -0
- package/dist/browser.js +13 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +3 -1
- package/dist/commands/schemas.d.ts +3 -1
- package/dist/commands/schemas.js +25 -53
- package/dist/commands/spaces.js +4 -6
- package/dist/commands/validate.js +20 -19
- package/dist/config/health.d.ts +10 -6
- package/dist/config/health.js +11 -44
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +15 -6
- package/dist/filter/augment-nodes.d.ts +2 -2
- package/dist/filter/augment-nodes.js +2 -2
- package/dist/filter/expand-include.js +2 -2
- package/dist/index.js +12 -3
- package/dist/judge/context.js +1 -1
- package/dist/load-space-graph.js +1 -13
- package/dist/plugins/markdown/parse-embedded.js +4 -4
- package/dist/plugins/miro/cache.js +1 -1
- package/dist/plugins/miro/sync.js +2 -2
- package/dist/read/resolve-graph-edges.js +3 -3
- package/dist/schema/list-schemas.d.ts +27 -0
- package/dist/schema/list-schemas.js +108 -0
- package/dist/schema/metadata-contract.d.ts +34 -0
- package/dist/schema/metadata-contract.js +2 -2
- package/dist/schema/validate-graph.d.ts +2 -2
- package/dist/schema/validate-graph.js +4 -4
- package/dist/schemas/generated/space-graph/1.json +216 -0
- package/dist/schemas/generated/space-graph/latest.json +216 -0
- package/dist/space-context.js +11 -1
- package/dist/space-graph-data.d.ts +237 -0
- package/dist/space-graph-data.js +129 -0
- package/dist/space-graph.d.ts +12 -1
- package/dist/space-graph.js +25 -1
- package/dist/testing.js +3 -3
- package/dist/types.d.ts +2 -2
- package/docs/concepts.md +8 -4
- package/package.json +5 -1
- package/schemas/generated/space-graph/1.json +216 -0
- package/schemas/generated/space-graph/latest.json +216 -0
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dump } from './commands/dump';
|
|
|
8
8
|
import { miroSyncCommand } from './commands/miro-sync';
|
|
9
9
|
import { listPlugins } from './commands/plugins';
|
|
10
10
|
import { render, renderList } from './commands/render';
|
|
11
|
-
import {
|
|
11
|
+
import { printSchemas, showSchema } from './commands/schemas';
|
|
12
12
|
import { show } from './commands/show';
|
|
13
13
|
import { addSpaceCommand, listSpaces } from './commands/spaces';
|
|
14
14
|
import { templateSync } from './commands/template-sync';
|
|
@@ -16,6 +16,7 @@ import { validate, watchValidate } from './commands/validate';
|
|
|
16
16
|
import { validateFileCommand } from './commands/validate-file';
|
|
17
17
|
import { loadConfig, setConfigPath } from './config';
|
|
18
18
|
import { CLI_NAME } from './constants';
|
|
19
|
+
import { SctxError } from './errors';
|
|
19
20
|
import { JudgeConfigError } from './judge/factory';
|
|
20
21
|
import { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
21
22
|
// Once node:process is loaded (vfile, via unified, loads it), Bun's console.log drops piped output
|
|
@@ -121,6 +122,7 @@ program
|
|
|
121
122
|
.argument('<space-name>', 'Space name')
|
|
122
123
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
123
124
|
.option('--hierarchy <name>', 'Hierarchy used to evaluate --filter (default: the main hierarchy)')
|
|
125
|
+
.option('--graph', 'Output the space graph format: nodes plus every hierarchy')
|
|
124
126
|
.action(async (spaceName, options) => dump(buildSpaceContext(spaceName), options));
|
|
125
127
|
program
|
|
126
128
|
.command('miro-sync')
|
|
@@ -176,7 +178,8 @@ const schemasCmd = new Command('schemas').alias('schema').description('List and
|
|
|
176
178
|
schemasCmd
|
|
177
179
|
.command('list', { isDefault: true })
|
|
178
180
|
.description('List available schemas')
|
|
179
|
-
.
|
|
181
|
+
.option('--json', 'Output as JSON')
|
|
182
|
+
.action(async (options) => await printSchemas({ json: options.json ?? false }));
|
|
180
183
|
schemasCmd
|
|
181
184
|
.command('show')
|
|
182
185
|
.description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
|
|
@@ -226,4 +229,10 @@ renderCmd
|
|
|
226
229
|
await render(buildSpaceContext(spaceName), format, options);
|
|
227
230
|
});
|
|
228
231
|
program.addCommand(renderCmd);
|
|
229
|
-
|
|
232
|
+
// Expected errors print as one line; anything else keeps its stack trace
|
|
233
|
+
program.parseAsync().catch((err) => {
|
|
234
|
+
if (!(err instanceof SctxError))
|
|
235
|
+
throw err;
|
|
236
|
+
console.error(`${err.code}: ${err.message}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|
package/dist/judge/context.js
CHANGED
|
@@ -49,7 +49,7 @@ export function relatedNodes(graph, node, context) {
|
|
|
49
49
|
const parentsOf = (n) => graph.hierarchy === undefined
|
|
50
50
|
? []
|
|
51
51
|
: n.resolvedParents
|
|
52
|
-
.filter((p) => p.
|
|
52
|
+
.filter((p) => p.kind === 'hierarchy' && p.hierarchy === hierarchyName)
|
|
53
53
|
.map((p) => graph.nodes.get(p.id))
|
|
54
54
|
.filter((p) => p !== undefined);
|
|
55
55
|
const childrenOf = (n) => graph.hierarchyChildren.get(n.id) ?? [];
|
package/dist/load-space-graph.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { filterNodes } from './filter/filter-nodes';
|
|
2
2
|
import { readSpace } from './read/read-space';
|
|
3
3
|
import { createSpaceContext } from './space-context';
|
|
4
|
-
import { buildSpaceGraph } from './space-graph';
|
|
4
|
+
import { buildSpaceGraph, selectHierarchy } from './space-graph';
|
|
5
5
|
/**
|
|
6
6
|
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
7
|
* fail schema validation, build the graph for the selected hierarchy (the main
|
|
@@ -26,18 +26,6 @@ export async function assembleSpaceGraph(context, options = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
return graph;
|
|
28
28
|
}
|
|
29
|
-
/** Resolve a hierarchy by name, or the main hierarchy when no name is given. */
|
|
30
|
-
function selectHierarchy(metadata, name) {
|
|
31
|
-
if (name === undefined)
|
|
32
|
-
return metadata.hierarchy;
|
|
33
|
-
const hierarchies = metadata.hierarchies ?? [];
|
|
34
|
-
const hierarchy = hierarchies.find((h) => h.name === name);
|
|
35
|
-
if (!hierarchy) {
|
|
36
|
-
const available = hierarchies.map((h) => h.name).join(', ');
|
|
37
|
-
throw new Error(`Unknown hierarchy: "${name}".${available ? ` Available: ${available}` : ' The schema defines no hierarchies.'}`);
|
|
38
|
-
}
|
|
39
|
-
return hierarchy;
|
|
40
|
-
}
|
|
41
29
|
/**
|
|
42
30
|
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
43
31
|
*
|
|
@@ -364,7 +364,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
364
364
|
fieldOn: rel.fieldOn,
|
|
365
365
|
multiple: rel.multiple,
|
|
366
366
|
templateFormat: rel.templateFormat ?? 'heading',
|
|
367
|
-
|
|
367
|
+
kind: 'relationship',
|
|
368
368
|
matchers: rel.matchers,
|
|
369
369
|
embeddedTemplateFields: rel.embeddedTemplateFields,
|
|
370
370
|
};
|
|
@@ -378,7 +378,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
378
378
|
fieldOn: level.fieldOn,
|
|
379
379
|
multiple: level.multiple,
|
|
380
380
|
templateFormat: level.templateFormat ?? 'heading',
|
|
381
|
-
|
|
381
|
+
kind: 'hierarchy',
|
|
382
382
|
matchers: level.matchers,
|
|
383
383
|
embeddedTemplateFields: level.embeddedTemplateFields,
|
|
384
384
|
};
|
|
@@ -435,7 +435,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
435
435
|
fieldOn: currentLevel.fieldOn,
|
|
436
436
|
multiple: currentLevel.multiple,
|
|
437
437
|
templateFormat: currentLevel.templateFormat ?? 'list',
|
|
438
|
-
|
|
438
|
+
kind: 'hierarchy',
|
|
439
439
|
matchers: immediateParentLevel.matchers,
|
|
440
440
|
};
|
|
441
441
|
}
|
|
@@ -651,7 +651,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
651
651
|
// e.g. definition.type='capabilities', definition.parent='application' → capabilities is above application.
|
|
652
652
|
const typeIdx = hierarchy.indexOf(definition.type);
|
|
653
653
|
const parentIdx = hierarchy.indexOf(definition.parent);
|
|
654
|
-
const isParentLevelMatch = definition.
|
|
654
|
+
const isParentLevelMatch = definition.kind === 'hierarchy' && typeIdx !== -1 && parentIdx !== -1 && typeIdx < parentIdx;
|
|
655
655
|
const activeNodeFieldAppendArg = isParentLevelMatch && semanticParent.node
|
|
656
656
|
? { node: semanticParent.node, field: definition.field }
|
|
657
657
|
: undefined;
|
|
@@ -30,7 +30,7 @@ export function computeNodeHash(node) {
|
|
|
30
30
|
status: node.schemaData.status,
|
|
31
31
|
summary: node.schemaData.summary,
|
|
32
32
|
priority: node.schemaData.priority,
|
|
33
|
-
parents: node.resolvedParents.filter((r) => r.
|
|
33
|
+
parents: node.resolvedParents.filter((r) => r.kind === 'hierarchy').map((r) => r.id),
|
|
34
34
|
};
|
|
35
35
|
return createHash('sha256').update(JSON.stringify(relevant)).digest('hex').slice(0, 16);
|
|
36
36
|
}
|
|
@@ -257,8 +257,8 @@ export async function miroSync(context, graph, options) {
|
|
|
257
257
|
const desiredEdges = new Map();
|
|
258
258
|
for (const node of nodes) {
|
|
259
259
|
const childTitle = node.title;
|
|
260
|
-
for (const { id: parentId,
|
|
261
|
-
if (
|
|
260
|
+
for (const { id: parentId, kind, hierarchy } of node.resolvedParents) {
|
|
261
|
+
if (kind !== 'hierarchy' || hierarchy !== graph.hierarchy?.name)
|
|
262
262
|
continue;
|
|
263
263
|
const parentTitle = graph.nodes.get(parentId)?.title ?? parentId;
|
|
264
264
|
// Both endpoints must have verified cards on the board
|
|
@@ -21,13 +21,13 @@ 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
|
|
24
|
+
* @param kind Whether this edge comes from hierarchy levels or relationships
|
|
25
25
|
* @param hierarchy Name of the hierarchy the edge belongs to (hierarchy edges only)
|
|
26
26
|
* @param selfRef Whether child and parent are the same node type
|
|
27
27
|
* @param unresolvedRefs Array to push broken/invalid link entries into
|
|
28
28
|
* @param typeAliases Optional type aliases for resolution
|
|
29
29
|
*/
|
|
30
|
-
function resolveEdge(nodesByType, targetIndex, edge,
|
|
30
|
+
function resolveEdge(nodesByType, targetIndex, edge, kind, hierarchy, selfRef, unresolvedRefs, typeAliases) {
|
|
31
31
|
const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
|
|
32
32
|
const childType = resolveNodeType(rawChildType, typeAliases);
|
|
33
33
|
const parentType = resolveNodeType(rawParentType, typeAliases);
|
|
@@ -38,7 +38,7 @@ function resolveEdge(nodesByType, targetIndex, edge, source, hierarchy, selfRef,
|
|
|
38
38
|
id: parentId,
|
|
39
39
|
field,
|
|
40
40
|
fieldOn,
|
|
41
|
-
|
|
41
|
+
kind,
|
|
42
42
|
...(hierarchy !== undefined ? { hierarchy } : {}),
|
|
43
43
|
selfRef,
|
|
44
44
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Config } from '../config';
|
|
2
|
+
type SchemaListing = {
|
|
3
|
+
/** The value for a config's `schema` field. */
|
|
4
|
+
schema: string;
|
|
5
|
+
/** Absolute location or URL. Omitted for bundled schemas. */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** `core`, a plugin's short name, or `configured`. */
|
|
8
|
+
source: string;
|
|
9
|
+
/** Names of the spaces that use it, and `global` when the top-level `schema` does. */
|
|
10
|
+
usedBy: string[];
|
|
11
|
+
partial: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Why the schema could not be read. */
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
|
|
20
|
+
* schema file the config points at. Each schema appears once, under the place its file lives.
|
|
21
|
+
*/
|
|
22
|
+
export declare function listSchemas(options?: {
|
|
23
|
+
/** Loaded by discovery from `startDir` when omitted. */
|
|
24
|
+
config?: Config;
|
|
25
|
+
startDir?: string;
|
|
26
|
+
}): Promise<SchemaListing[]>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { loadConfigOrEmpty, resolveConfigPath, resolveSchemaPath } from '../config';
|
|
4
|
+
import { PLUGIN_PREFIX } from '../constants';
|
|
5
|
+
import { CONFIG_PLUGINS_DIR, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
|
+
import { bundledSchemasDir, readRawSchema } from './schema';
|
|
7
|
+
function isRemote(ref) {
|
|
8
|
+
return /^https?:\/\//.test(ref);
|
|
9
|
+
}
|
|
10
|
+
function describeSchema(path) {
|
|
11
|
+
try {
|
|
12
|
+
const schema = readRawSchema(path);
|
|
13
|
+
return {
|
|
14
|
+
...(typeof schema.$id === 'string' && { id: schema.$id }),
|
|
15
|
+
...(typeof schema.title === 'string' && { title: schema.title }),
|
|
16
|
+
...(typeof schema.description === 'string' && { description: schema.description }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function jsonFiles(dir) {
|
|
24
|
+
if (!dir || !existsSync(dir))
|
|
25
|
+
return [];
|
|
26
|
+
return readdirSync(dir)
|
|
27
|
+
.filter((f) => f.endsWith('.json'))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
|
|
32
|
+
* schema file the config points at. Each schema appears once, under the place its file lives.
|
|
33
|
+
*/
|
|
34
|
+
export async function listSchemas(options = {}) {
|
|
35
|
+
const config = options.config ?? loadConfigOrEmpty({ startDir: options.startDir });
|
|
36
|
+
const listings = [];
|
|
37
|
+
const byPath = new Map();
|
|
38
|
+
const add = (listing, location) => {
|
|
39
|
+
listings.push(listing);
|
|
40
|
+
byPath.set(resolve(location), listing);
|
|
41
|
+
};
|
|
42
|
+
for (const file of jsonFiles(bundledSchemasDir)) {
|
|
43
|
+
const location = join(bundledSchemasDir, file);
|
|
44
|
+
add({ schema: file, source: 'core', usedBy: [], partial: file.startsWith('_'), ...describeSchema(location) }, location);
|
|
45
|
+
}
|
|
46
|
+
// Relative schema references are anchored at the folder of the config that holds them
|
|
47
|
+
const fallbackDir = dirname(resolveConfigPath(options.startDir).path);
|
|
48
|
+
const configDirs = [...new Set([fallbackDir, ...config.spaces.map((s) => s.sourceDir ?? fallbackDir)])];
|
|
49
|
+
// Plugins are found the way schema references resolve them, without loading their code: those declared by a
|
|
50
|
+
// space or the config, and any installed beside a config file
|
|
51
|
+
const pluginSchemaDirs = new Map();
|
|
52
|
+
for (const space of [undefined, ...config.spaces]) {
|
|
53
|
+
for (const { pluginName, schemasDir } of resolveSpacePluginSchemas(space, config, space?.sourceDir ?? fallbackDir)) {
|
|
54
|
+
pluginSchemaDirs.set(pluginName, schemasDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const dir of configDirs) {
|
|
58
|
+
const pluginsDir = join(dir, CONFIG_PLUGINS_DIR);
|
|
59
|
+
if (!existsSync(pluginsDir))
|
|
60
|
+
continue;
|
|
61
|
+
for (const entry of readdirSync(pluginsDir).filter((e) => e.startsWith(PLUGIN_PREFIX))) {
|
|
62
|
+
const schemasDir = resolvePluginSchemasDir(entry, dir);
|
|
63
|
+
if (schemasDir && !pluginSchemaDirs.has(shortenPluginName(entry))) {
|
|
64
|
+
pluginSchemaDirs.set(shortenPluginName(entry), schemasDir);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const [source, schemasDir] of pluginSchemaDirs) {
|
|
69
|
+
for (const file of jsonFiles(schemasDir)) {
|
|
70
|
+
const location = join(schemasDir, file);
|
|
71
|
+
add({
|
|
72
|
+
schema: `${source}/${file}`,
|
|
73
|
+
path: location,
|
|
74
|
+
source,
|
|
75
|
+
usedBy: [],
|
|
76
|
+
partial: file.startsWith('_'),
|
|
77
|
+
...describeSchema(location),
|
|
78
|
+
}, location);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const references = [
|
|
82
|
+
...(config.schema ? [{ user: 'global', raw: config.schema, dir: fallbackDir }] : []),
|
|
83
|
+
...config.spaces.flatMap((s) => s.schema ? [{ user: s.name, raw: s.schema, dir: s.sourceDir ?? fallbackDir }] : []),
|
|
84
|
+
];
|
|
85
|
+
for (const { user, raw, dir } of references) {
|
|
86
|
+
const location = isRemote(raw) ? raw : resolveSchemaPath(raw, dir);
|
|
87
|
+
const key = isRemote(raw) ? raw : resolve(location);
|
|
88
|
+
let listing = byPath.get(key);
|
|
89
|
+
if (!listing) {
|
|
90
|
+
listing = {
|
|
91
|
+
schema: raw,
|
|
92
|
+
path: isRemote(raw) || isAbsolute(location) ? location : resolve(location),
|
|
93
|
+
source: 'configured',
|
|
94
|
+
usedBy: [],
|
|
95
|
+
partial: basename(raw).startsWith('_'),
|
|
96
|
+
...(isRemote(raw)
|
|
97
|
+
? { error: 'Remote schemas are not read' }
|
|
98
|
+
: existsSync(location)
|
|
99
|
+
? describeSchema(location)
|
|
100
|
+
: { error: `Schema file not found: ${location}` }),
|
|
101
|
+
};
|
|
102
|
+
add(listing, key);
|
|
103
|
+
}
|
|
104
|
+
if (!listing.usedBy.includes(user))
|
|
105
|
+
listing.usedBy.push(user);
|
|
106
|
+
}
|
|
107
|
+
return listings;
|
|
108
|
+
}
|
|
@@ -7,6 +7,40 @@ export declare const SCHEMA_META_FILE = "_structured_context_schema_meta.json";
|
|
|
7
7
|
export declare function schemaMetaId(folder: string): string;
|
|
8
8
|
/** The URL schemas set as `$schema` to follow the newest dialect. Resolves to the newest versioned meta-schema. */
|
|
9
9
|
export declare const SCHEMA_META_ID: string;
|
|
10
|
+
/** Graph edge routing fields shared by hierarchy levels and relationships. */
|
|
11
|
+
export declare const EDGE_PROPS: {
|
|
12
|
+
readonly field: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly minLength: 1;
|
|
15
|
+
};
|
|
16
|
+
readonly fieldOn: {
|
|
17
|
+
readonly enum: readonly ["child", "parent"];
|
|
18
|
+
};
|
|
19
|
+
readonly multiple: {
|
|
20
|
+
readonly type: "boolean";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** Embedding/template hint fields shared by hierarchy levels and relationships. */
|
|
24
|
+
export declare const EMBEDDING_PROPS: {
|
|
25
|
+
readonly templateFormat: {
|
|
26
|
+
readonly enum: readonly ["heading", "list", "table", "page"];
|
|
27
|
+
};
|
|
28
|
+
readonly matchers: {
|
|
29
|
+
readonly type: "array";
|
|
30
|
+
readonly items: {
|
|
31
|
+
readonly type: "string";
|
|
32
|
+
readonly minLength: 1;
|
|
33
|
+
};
|
|
34
|
+
readonly minItems: 1;
|
|
35
|
+
};
|
|
36
|
+
readonly embeddedTemplateFields: {
|
|
37
|
+
readonly type: "array";
|
|
38
|
+
readonly items: {
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
readonly minLength: 1;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
10
44
|
/** Schema objects wrapping each prop group for type derivation via FromSchema. */
|
|
11
45
|
declare const EDGE_SCHEMA: {
|
|
12
46
|
readonly type: "object";
|
|
@@ -9,13 +9,13 @@ export function schemaMetaId(folder) {
|
|
|
9
9
|
/** The URL schemas set as `$schema` to follow the newest dialect. Resolves to the newest versioned meta-schema. */
|
|
10
10
|
export const SCHEMA_META_ID = schemaMetaId('latest');
|
|
11
11
|
/** Graph edge routing fields shared by hierarchy levels and relationships. */
|
|
12
|
-
const EDGE_PROPS = {
|
|
12
|
+
export const EDGE_PROPS = {
|
|
13
13
|
field: { type: 'string', minLength: 1 },
|
|
14
14
|
fieldOn: { enum: ['child', 'parent'] },
|
|
15
15
|
multiple: { type: 'boolean' },
|
|
16
16
|
};
|
|
17
17
|
/** Embedding/template hint fields shared by hierarchy levels and relationships. */
|
|
18
|
-
const EMBEDDING_PROPS = {
|
|
18
|
+
export const EMBEDDING_PROPS = {
|
|
19
19
|
templateFormat: { enum: ['heading', 'list', 'table', 'page'] },
|
|
20
20
|
matchers: { type: 'array', items: { type: 'string', minLength: 1 }, minItems: 1 },
|
|
21
21
|
embeddedTemplateFields: { type: 'array', items: { type: 'string', minLength: 1 } },
|
|
@@ -16,8 +16,8 @@ export declare function validateGraph(nodes: SpaceNode[], metadata: SchemaMetada
|
|
|
16
16
|
/**
|
|
17
17
|
* Validate that resolved parents follow hierarchy level rules and relationship type constraints.
|
|
18
18
|
*
|
|
19
|
-
* For hierarchy edges (fieldOn:'child',
|
|
20
|
-
* For relationship edges (fieldOn:'child',
|
|
19
|
+
* For hierarchy edges (fieldOn:'child', kind:'hierarchy'): validates level ordering within the edge's hierarchy.
|
|
20
|
+
* For relationship edges (fieldOn:'child', kind:'relationship'): validates parent type.
|
|
21
21
|
* For fieldOn:'parent' edges: validates child type, violation attributed to the field-owner node.
|
|
22
22
|
*
|
|
23
23
|
* Assumes resolveGraphEdges has already been called to populate node.resolvedParents.
|
|
@@ -16,8 +16,8 @@ export function validateGraph(nodes, metadata, unresolvedRefs = []) {
|
|
|
16
16
|
/**
|
|
17
17
|
* Validate that resolved parents follow hierarchy level rules and relationship type constraints.
|
|
18
18
|
*
|
|
19
|
-
* For hierarchy edges (fieldOn:'child',
|
|
20
|
-
* For relationship edges (fieldOn:'child',
|
|
19
|
+
* For hierarchy edges (fieldOn:'child', kind:'hierarchy'): validates level ordering within the edge's hierarchy.
|
|
20
|
+
* For relationship edges (fieldOn:'child', kind:'relationship'): validates parent type.
|
|
21
21
|
* For fieldOn:'parent' edges: validates child type, violation attributed to the field-owner node.
|
|
22
22
|
*
|
|
23
23
|
* Assumes resolveGraphEdges has already been called to populate node.resolvedParents.
|
|
@@ -96,7 +96,7 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
else if (parentRef.
|
|
99
|
+
else if (parentRef.kind === 'relationship') {
|
|
100
100
|
// Relationship edge (fieldOn:'child'): validate parent type matches relationship definition.
|
|
101
101
|
const allowedTypes = typeRules.get(nodeType)?.get(parentRef.field);
|
|
102
102
|
if (allowedTypes && !allowedTypes.has(parentType)) {
|
|
@@ -112,7 +112,7 @@ export function validateHierarchyStructure(nodes, metadata) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
|
-
// Hierarchy edge (fieldOn:'child',
|
|
115
|
+
// Hierarchy edge (fieldOn:'child', kind:'hierarchy'): validate level ordering within its hierarchy.
|
|
116
116
|
const edgeHierarchy = parentRef.hierarchy !== undefined ? hierarchiesByName.get(parentRef.hierarchy) : undefined;
|
|
117
117
|
if (!edgeHierarchy)
|
|
118
118
|
continue;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/space-graph/1.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"title": "structured-context space graph",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"formatVersion": {
|
|
8
|
+
"const": 1
|
|
9
|
+
},
|
|
10
|
+
"hierarchies": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"name": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"levels": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"type": {
|
|
24
|
+
"type": "string"
|
|
25
|
+
},
|
|
26
|
+
"field": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
},
|
|
30
|
+
"fieldOn": {
|
|
31
|
+
"enum": ["child", "parent"]
|
|
32
|
+
},
|
|
33
|
+
"multiple": {
|
|
34
|
+
"type": "boolean"
|
|
35
|
+
},
|
|
36
|
+
"templateFormat": {
|
|
37
|
+
"enum": ["heading", "list", "table", "page"]
|
|
38
|
+
},
|
|
39
|
+
"matchers": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1
|
|
44
|
+
},
|
|
45
|
+
"minItems": 1
|
|
46
|
+
},
|
|
47
|
+
"embeddedTemplateFields": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"items": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"selfRef": {
|
|
55
|
+
"type": "boolean"
|
|
56
|
+
},
|
|
57
|
+
"selfRefField": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": ["type", "field", "fieldOn", "multiple", "selfRef"],
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"allowSkipLevels": {
|
|
66
|
+
"type": "boolean"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"required": ["name", "levels"],
|
|
70
|
+
"additionalProperties": false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"mainHierarchy": {
|
|
74
|
+
"type": "string"
|
|
75
|
+
},
|
|
76
|
+
"nodes": {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"id": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"title": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
"schemaData": {
|
|
88
|
+
"type": "object"
|
|
89
|
+
},
|
|
90
|
+
"linkTargets": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": {
|
|
93
|
+
"type": "string"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"type": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
},
|
|
99
|
+
"content": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"contentLinks": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"text": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
},
|
|
110
|
+
"target": {
|
|
111
|
+
"type": "string"
|
|
112
|
+
},
|
|
113
|
+
"action": {
|
|
114
|
+
"enum": ["link", "embed"]
|
|
115
|
+
},
|
|
116
|
+
"anchor": {
|
|
117
|
+
"type": "string"
|
|
118
|
+
},
|
|
119
|
+
"linkSyntax": {
|
|
120
|
+
"enum": ["wikilink", "markdown"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"required": ["text", "target", "action", "linkSyntax"],
|
|
124
|
+
"additionalProperties": false
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"source": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"properties": {
|
|
130
|
+
"file": {
|
|
131
|
+
"type": "string"
|
|
132
|
+
},
|
|
133
|
+
"line": {
|
|
134
|
+
"type": "integer"
|
|
135
|
+
},
|
|
136
|
+
"column": {
|
|
137
|
+
"type": "integer"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"required": ["file"],
|
|
141
|
+
"additionalProperties": false
|
|
142
|
+
},
|
|
143
|
+
"resolvedType": {
|
|
144
|
+
"type": "string"
|
|
145
|
+
},
|
|
146
|
+
"resolvedParents": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"items": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"id": {
|
|
152
|
+
"type": "string"
|
|
153
|
+
},
|
|
154
|
+
"field": {
|
|
155
|
+
"type": "string"
|
|
156
|
+
},
|
|
157
|
+
"kind": {
|
|
158
|
+
"enum": ["hierarchy", "relationship"]
|
|
159
|
+
},
|
|
160
|
+
"hierarchy": {
|
|
161
|
+
"type": "string"
|
|
162
|
+
},
|
|
163
|
+
"selfRef": {
|
|
164
|
+
"type": "boolean"
|
|
165
|
+
},
|
|
166
|
+
"fieldOn": {
|
|
167
|
+
"enum": ["child", "parent"]
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": ["id", "field", "kind", "selfRef", "fieldOn"],
|
|
171
|
+
"additionalProperties": false
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"resolvedLinks": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"text": {
|
|
180
|
+
"type": "string"
|
|
181
|
+
},
|
|
182
|
+
"target": {
|
|
183
|
+
"type": "string"
|
|
184
|
+
},
|
|
185
|
+
"action": {
|
|
186
|
+
"enum": ["link", "embed"]
|
|
187
|
+
},
|
|
188
|
+
"anchor": {
|
|
189
|
+
"type": "string"
|
|
190
|
+
},
|
|
191
|
+
"location": {
|
|
192
|
+
"enum": ["node", "internal", "external", "system", "protocol"]
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"required": ["text", "target", "action", "location"],
|
|
196
|
+
"additionalProperties": false
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": [
|
|
201
|
+
"id",
|
|
202
|
+
"title",
|
|
203
|
+
"schemaData",
|
|
204
|
+
"linkTargets",
|
|
205
|
+
"type",
|
|
206
|
+
"resolvedType",
|
|
207
|
+
"resolvedParents",
|
|
208
|
+
"resolvedLinks"
|
|
209
|
+
],
|
|
210
|
+
"additionalProperties": false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"required": ["formatVersion", "hierarchies", "nodes"],
|
|
215
|
+
"additionalProperties": false
|
|
216
|
+
}
|