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.
- package/README.md +348 -0
- package/dist/commands/diagram.d.ts +5 -0
- package/dist/commands/diagram.js +12 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +6 -0
- package/dist/commands/plugins.d.ts +1 -0
- package/dist/commands/plugins.js +23 -0
- package/dist/commands/render.d.ts +6 -0
- package/dist/commands/render.js +35 -0
- package/dist/commands/schemas.d.ts +6 -0
- package/dist/commands/schemas.js +268 -0
- package/dist/commands/show.d.ts +4 -0
- package/dist/commands/show.js +7 -0
- package/dist/commands/spaces.d.ts +1 -0
- package/dist/commands/spaces.js +36 -0
- package/dist/commands/template-sync.d.ts +3 -0
- package/dist/commands/template-sync.js +13 -0
- package/dist/commands/validate-file.d.ts +28 -0
- package/dist/commands/validate-file.js +133 -0
- package/dist/commands/validate.d.ts +16 -0
- package/dist/commands/validate.js +349 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +179 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/filter/augment-nodes.d.ts +23 -0
- package/dist/filter/augment-nodes.js +95 -0
- package/dist/filter/expand-include.d.ts +62 -0
- package/dist/filter/expand-include.js +181 -0
- package/dist/filter/filter-nodes.d.ts +21 -0
- package/dist/filter/filter-nodes.js +73 -0
- package/dist/filter/parse-expression.d.ts +20 -0
- package/dist/filter/parse-expression.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -0
- package/dist/integrations/miro/cache.d.ts +21 -0
- package/dist/integrations/miro/cache.js +55 -0
- package/dist/integrations/miro/client.d.ts +99 -0
- package/dist/integrations/miro/client.js +118 -0
- package/dist/integrations/miro/layout.d.ts +28 -0
- package/dist/integrations/miro/layout.js +72 -0
- package/dist/integrations/miro/styles.d.ts +11 -0
- package/dist/integrations/miro/styles.js +65 -0
- package/dist/integrations/miro/sync.d.ts +8 -0
- package/dist/integrations/miro/sync.js +347 -0
- package/dist/plugin-api.d.ts +12 -0
- package/dist/plugin-api.js +7 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/loader.d.ts +21 -0
- package/dist/plugins/loader.js +104 -0
- package/dist/plugins/markdown/index.d.ts +48 -0
- package/dist/plugins/markdown/index.js +51 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
- package/dist/plugins/markdown/parse-embedded.js +663 -0
- package/dist/plugins/markdown/read-space.d.ts +7 -0
- package/dist/plugins/markdown/read-space.js +89 -0
- package/dist/plugins/markdown/render-bullets.d.ts +2 -0
- package/dist/plugins/markdown/render-bullets.js +42 -0
- package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
- package/dist/plugins/markdown/render-mermaid.js +57 -0
- package/dist/plugins/markdown/template-sync.d.ts +16 -0
- package/dist/plugins/markdown/template-sync.js +294 -0
- package/dist/plugins/markdown/util.d.ts +19 -0
- package/dist/plugins/markdown/util.js +80 -0
- package/dist/plugins/util.d.ts +60 -0
- package/dist/plugins/util.js +7 -0
- package/dist/read/read-space.d.ts +2 -0
- package/dist/read/read-space.js +22 -0
- package/dist/read/resolve-graph-edges.d.ts +11 -0
- package/dist/read/resolve-graph-edges.js +201 -0
- package/dist/read/wikilink-utils.d.ts +16 -0
- package/dist/read/wikilink-utils.js +38 -0
- package/dist/render/registry.d.ts +13 -0
- package/dist/render/registry.js +22 -0
- package/dist/render/render.d.ts +4 -0
- package/dist/render/render.js +28 -0
- package/dist/schema/evaluate-rule.d.ts +30 -0
- package/dist/schema/evaluate-rule.js +82 -0
- package/dist/schema/metadata-contract.d.ts +538 -0
- package/dist/schema/metadata-contract.js +115 -0
- package/dist/schema/schema-refs.d.ts +22 -0
- package/dist/schema/schema-refs.js +168 -0
- package/dist/schema/schema.d.ts +27 -0
- package/dist/schema/schema.js +378 -0
- package/dist/schema/validate-graph.d.ts +24 -0
- package/dist/schema/validate-graph.js +141 -0
- package/dist/schema/validate-rules.d.ts +10 -0
- package/dist/schema/validate-rules.js +51 -0
- package/dist/schemas/_ost_strict.json +81 -0
- package/dist/schemas/_sctx_base.json +72 -0
- package/dist/schemas/general.json +261 -0
- package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/dist/schemas/knowledge_wiki.json +206 -0
- package/dist/schemas/strict_ost.json +97 -0
- package/dist/space-graph.d.ts +28 -0
- package/dist/space-graph.js +82 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +0 -0
- package/docs/concepts.md +391 -0
- package/docs/config.md +140 -0
- package/docs/rules.md +120 -0
- package/docs/schemas.md +340 -0
- package/package.json +69 -0
- package/schemas/_ost_strict.json +81 -0
- package/schemas/_sctx_base.json +72 -0
- package/schemas/general.json +261 -0
- package/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/schemas/knowledge_wiki.json +206 -0
- 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,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,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
|
+
}
|