structured-context 0.9.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 (112) hide show
  1. package/README.md +348 -0
  2. package/dist/commands/diagram.d.ts +5 -0
  3. package/dist/commands/diagram.js +12 -0
  4. package/dist/commands/docs.d.ts +1 -0
  5. package/dist/commands/docs.js +67 -0
  6. package/dist/commands/dump.d.ts +2 -0
  7. package/dist/commands/dump.js +6 -0
  8. package/dist/commands/plugins.d.ts +1 -0
  9. package/dist/commands/plugins.js +23 -0
  10. package/dist/commands/render.d.ts +6 -0
  11. package/dist/commands/render.js +35 -0
  12. package/dist/commands/schemas.d.ts +6 -0
  13. package/dist/commands/schemas.js +268 -0
  14. package/dist/commands/show.d.ts +4 -0
  15. package/dist/commands/show.js +7 -0
  16. package/dist/commands/spaces.d.ts +1 -0
  17. package/dist/commands/spaces.js +36 -0
  18. package/dist/commands/template-sync.d.ts +3 -0
  19. package/dist/commands/template-sync.js +13 -0
  20. package/dist/commands/validate-file.d.ts +28 -0
  21. package/dist/commands/validate-file.js +133 -0
  22. package/dist/commands/validate.d.ts +16 -0
  23. package/dist/commands/validate.js +349 -0
  24. package/dist/config.d.ts +38 -0
  25. package/dist/config.js +179 -0
  26. package/dist/constants.d.ts +6 -0
  27. package/dist/constants.js +6 -0
  28. package/dist/filter/augment-nodes.d.ts +23 -0
  29. package/dist/filter/augment-nodes.js +95 -0
  30. package/dist/filter/expand-include.d.ts +62 -0
  31. package/dist/filter/expand-include.js +181 -0
  32. package/dist/filter/filter-nodes.d.ts +21 -0
  33. package/dist/filter/filter-nodes.js +73 -0
  34. package/dist/filter/parse-expression.d.ts +20 -0
  35. package/dist/filter/parse-expression.js +60 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +161 -0
  38. package/dist/integrations/miro/cache.d.ts +21 -0
  39. package/dist/integrations/miro/cache.js +55 -0
  40. package/dist/integrations/miro/client.d.ts +99 -0
  41. package/dist/integrations/miro/client.js +118 -0
  42. package/dist/integrations/miro/layout.d.ts +28 -0
  43. package/dist/integrations/miro/layout.js +72 -0
  44. package/dist/integrations/miro/styles.d.ts +11 -0
  45. package/dist/integrations/miro/styles.js +65 -0
  46. package/dist/integrations/miro/sync.d.ts +8 -0
  47. package/dist/integrations/miro/sync.js +347 -0
  48. package/dist/plugin-api.d.ts +12 -0
  49. package/dist/plugin-api.js +7 -0
  50. package/dist/plugins/index.d.ts +3 -0
  51. package/dist/plugins/index.js +3 -0
  52. package/dist/plugins/loader.d.ts +21 -0
  53. package/dist/plugins/loader.js +104 -0
  54. package/dist/plugins/markdown/index.d.ts +48 -0
  55. package/dist/plugins/markdown/index.js +51 -0
  56. package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
  57. package/dist/plugins/markdown/parse-embedded.js +663 -0
  58. package/dist/plugins/markdown/read-space.d.ts +7 -0
  59. package/dist/plugins/markdown/read-space.js +89 -0
  60. package/dist/plugins/markdown/render-bullets.d.ts +2 -0
  61. package/dist/plugins/markdown/render-bullets.js +42 -0
  62. package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
  63. package/dist/plugins/markdown/render-mermaid.js +57 -0
  64. package/dist/plugins/markdown/template-sync.d.ts +16 -0
  65. package/dist/plugins/markdown/template-sync.js +294 -0
  66. package/dist/plugins/markdown/util.d.ts +19 -0
  67. package/dist/plugins/markdown/util.js +80 -0
  68. package/dist/plugins/util.d.ts +60 -0
  69. package/dist/plugins/util.js +7 -0
  70. package/dist/read/read-space.d.ts +2 -0
  71. package/dist/read/read-space.js +22 -0
  72. package/dist/read/resolve-graph-edges.d.ts +11 -0
  73. package/dist/read/resolve-graph-edges.js +201 -0
  74. package/dist/read/wikilink-utils.d.ts +16 -0
  75. package/dist/read/wikilink-utils.js +38 -0
  76. package/dist/render/registry.d.ts +13 -0
  77. package/dist/render/registry.js +22 -0
  78. package/dist/render/render.d.ts +4 -0
  79. package/dist/render/render.js +28 -0
  80. package/dist/schema/evaluate-rule.d.ts +30 -0
  81. package/dist/schema/evaluate-rule.js +82 -0
  82. package/dist/schema/metadata-contract.d.ts +538 -0
  83. package/dist/schema/metadata-contract.js +115 -0
  84. package/dist/schema/schema-refs.d.ts +22 -0
  85. package/dist/schema/schema-refs.js +168 -0
  86. package/dist/schema/schema.d.ts +27 -0
  87. package/dist/schema/schema.js +378 -0
  88. package/dist/schema/validate-graph.d.ts +24 -0
  89. package/dist/schema/validate-graph.js +141 -0
  90. package/dist/schema/validate-rules.d.ts +10 -0
  91. package/dist/schema/validate-rules.js +51 -0
  92. package/dist/schemas/_ost_strict.json +81 -0
  93. package/dist/schemas/_sctx_base.json +72 -0
  94. package/dist/schemas/general.json +261 -0
  95. package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
  96. package/dist/schemas/knowledge_wiki.json +206 -0
  97. package/dist/schemas/strict_ost.json +97 -0
  98. package/dist/space-graph.d.ts +28 -0
  99. package/dist/space-graph.js +82 -0
  100. package/dist/types.d.ts +145 -0
  101. package/dist/types.js +0 -0
  102. package/docs/concepts.md +391 -0
  103. package/docs/config.md +140 -0
  104. package/docs/rules.md +120 -0
  105. package/docs/schemas.md +340 -0
  106. package/package.json +69 -0
  107. package/schemas/_ost_strict.json +81 -0
  108. package/schemas/_sctx_base.json +72 -0
  109. package/schemas/general.json +261 -0
  110. package/schemas/generated/_structured_context_schema_meta.json +191 -0
  111. package/schemas/knowledge_wiki.json +206 -0
  112. package/schemas/strict_ost.json +97 -0
@@ -0,0 +1,7 @@
1
+ import { PLUGIN_PREFIX as _PLUGIN_PREFIX } from '../constants';
2
+ export const PLUGIN_PREFIX = _PLUGIN_PREFIX;
3
+ export const CONFIG_PLUGINS_DIR = 'plugins';
4
+ /** Normalize a plugin name to its canonical prefixed form. */
5
+ export function normalizePluginName(name) {
6
+ return name.startsWith(PLUGIN_PREFIX) ? name : `${PLUGIN_PREFIX}${name}`;
7
+ }
@@ -0,0 +1,2 @@
1
+ import type { ReadSpaceResult, SpaceContext } from '../types';
2
+ export declare function readSpace(context: SpaceContext): Promise<ReadSpaceResult>;
@@ -0,0 +1,22 @@
1
+ import { loadPlugins } from '../plugins/loader';
2
+ import { resolveGraphEdges } from './resolve-graph-edges';
3
+ export async function readSpace(context) {
4
+ const pluginMap = context.space?.plugins ?? {};
5
+ const loaded = await loadPlugins(pluginMap, context.configDir);
6
+ for (const { plugin, pluginConfig } of loaded) {
7
+ if (!plugin.parse)
8
+ continue;
9
+ const result = await plugin.parse({ ...context, pluginConfig });
10
+ if (result !== null) {
11
+ const { nodes, unresolvedRefs } = resolveGraphEdges(result.nodes, context.schema.metadata);
12
+ return {
13
+ nodes,
14
+ source: plugin.name,
15
+ parseIgnored: result.parseIgnored,
16
+ diagnostics: result.diagnostics,
17
+ unresolvedRefs,
18
+ };
19
+ }
20
+ }
21
+ throw new Error(`No plugin handled space at: ${context.space.path}`);
22
+ }
@@ -0,0 +1,11 @@
1
+ import type { BaseNode, SchemaMetadata, SpaceNode, UnresolvedRef } from '../types';
2
+ /**
3
+ * Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
4
+ * parent links using the hierarchy levels and relationships from schema metadata.
5
+ *
6
+ * Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
7
+ */
8
+ export declare function resolveGraphEdges(nodes: BaseNode[], metadata: SchemaMetadata): {
9
+ nodes: SpaceNode[];
10
+ unresolvedRefs: UnresolvedRef[];
11
+ };
@@ -0,0 +1,201 @@
1
+ import { resolveNodeType } from '../schema/schema';
2
+ import { buildTargetIndex, wikilinkToTarget } from './wikilink-utils';
3
+ /**
4
+ * Extract wikilink refs from a field value.
5
+ * If multiple=true: expects an array; returns string elements.
6
+ * If multiple=false: expects a single string; returns it in a one-element array.
7
+ */
8
+ function getRefs(rawField, multiple) {
9
+ if (multiple) {
10
+ return Array.isArray(rawField) ? rawField.filter((v) => typeof v === 'string') : [];
11
+ }
12
+ return typeof rawField === 'string' ? [rawField] : [];
13
+ }
14
+ /**
15
+ * Resolves parent-child links for a specific edge definition, pushing a ResolvedParentRef
16
+ * onto each child's resolvedParents array. Records unresolved refs for broken/invalid links.
17
+ *
18
+ * For all edges, any existing node is accepted as the target (permissive).
19
+ * Type correctness is enforced by validateHierarchyStructure via resolvedParents, not here.
20
+ *
21
+ * @param nodesByType Map of node type to nodes
22
+ * @param targetIndex Map of link targets to nodes
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
25
+ * @param selfRef Whether child and parent are the same node type
26
+ * @param unresolvedRefs Array to push broken/invalid link entries into
27
+ * @param typeAliases Optional type aliases for resolution
28
+ */
29
+ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolvedRefs, typeAliases) {
30
+ const { type: rawChildType, parent: rawParentType, field, fieldOn, multiple } = edge;
31
+ const childType = resolveNodeType(rawChildType, typeAliases);
32
+ const parentType = resolveNodeType(rawParentType, typeAliases);
33
+ function pushParentRef(childNode, parentTitle) {
34
+ // Deduplicate by (field, title) — same parent via different fields is intentional
35
+ if (!childNode.resolvedParents.some((r) => r.field === field && r.title === parentTitle)) {
36
+ childNode.resolvedParents.push({ title: parentTitle, field, fieldOn, source, selfRef });
37
+ }
38
+ }
39
+ function checkShape(ownerNode, rawField) {
40
+ if (multiple && !Array.isArray(rawField)) {
41
+ unresolvedRefs.push({
42
+ label: ownerNode.label,
43
+ ref: String(rawField),
44
+ field,
45
+ reason: 'invalid_shape',
46
+ message: `Field "${field}" must be an array of wikilinks, got ${typeof rawField}`,
47
+ });
48
+ return false;
49
+ }
50
+ if (!multiple && typeof rawField !== 'string') {
51
+ unresolvedRefs.push({
52
+ label: ownerNode.label,
53
+ ref: String(rawField),
54
+ field,
55
+ reason: 'invalid_shape',
56
+ message: `Field "${field}" must be a wikilink string, got ${typeof rawField}`,
57
+ });
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+ function checkAndRecordRef(ownerNode, ref) {
63
+ const target = wikilinkToTarget(ref);
64
+ const resolved = targetIndex.get(target);
65
+ if (resolved === undefined) {
66
+ unresolvedRefs.push({
67
+ label: ownerNode.label,
68
+ ref,
69
+ field,
70
+ reason: 'not_found',
71
+ message: `Link target "${target}" in field "${field}" not found`,
72
+ });
73
+ return null;
74
+ }
75
+ if (resolved === null) {
76
+ unresolvedRefs.push({
77
+ label: ownerNode.label,
78
+ ref,
79
+ field,
80
+ reason: 'ambiguous',
81
+ message: `Link target "${target}" in field "${field}" is ambiguous (matches multiple nodes)`,
82
+ });
83
+ return null;
84
+ }
85
+ return resolved;
86
+ }
87
+ if (fieldOn === 'parent') {
88
+ // Parent nodes have the field pointing to children; resolve permissively (any target type).
89
+ // Type correctness is enforced by validateHierarchyStructure, not the resolver.
90
+ for (const parentNode of nodesByType.get(parentType) ?? []) {
91
+ const rawField = parentNode.schemaData[field];
92
+ if (rawField === undefined || rawField === null)
93
+ continue;
94
+ if (!checkShape(parentNode, rawField))
95
+ continue;
96
+ const refs = getRefs(rawField, multiple);
97
+ for (const ref of refs) {
98
+ const childNode = checkAndRecordRef(parentNode, ref);
99
+ if (!childNode)
100
+ continue;
101
+ const parentTitle = parentNode.schemaData.title;
102
+ if (typeof parentTitle !== 'string')
103
+ continue;
104
+ pushParentRef(childNode, parentTitle);
105
+ }
106
+ }
107
+ }
108
+ else {
109
+ // Child nodes have the field pointing to parents; accept any resolved target
110
+ for (const childNode of nodesByType.get(childType) ?? []) {
111
+ const rawField = childNode.schemaData[field];
112
+ if (rawField === undefined || rawField === null)
113
+ continue;
114
+ if (!checkShape(childNode, rawField))
115
+ continue;
116
+ const refs = getRefs(rawField, multiple);
117
+ for (const ref of refs) {
118
+ const parentNode = checkAndRecordRef(childNode, ref);
119
+ if (!parentNode)
120
+ continue;
121
+ const parentTitle = parentNode.schemaData.title;
122
+ if (typeof parentTitle !== 'string')
123
+ continue;
124
+ pushParentRef(childNode, parentTitle);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ /**
130
+ * Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
131
+ * parent links using the hierarchy levels and relationships from schema metadata.
132
+ *
133
+ * Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
134
+ */
135
+ export function resolveGraphEdges(nodes, metadata) {
136
+ const levels = metadata.hierarchy?.levels ?? [];
137
+ const relationships = metadata.relationships ?? [];
138
+ const typeAliases = metadata.typeAliases;
139
+ const unresolvedRefs = [];
140
+ const spaceNodes = nodes.map((n) => ({
141
+ ...n,
142
+ resolvedType: resolveNodeType(n.type, typeAliases),
143
+ resolvedParents: [],
144
+ }));
145
+ const targetIndex = buildTargetIndex(spaceNodes);
146
+ // Build nodesByType map
147
+ const nodesByType = new Map();
148
+ for (const node of spaceNodes) {
149
+ const type = node.resolvedType;
150
+ if (!nodesByType.has(type)) {
151
+ nodesByType.set(type, []);
152
+ }
153
+ nodesByType.get(type).push(node);
154
+ }
155
+ // 1. Process hierarchy levels
156
+ for (let i = 0; i < levels.length; i++) {
157
+ const level = levels[i];
158
+ // Regular relationship (child type → parent type)
159
+ if (i > 0) {
160
+ const parentLevel = levels[i - 1];
161
+ resolveEdge(nodesByType, targetIndex, {
162
+ type: level.type,
163
+ parent: parentLevel.type,
164
+ field: level.field,
165
+ fieldOn: level.fieldOn,
166
+ multiple: level.multiple,
167
+ }, 'hierarchy', false, unresolvedRefs, typeAliases);
168
+ }
169
+ // Same-type relationship (child type → same type) via primary field
170
+ if (level.selfRef) {
171
+ resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.field, fieldOn: level.fieldOn, multiple: level.multiple }, 'hierarchy', true, unresolvedRefs, typeAliases);
172
+ }
173
+ // Same-type relationship via explicit selfRefField
174
+ if (level.selfRefField) {
175
+ resolveEdge(nodesByType, targetIndex, { type: level.type, parent: level.type, field: level.selfRefField, fieldOn: 'child', multiple: false }, 'hierarchy', true, unresolvedRefs, typeAliases);
176
+ }
177
+ }
178
+ // 2. Process additional relationships
179
+ for (const rel of relationships) {
180
+ const edge = {
181
+ type: rel.type,
182
+ parent: rel.parent,
183
+ field: rel.field,
184
+ fieldOn: rel.fieldOn,
185
+ multiple: rel.multiple,
186
+ };
187
+ resolveEdge(nodesByType, targetIndex, edge, 'relationship', rel.type === rel.parent, unresolvedRefs, typeAliases);
188
+ }
189
+ // Deduplicate by (label, field, ref) — the same broken link may be encountered across
190
+ // multiple resolveEdge calls (e.g. selfRef + regular hierarchy share the same field).
191
+ const seen = new Set();
192
+ const deduped = [];
193
+ for (const u of unresolvedRefs) {
194
+ const key = `${u.label}\0${u.field}\0${u.ref}`;
195
+ if (!seen.has(key)) {
196
+ seen.add(key);
197
+ deduped.push(u);
198
+ }
199
+ }
200
+ return { nodes: spaceNodes, unresolvedRefs: deduped };
201
+ }
@@ -0,0 +1,16 @@
1
+ import type { BaseNode } from '../types';
2
+ /**
3
+ * Extract the lookup key from a wikilink string such as:
4
+ * [[Personal Vision]] → "Personal Vision"
5
+ * [[Personal Vision#Our Mission]] → "Personal Vision#Our Mission"
6
+ * [[vision_page#^ourmission]] → "vision_page#^ourmission"
7
+ */
8
+ export declare function wikilinkToTarget(wikilink: string): string;
9
+ /**
10
+ * Builds a fast lookup index mapping link targets to nodes.
11
+ * Used for both hierarchy and relationship validation.
12
+ *
13
+ * @param nodes The complete set of nodes (BaseNode, SpaceNode, or subtypes)
14
+ * @returns Map of target strings to nodes. If a target is ambiguous (points to multiple nodes), its value is null.
15
+ */
16
+ export declare function buildTargetIndex<T extends BaseNode>(nodes: T[]): Map<string, T | null>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Extract the lookup key from a wikilink string such as:
3
+ * [[Personal Vision]] → "Personal Vision"
4
+ * [[Personal Vision#Our Mission]] → "Personal Vision#Our Mission"
5
+ * [[vision_page#^ourmission]] → "vision_page#^ourmission"
6
+ */
7
+ export function wikilinkToTarget(wikilink) {
8
+ const cleaned = wikilink.replace(/^"|"$/g, '').trim();
9
+ if (!cleaned.startsWith('[[') || !cleaned.endsWith(']]')) {
10
+ return cleaned;
11
+ }
12
+ return cleaned.slice(2, -2).trim();
13
+ }
14
+ /**
15
+ * Builds a fast lookup index mapping link targets to nodes.
16
+ * Used for both hierarchy and relationship validation.
17
+ *
18
+ * @param nodes The complete set of nodes (BaseNode, SpaceNode, or subtypes)
19
+ * @returns Map of target strings to nodes. If a target is ambiguous (points to multiple nodes), its value is null.
20
+ */
21
+ export function buildTargetIndex(nodes) {
22
+ const index = new Map();
23
+ for (const node of nodes) {
24
+ for (const target of node.linkTargets) {
25
+ const normalized = target.trim();
26
+ if (!normalized)
27
+ continue; // Skip empty strings after trimming
28
+ const existing = index.get(normalized);
29
+ if (existing === undefined) {
30
+ index.set(normalized, node);
31
+ }
32
+ else if (existing !== node) {
33
+ index.set(normalized, null); // mark as ambiguous
34
+ }
35
+ }
36
+ }
37
+ return index;
38
+ }
@@ -0,0 +1,13 @@
1
+ import type { LoadedPlugin } from '../plugins/loader';
2
+ import type { RenderFormat } from '../plugins/util';
3
+ export type ResolvedFormat = {
4
+ qualifiedName: string;
5
+ format: RenderFormat;
6
+ plugin: LoadedPlugin;
7
+ };
8
+ /**
9
+ * Build a registry of all render formats from loaded plugins.
10
+ * Formats are namespaced as `{shortPluginName}.{formatName}` where
11
+ * shortPluginName strips the `sctx-` prefix.
12
+ */
13
+ export declare function buildFormatRegistry(loaded: LoadedPlugin[]): ResolvedFormat[];
@@ -0,0 +1,22 @@
1
+ import { PLUGIN_PREFIX } from '../plugins/util';
2
+ /**
3
+ * Build a registry of all render formats from loaded plugins.
4
+ * Formats are namespaced as `{shortPluginName}.{formatName}` where
5
+ * shortPluginName strips the `sctx-` prefix.
6
+ */
7
+ export function buildFormatRegistry(loaded) {
8
+ const registry = [];
9
+ for (const lp of loaded) {
10
+ if (!lp.plugin.render)
11
+ continue;
12
+ const shortName = lp.plugin.name.replace(PLUGIN_PREFIX, '');
13
+ for (const format of lp.plugin.render.formats) {
14
+ registry.push({
15
+ qualifiedName: `${shortName}.${format.name}`,
16
+ format,
17
+ plugin: lp,
18
+ });
19
+ }
20
+ }
21
+ return registry;
22
+ }
@@ -0,0 +1,4 @@
1
+ import type { SpaceContext } from '../types';
2
+ export declare function executeRender(formatName: string, context: SpaceContext, options: {
3
+ filter?: string;
4
+ }): Promise<string>;
@@ -0,0 +1,28 @@
1
+ import { filterNodes } from '../filter/filter-nodes';
2
+ import { loadPlugins } from '../plugins/loader';
3
+ import { readSpace } from '../read/read-space';
4
+ import { buildSpaceGraph } from '../space-graph';
5
+ import { buildFormatRegistry } from './registry';
6
+ export async function executeRender(formatName, context, options) {
7
+ const pluginMap = context.space?.plugins ?? {};
8
+ const loaded = await loadPlugins(pluginMap, context.configDir);
9
+ const registry = buildFormatRegistry(loaded);
10
+ const entry = registry.find((r) => r.qualifiedName === formatName);
11
+ if (!entry) {
12
+ const available = registry.map((r) => r.qualifiedName).join(', ');
13
+ throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
14
+ }
15
+ const { nodes: allNodes } = await readSpace(context);
16
+ // Validate: drop nodes that fail schema validation
17
+ const { schemaValidator } = context;
18
+ const validNodes = allNodes.filter((node) => schemaValidator(node.schemaData));
19
+ const levels = context.schema.metadata.hierarchy?.levels ?? [];
20
+ let graph = buildSpaceGraph(validNodes, levels);
21
+ // Filter: apply filter expression if provided
22
+ if (options.filter) {
23
+ const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
24
+ graph = await filterNodes(expression, graph);
25
+ }
26
+ const pluginContext = { ...context, pluginConfig: entry.plugin.pluginConfig };
27
+ return entry.plugin.plugin.render.render(pluginContext, graph, { format: entry.format.name });
28
+ }
@@ -0,0 +1,30 @@
1
+ import type { SpaceNode } from '../types';
2
+ /** Evaluation context for JSONata expressions */
3
+ export interface EvalContext {
4
+ /** All nodes in the space (flattened) */
5
+ nodes: Record<string, unknown>[];
6
+ /** Current node being evaluated (flattened) */
7
+ $$: Record<string, unknown>;
8
+ /** First resolved parent node (undefined if no parent) (flattened) — provided as a convenience */
9
+ parent?: Record<string, unknown>;
10
+ /** All resolved parent nodes (flattened) */
11
+ parents?: Record<string, unknown>[];
12
+ }
13
+ /**
14
+ * Evaluate a JSONata expression against a context.
15
+ * Returns the result of the expression (boolean for rule checks).
16
+ *
17
+ * @param expr - JSONata expression string
18
+ * @param context - Evaluation context with $, $$, parent, $lookup
19
+ * @returns Result of expression evaluation
20
+ */
21
+ export declare function evaluateExpression(expr: string, context: EvalContext): Promise<boolean | string | number>;
22
+ /**
23
+ * Build an evaluation context for a given node.
24
+ *
25
+ * @param node - The node to build context for
26
+ * @param allNodes - All nodes in the space
27
+ * @param nodeIndex - Map of node titles to nodes for efficient lookup
28
+ * @returns Evaluation context for the node
29
+ */
30
+ export declare function buildEvalContext(node: SpaceNode, allNodes: SpaceNode[], nodeIndex: Map<string, SpaceNode>): EvalContext;
@@ -0,0 +1,82 @@
1
+ import jsonata from 'jsonata';
2
+ const expressionCache = new Map();
3
+ /**
4
+ * Evaluate a JSONata expression against a context.
5
+ * Returns the result of the expression (boolean for rule checks).
6
+ *
7
+ * @param expr - JSONata expression string
8
+ * @param context - Evaluation context with $, $$, parent, $lookup
9
+ * @returns Result of expression evaluation
10
+ */
11
+ export async function evaluateExpression(expr, context) {
12
+ // Build a structured input object for JSONata
13
+ // This allows expressions to access nodes, current, and parent as properties
14
+ const input = {
15
+ nodes: context.nodes,
16
+ current: context.$$,
17
+ };
18
+ // Only add parent if it exists (not undefined)
19
+ if (context.parent !== undefined) {
20
+ input.parent = context.parent;
21
+ }
22
+ // Add parents array if present
23
+ if (context.parents !== undefined) {
24
+ input.parents = context.parents;
25
+ }
26
+ try {
27
+ let expression = expressionCache.get(expr);
28
+ if (!expression) {
29
+ expression = jsonata(expr);
30
+ expressionCache.set(expr, expression);
31
+ }
32
+ // Pass the structured input - expressions access nodes, current, parent from it
33
+ const result = await expression.evaluate(input);
34
+ return result;
35
+ }
36
+ catch (error) {
37
+ // Log warning and return false (fail safe)
38
+ console.warn(`Warning: Error evaluating expression "${expr}":`, error);
39
+ return false;
40
+ }
41
+ }
42
+ /**
43
+ * Flatten a SpaceNode for JSONata evaluation.
44
+ * Creates a new object with schemaData properties at the top level.
45
+ *
46
+ * @param node - The node to flatten
47
+ * @returns Flattened node with properties directly accessible
48
+ */
49
+ function flattenNode(node) {
50
+ return {
51
+ ...node.schemaData,
52
+ resolvedType: node.resolvedType,
53
+ resolvedParentTitle: node.resolvedParents[0]?.title, // first parent or undefined, provided for convenience
54
+ resolvedParentTitles: node.resolvedParents.map((r) => r.title), // full array of parent titles
55
+ };
56
+ }
57
+ /**
58
+ * Build an evaluation context for a given node.
59
+ *
60
+ * @param node - The node to build context for
61
+ * @param allNodes - All nodes in the space
62
+ * @param nodeIndex - Map of node titles to nodes for efficient lookup
63
+ * @returns Evaluation context for the node
64
+ */
65
+ export function buildEvalContext(node, allNodes, nodeIndex) {
66
+ // Flatten all nodes for JSONata access
67
+ const flattenedNodes = allNodes.map(flattenNode);
68
+ // Build all parent objects from resolvedParents array
69
+ const flattenedParents = [];
70
+ for (const { title: parentTitle } of node.resolvedParents) {
71
+ const parentNode = nodeIndex.get(parentTitle);
72
+ if (parentNode) {
73
+ flattenedParents.push(flattenNode(parentNode));
74
+ }
75
+ }
76
+ return {
77
+ nodes: flattenedNodes,
78
+ $$: flattenNode(node),
79
+ parent: flattenedParents[0],
80
+ parents: flattenedParents.length > 0 ? flattenedParents : undefined,
81
+ };
82
+ }