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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -13
  3. package/dist/api.d.ts +5 -2
  4. package/dist/api.js +4 -0
  5. package/dist/browser.d.ts +14 -0
  6. package/dist/browser.js +13 -0
  7. package/dist/commands/dump.d.ts +2 -0
  8. package/dist/commands/dump.js +3 -1
  9. package/dist/commands/schemas.d.ts +3 -1
  10. package/dist/commands/schemas.js +25 -53
  11. package/dist/commands/spaces.js +4 -6
  12. package/dist/commands/validate.js +20 -19
  13. package/dist/config/health.d.ts +10 -6
  14. package/dist/config/health.js +11 -44
  15. package/dist/config/index.d.ts +4 -0
  16. package/dist/config/index.js +15 -6
  17. package/dist/filter/augment-nodes.d.ts +2 -2
  18. package/dist/filter/augment-nodes.js +2 -2
  19. package/dist/filter/expand-include.js +2 -2
  20. package/dist/index.js +12 -3
  21. package/dist/judge/context.js +1 -1
  22. package/dist/load-space-graph.js +1 -13
  23. package/dist/plugins/markdown/parse-embedded.js +4 -4
  24. package/dist/plugins/miro/cache.js +1 -1
  25. package/dist/plugins/miro/sync.js +2 -2
  26. package/dist/read/resolve-graph-edges.js +3 -3
  27. package/dist/schema/list-schemas.d.ts +27 -0
  28. package/dist/schema/list-schemas.js +108 -0
  29. package/dist/schema/metadata-contract.d.ts +34 -0
  30. package/dist/schema/metadata-contract.js +2 -2
  31. package/dist/schema/validate-graph.d.ts +2 -2
  32. package/dist/schema/validate-graph.js +4 -4
  33. package/dist/schemas/generated/space-graph/1.json +216 -0
  34. package/dist/schemas/generated/space-graph/latest.json +216 -0
  35. package/dist/space-context.js +11 -1
  36. package/dist/space-graph-data.d.ts +237 -0
  37. package/dist/space-graph-data.js +129 -0
  38. package/dist/space-graph.d.ts +12 -1
  39. package/dist/space-graph.js +25 -1
  40. package/dist/testing.js +3 -3
  41. package/dist/types.d.ts +2 -2
  42. package/docs/concepts.md +8 -4
  43. package/package.json +5 -1
  44. package/schemas/generated/space-graph/1.json +216 -0
  45. 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 { listSchemas, showSchema } from './commands/schemas';
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
- .action(async () => await listSchemas());
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
- program.parse();
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
+ });
@@ -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.source === 'hierarchy' && p.hierarchy === hierarchyName)
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) ?? [];
@@ -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
- source: 'relationship',
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
- source: 'hierarchy',
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
- source: 'hierarchy',
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.source === 'hierarchy' && typeIdx !== -1 && parentIdx !== -1 && typeIdx < parentIdx;
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.source === 'hierarchy').map((r) => r.id),
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, source, hierarchy } of node.resolvedParents) {
261
- if (source !== 'hierarchy' || hierarchy !== graph.hierarchy?.name)
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 source Whether this edge comes from hierarchy levels or relationships
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, source, hierarchy, selfRef, unresolvedRefs, typeAliases) {
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
- source,
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', source:'hierarchy'): validates level ordering within the edge's hierarchy.
20
- * For relationship edges (fieldOn:'child', source:'relationship'): validates parent type.
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', source:'hierarchy'): validates level ordering within the edge's hierarchy.
20
- * For relationship edges (fieldOn:'child', source:'relationship'): validates parent type.
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.source === 'relationship') {
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', source:'hierarchy'): validate level ordering within its hierarchy.
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
+ }