structured-context 0.10.0 → 0.12.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 +85 -25
- package/dist/api.d.ts +25 -5
- package/dist/api.js +22 -2
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/miro-sync.d.ts +3 -0
- package/dist/commands/miro-sync.js +8 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +117 -24
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -4
- package/dist/commands/validate-file.js +20 -5
- package/dist/commands/validate.js +54 -21
- package/dist/config.d.ts +26 -13
- package/dist/config.js +104 -31
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.js +51 -35
- package/dist/load-space-graph.d.ts +42 -0
- package/dist/load-space-graph.js +53 -0
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/loader.js +4 -4
- package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
- package/dist/plugins/markdown/extract-content-links.js +123 -0
- package/dist/plugins/markdown/index.js +5 -5
- package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
- package/dist/plugins/markdown/parse-embedded.js +50 -7
- package/dist/plugins/markdown/read-space.js +41 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/mermaid/index.d.ts +2 -0
- package/dist/plugins/mermaid/index.js +14 -0
- package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -15
- package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
- package/dist/plugins/miro/index.d.ts +2 -0
- package/dist/plugins/miro/index.js +27 -0
- package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
- package/dist/{integrations → plugins}/miro/layout.js +3 -16
- package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
- package/dist/plugins/miro/sync.d.ts +7 -0
- package/dist/{integrations → plugins}/miro/sync.js +22 -39
- package/dist/plugins/util.d.ts +48 -1
- package/dist/plugins/util.js +143 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +78 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +4 -1
- package/dist/render/render.js +20 -16
- package/dist/schema/evaluate-rule.js +1 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +343 -187
- package/dist/schema/metadata-contract.js +68 -30
- package/dist/schema/schema.d.ts +17 -9
- package/dist/schema/schema.js +283 -200
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +5 -1
- package/dist/schema/validate-rules.js +20 -1
- package/dist/schemas/_knowledge_wiki.json +21 -21
- package/dist/schemas/_ost_strict.json +2 -2
- package/dist/schemas/_sctx_base.json +2 -2
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +27 -0
- package/dist/space-context.js +46 -0
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +101 -8
- package/dist/validate.d.ts +15 -3
- package/dist/validate.js +55 -20
- package/docs/concepts.md +37 -4
- package/docs/config.md +10 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +21 -21
- package/schemas/_ost_strict.json +2 -2
- package/schemas/_sctx_base.json +2 -2
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/integrations/miro/sync.d.ts +0 -8
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
- /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/cache.js +0 -0
- /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/client.js +0 -0
- /package/dist/{integrations → plugins}/miro/styles.js +0 -0
package/dist/config.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
5
5
|
import JSON5 from 'json5';
|
|
6
6
|
import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
|
|
7
|
-
import { normalizePluginName } from './plugins/util';
|
|
7
|
+
import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from './plugins/util';
|
|
8
8
|
import { bundledSchemasDir } from './schema/schema';
|
|
9
9
|
const CONFIG_SCHEMA = {
|
|
10
10
|
type: 'object',
|
|
@@ -17,8 +17,6 @@ const CONFIG_SCHEMA = {
|
|
|
17
17
|
name: { type: 'string', pattern: '^[a-z0-9_-]+$' },
|
|
18
18
|
path: { type: 'string' },
|
|
19
19
|
schema: { type: 'string' },
|
|
20
|
-
miroBoardId: { type: 'string' },
|
|
21
|
-
miroFrameId: { type: 'string' },
|
|
22
20
|
plugins: { type: 'object', additionalProperties: { type: 'object' } },
|
|
23
21
|
views: {
|
|
24
22
|
type: 'object',
|
|
@@ -37,7 +35,7 @@ const CONFIG_SCHEMA = {
|
|
|
37
35
|
schema: { type: 'string' },
|
|
38
36
|
includeSpacesFrom: { type: 'array', items: { type: 'string' } },
|
|
39
37
|
},
|
|
40
|
-
required: ['spaces'],
|
|
38
|
+
anyOf: [{ required: ['spaces'] }, { required: ['includeSpacesFrom'] }],
|
|
41
39
|
additionalProperties: false,
|
|
42
40
|
};
|
|
43
41
|
let _configPathOverride;
|
|
@@ -54,18 +52,6 @@ export function getConfigSourceFiles() {
|
|
|
54
52
|
}
|
|
55
53
|
return new Set(_spaceSourceFiles.values());
|
|
56
54
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the directory of the config file that defines a given space.
|
|
59
|
-
*/
|
|
60
|
-
export function getSpaceConfigDir(spaceName) {
|
|
61
|
-
if (_spaceSourceFiles.size === 0) {
|
|
62
|
-
loadConfig();
|
|
63
|
-
}
|
|
64
|
-
const spaceConfigPath = _spaceSourceFiles.get(spaceName);
|
|
65
|
-
if (!spaceConfigPath)
|
|
66
|
-
throw new Error('Space config path not found');
|
|
67
|
-
return dirname(spaceConfigPath);
|
|
68
|
-
}
|
|
69
55
|
export function configPath() {
|
|
70
56
|
if (_configPathOverride) {
|
|
71
57
|
return _configPathOverride;
|
|
@@ -101,13 +87,10 @@ function resolveRelativePaths(config, configDir) {
|
|
|
101
87
|
const relSchema = (p) => {
|
|
102
88
|
if (!p || isAbsolute(p) || isUrl(p))
|
|
103
89
|
return p;
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (existsSync(localPath))
|
|
107
|
-
return localPath;
|
|
108
|
-
return join(bundledSchemasDir, p);
|
|
90
|
+
if (p.startsWith('./') || p.startsWith('../')) {
|
|
91
|
+
return resolve(configDir, p);
|
|
109
92
|
}
|
|
110
|
-
return
|
|
93
|
+
return p;
|
|
111
94
|
};
|
|
112
95
|
return {
|
|
113
96
|
...config,
|
|
@@ -125,11 +108,13 @@ function _loadConfig(path) {
|
|
|
125
108
|
throw new Error(`Config file not found: ${path}`);
|
|
126
109
|
}
|
|
127
110
|
const config = JSON5.parse(readFileSync(path, 'utf-8'));
|
|
128
|
-
const ajv = new
|
|
111
|
+
const ajv = new Ajv2020();
|
|
129
112
|
const validate = ajv.compile(CONFIG_SCHEMA);
|
|
130
113
|
if (!validate(config)) {
|
|
131
114
|
throw new Error(`Invalid config in ${path}: ${JSON.stringify(validate.errors)}`);
|
|
132
115
|
}
|
|
116
|
+
if (!config.spaces)
|
|
117
|
+
config.spaces = [];
|
|
133
118
|
return config;
|
|
134
119
|
}
|
|
135
120
|
export function loadConfig() {
|
|
@@ -141,8 +126,10 @@ export function loadConfig() {
|
|
|
141
126
|
const config = _loadConfig(path);
|
|
142
127
|
_spaceSourceFiles.clear();
|
|
143
128
|
// Track which spaces come from the main config file
|
|
129
|
+
const mainSourceDir = dirname(resolve(path));
|
|
144
130
|
for (const space of config.spaces) {
|
|
145
131
|
_spaceSourceFiles.set(space.name, path);
|
|
132
|
+
space.sourceDir = mainSourceDir;
|
|
146
133
|
}
|
|
147
134
|
// Load includeSpacesFrom configs and merge their spaces in, with later entries taking precedence over earlier ones
|
|
148
135
|
if (config.includeSpacesFrom) {
|
|
@@ -154,8 +141,10 @@ export function loadConfig() {
|
|
|
154
141
|
throw new Error(`Included config contains spaces with duplicate names: ${resolvedIncludePath}`);
|
|
155
142
|
}
|
|
156
143
|
// Track which spaces come from this included config file
|
|
144
|
+
const includeSourceDir = dirname(resolvedIncludePath);
|
|
157
145
|
for (const space of includedConfig.spaces) {
|
|
158
146
|
_spaceSourceFiles.set(space.name, resolvedIncludePath);
|
|
147
|
+
space.sourceDir = includeSourceDir;
|
|
159
148
|
}
|
|
160
149
|
config.spaces.push(...includedConfig.spaces);
|
|
161
150
|
}
|
|
@@ -170,25 +159,109 @@ export function getSpaceConfig(name, config) {
|
|
|
170
159
|
}
|
|
171
160
|
return space;
|
|
172
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a schema identifier or path to an absolute file path.
|
|
164
|
+
*
|
|
165
|
+
* Precedence:
|
|
166
|
+
* 1. Absolute path or URL → used directly.
|
|
167
|
+
* 2. Explicit relative path (./... or ../...) → resolved relative to configDir.
|
|
168
|
+
* 3. Plugin-qualified path (e.g. "sctx-wardley-mapping/wardley_map.json" or "wardley-mapping/wardley_map.json"):
|
|
169
|
+
* resolved against the matching loaded plugin's schemasDir.
|
|
170
|
+
* 4. Simple filename without directory (e.g. "wardley_map.json" or "strategy_general.json"):
|
|
171
|
+
* - configDir/filename if it exists
|
|
172
|
+
* - plugin.schemasDir/filename for any loaded plugin with schemasDir
|
|
173
|
+
* - bundledSchemasDir/filename if it exists
|
|
174
|
+
* - fallback: configDir/filename
|
|
175
|
+
*/
|
|
176
|
+
export function resolveSchemaPath(rawSchema, configDir, space, config) {
|
|
177
|
+
if (!rawSchema) {
|
|
178
|
+
throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
|
|
179
|
+
}
|
|
180
|
+
if (isAbsolute(rawSchema) || isUrl(rawSchema)) {
|
|
181
|
+
return rawSchema;
|
|
182
|
+
}
|
|
183
|
+
if (rawSchema.startsWith('./') || rawSchema.startsWith('../')) {
|
|
184
|
+
return resolve(configDir, rawSchema);
|
|
185
|
+
}
|
|
186
|
+
if (rawSchema.includes('/') || rawSchema.includes('\\')) {
|
|
187
|
+
const normalizedSlash = rawSchema.replace(/\\/g, '/');
|
|
188
|
+
const slashIdx = normalizedSlash.indexOf('/');
|
|
189
|
+
const prefix = normalizedSlash.slice(0, slashIdx);
|
|
190
|
+
const subPath = normalizedSlash.slice(slashIdx + 1);
|
|
191
|
+
const pluginSchemasDir = resolvePluginSchemasDir(prefix, configDir);
|
|
192
|
+
if (pluginSchemasDir) {
|
|
193
|
+
const candidate = resolve(pluginSchemasDir, subPath);
|
|
194
|
+
if (existsSync(candidate)) {
|
|
195
|
+
return candidate;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Check if relative to configDir exists
|
|
199
|
+
const localCandidate = resolve(configDir, rawSchema);
|
|
200
|
+
if (existsSync(localCandidate)) {
|
|
201
|
+
return localCandidate;
|
|
202
|
+
}
|
|
203
|
+
// If plugin schemas dir was found, return candidate under it
|
|
204
|
+
if (pluginSchemasDir) {
|
|
205
|
+
return resolve(pluginSchemasDir, subPath);
|
|
206
|
+
}
|
|
207
|
+
return localCandidate;
|
|
208
|
+
}
|
|
209
|
+
// Simple filename without directory:
|
|
210
|
+
// 1. configDir/filename
|
|
211
|
+
const localCandidate = resolve(configDir, rawSchema);
|
|
212
|
+
if (existsSync(localCandidate)) {
|
|
213
|
+
return localCandidate;
|
|
214
|
+
}
|
|
215
|
+
// 2. Plugin schemas from declared space / config plugins
|
|
216
|
+
if (space || config) {
|
|
217
|
+
const pluginSources = resolveSpacePluginSchemas(space, config ?? { spaces: [] }, configDir);
|
|
218
|
+
for (const source of pluginSources) {
|
|
219
|
+
const pluginCandidate = resolve(source.schemasDir, rawSchema);
|
|
220
|
+
if (existsSync(pluginCandidate)) {
|
|
221
|
+
return pluginCandidate;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 3. Bundled schemas
|
|
226
|
+
const bundledCandidate = join(bundledSchemasDir, rawSchema);
|
|
227
|
+
if (existsSync(bundledCandidate)) {
|
|
228
|
+
return bundledCandidate;
|
|
229
|
+
}
|
|
230
|
+
return localCandidate;
|
|
231
|
+
}
|
|
173
232
|
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
174
|
-
export function resolveSchema(config, space) {
|
|
233
|
+
export function resolveSchema(config, space, options) {
|
|
175
234
|
const schema = space?.schema ?? config.schema;
|
|
176
235
|
if (!schema) {
|
|
177
236
|
throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
|
|
178
237
|
}
|
|
238
|
+
const configDir = options?.configDir ?? space?.sourceDir;
|
|
239
|
+
if (configDir) {
|
|
240
|
+
return resolveSchemaPath(schema, configDir, space, config);
|
|
241
|
+
}
|
|
179
242
|
return schema;
|
|
180
243
|
}
|
|
181
|
-
/** Update a string field on a space entry and persist config. */
|
|
182
|
-
export function updateSpaceField(spaceName, field, value) {
|
|
244
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
245
|
+
export function updateSpaceField(spaceName, field, value, plugin) {
|
|
183
246
|
const sourcePath = _spaceSourceFiles.get(spaceName);
|
|
184
|
-
if (!sourcePath)
|
|
247
|
+
if (!sourcePath)
|
|
185
248
|
throw new Error(`Space "${spaceName}" not found in any config file`);
|
|
186
|
-
}
|
|
187
249
|
const config = _loadConfig(sourcePath);
|
|
188
250
|
const space = config.spaces?.find((s) => s.name === spaceName);
|
|
189
|
-
if (!space)
|
|
251
|
+
if (!space)
|
|
190
252
|
throw new Error(`Unknown space config: "${spaceName}". Check config.`);
|
|
253
|
+
if (plugin !== undefined) {
|
|
254
|
+
if (!space.plugins)
|
|
255
|
+
space.plugins = {};
|
|
256
|
+
const normalized = normalizePluginName(plugin);
|
|
257
|
+
const shortName = shortenPluginName(normalized);
|
|
258
|
+
const existingKey = Object.keys(space.plugins).find((k) => k === normalized || k === shortName) ?? normalized;
|
|
259
|
+
if (!space.plugins[existingKey])
|
|
260
|
+
space.plugins[existingKey] = {};
|
|
261
|
+
space.plugins[existingKey][field] = value;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
space[field] = value;
|
|
191
265
|
}
|
|
192
|
-
space[field] = value;
|
|
193
266
|
writeFileSync(sourcePath, `${JSON5.stringify(config, null, 2)}\n`);
|
|
194
267
|
}
|
|
@@ -3,6 +3,8 @@ import type { SpaceNode } from '../types';
|
|
|
3
3
|
export type EdgeMetadata = {
|
|
4
4
|
_field: string;
|
|
5
5
|
_source: 'hierarchy' | 'relationship';
|
|
6
|
+
/** Hierarchy name, present only for hierarchy edges. */
|
|
7
|
+
_hierarchy?: string;
|
|
6
8
|
_selfRef: boolean;
|
|
7
9
|
};
|
|
8
10
|
/** A flat node representation augmented with ancestor and descendant traversal arrays. */
|
|
@@ -18,6 +20,6 @@ export type AugmentedFlatNode = Record<string, unknown> & {
|
|
|
18
20
|
*
|
|
19
21
|
* - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by title.
|
|
20
22
|
* - descendants: BFS via childrenIndex, nearest first, deduplicated by title.
|
|
21
|
-
* - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _selfRef).
|
|
23
|
+
* - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _hierarchy, _selfRef).
|
|
22
24
|
*/
|
|
23
25
|
export declare function augmentNode(node: SpaceNode, nodeIndex: ReadonlyMap<string, SpaceNode>, childrenIndex: ReadonlyMap<string, readonly SpaceNode[]>): AugmentedFlatNode;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** Edge metadata for the resolvedParents entry connecting two nodes. */
|
|
2
|
+
function edgeMetadata(ref) {
|
|
3
|
+
return {
|
|
4
|
+
_field: ref.field,
|
|
5
|
+
_source: ref.source,
|
|
6
|
+
...(ref.hierarchy !== undefined ? { _hierarchy: ref.hierarchy } : {}),
|
|
7
|
+
_selfRef: ref.selfRef,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
1
10
|
/** Flatten a SpaceNode's data fields for use in an augmented representation. */
|
|
2
11
|
function flattenData(node) {
|
|
3
12
|
return {
|
|
@@ -13,7 +22,7 @@ function flattenData(node) {
|
|
|
13
22
|
*
|
|
14
23
|
* - ancestors: BFS from node via resolvedParents, nearest first, deduplicated by title.
|
|
15
24
|
* - descendants: BFS via childrenIndex, nearest first, deduplicated by title.
|
|
16
|
-
* - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _selfRef).
|
|
25
|
+
* - Each entry merges the parent/child node's fields with edge metadata (_field, _source, _hierarchy, _selfRef).
|
|
17
26
|
*/
|
|
18
27
|
export function augmentNode(node, nodeIndex, childrenIndex) {
|
|
19
28
|
const ancestors = buildAncestors(node, nodeIndex);
|
|
@@ -44,9 +53,7 @@ function buildAncestors(node, nodeIndex) {
|
|
|
44
53
|
visited.add(title);
|
|
45
54
|
result.push({
|
|
46
55
|
...flattenData(item.node),
|
|
47
|
-
|
|
48
|
-
_source: item.ref.source,
|
|
49
|
-
_selfRef: item.ref.selfRef,
|
|
56
|
+
...edgeMetadata(item.ref),
|
|
50
57
|
});
|
|
51
58
|
// Continue BFS upward
|
|
52
59
|
for (const ref of item.node.resolvedParents) {
|
|
@@ -78,9 +85,7 @@ function buildDescendants(node, childrenIndex) {
|
|
|
78
85
|
visited.add(title);
|
|
79
86
|
result.push({
|
|
80
87
|
...flattenData(item.childNode),
|
|
81
|
-
|
|
82
|
-
_source: item.ref.source,
|
|
83
|
-
_selfRef: item.ref.selfRef,
|
|
88
|
+
...edgeMetadata(item.ref),
|
|
84
89
|
});
|
|
85
90
|
const grandchildren = childrenIndex.get(title) ?? [];
|
|
86
91
|
for (const grandchild of grandchildren) {
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { format } from 'node:util';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import { diagram } from './commands/diagram';
|
|
5
6
|
import { docs } from './commands/docs';
|
|
6
7
|
import { dump } from './commands/dump';
|
|
8
|
+
import { miroSyncCommand } from './commands/miro-sync';
|
|
7
9
|
import { listPlugins } from './commands/plugins';
|
|
8
10
|
import { render, renderList } from './commands/render';
|
|
9
11
|
import { listSchemas, showSchema } from './commands/schemas';
|
|
@@ -12,29 +14,25 @@ import { listSpaces } from './commands/spaces';
|
|
|
12
14
|
import { templateSync } from './commands/template-sync';
|
|
13
15
|
import { validate, watchValidate } from './commands/validate';
|
|
14
16
|
import { validateFileCommand } from './commands/validate-file';
|
|
15
|
-
import {
|
|
17
|
+
import { loadConfig, setConfigPath } from './config';
|
|
16
18
|
import { CLI_NAME } from './constants';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
+
import { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
20
|
+
// Once node:process is loaded (vfile, via unified, loads it), Bun's console.log drops piped output
|
|
21
|
+
// past 64KB when the reader is slow. process.stdout.write delivers all of it before exit.
|
|
22
|
+
console.log = (...args) => {
|
|
23
|
+
process.stdout.write(`${format(...args)}\n`);
|
|
24
|
+
};
|
|
19
25
|
export function buildSpaceContext(spaceName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
try {
|
|
27
|
+
return createSpaceContext(spaceName, loadConfig());
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof SpaceNotFoundError) {
|
|
31
|
+
console.error(`Error: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
25
35
|
}
|
|
26
|
-
const resolvedSchemaPath = resolveSchema(config, space);
|
|
27
|
-
const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
|
|
28
|
-
const configDir = getSpaceConfigDir(space.name);
|
|
29
|
-
return {
|
|
30
|
-
space,
|
|
31
|
-
config,
|
|
32
|
-
resolvedSchemaPath,
|
|
33
|
-
schema,
|
|
34
|
-
schemaRefRegistry,
|
|
35
|
-
schemaValidator,
|
|
36
|
-
configDir,
|
|
37
|
-
};
|
|
38
36
|
}
|
|
39
37
|
const require = createRequire(import.meta.url);
|
|
40
38
|
const packageJson = require('../package.json');
|
|
@@ -53,8 +51,7 @@ program
|
|
|
53
51
|
.argument('<path>', 'Path to the file to validate')
|
|
54
52
|
.option('--json', 'Output results as JSON (machine-readable, for hooks)')
|
|
55
53
|
.action(async (filePath, options) => {
|
|
56
|
-
|
|
57
|
-
process.exit(exitCode);
|
|
54
|
+
process.exitCode = await validateFileCommand(filePath, { json: options.json });
|
|
58
55
|
});
|
|
59
56
|
program
|
|
60
57
|
.command('validate')
|
|
@@ -68,8 +65,8 @@ program
|
|
|
68
65
|
await watchValidate(context);
|
|
69
66
|
}
|
|
70
67
|
else {
|
|
71
|
-
|
|
72
|
-
process.
|
|
68
|
+
// exitCode rather than process.exit(): exiting straight away can cut off large piped output
|
|
69
|
+
process.exitCode = await validate(context, { json: options.json });
|
|
73
70
|
}
|
|
74
71
|
});
|
|
75
72
|
program
|
|
@@ -77,19 +74,23 @@ program
|
|
|
77
74
|
.description('Generate mermaid diagram from space')
|
|
78
75
|
.argument('<space-name>', 'Space name')
|
|
79
76
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
77
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
80
78
|
.option('-o, --output <path>', 'Output file path (default: stdout)')
|
|
81
|
-
.action((spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
79
|
+
.action(async (spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
82
80
|
program
|
|
83
81
|
.command('show')
|
|
84
82
|
.description('Print space graph as a bullet list')
|
|
85
83
|
.argument('<space-name>', 'Space name')
|
|
86
84
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
87
|
-
.
|
|
85
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
86
|
+
.action(async (spaceName, options) => show(buildSpaceContext(spaceName), options));
|
|
88
87
|
program
|
|
89
88
|
.command('dump')
|
|
90
89
|
.description('Dump parsed space nodes as JSON')
|
|
91
90
|
.argument('<space-name>', 'Space name')
|
|
92
|
-
.
|
|
91
|
+
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
92
|
+
.option('--hierarchy <name>', 'Hierarchy used to evaluate --filter (default: the main hierarchy)')
|
|
93
|
+
.action(async (spaceName, options) => dump(buildSpaceContext(spaceName), options));
|
|
93
94
|
program
|
|
94
95
|
.command('miro-sync')
|
|
95
96
|
.description('Sync space to a Miro board')
|
|
@@ -97,16 +98,16 @@ program
|
|
|
97
98
|
.option('--new-frame <title>', 'Create a new frame on the board and sync into it')
|
|
98
99
|
.option('--dry-run', 'Show what would change without touching Miro')
|
|
99
100
|
.option('-v, --verbose', 'Detailed output')
|
|
100
|
-
.action((spaceName, options) =>
|
|
101
|
+
.action(async (spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
101
102
|
program
|
|
102
103
|
.command('template-sync')
|
|
103
104
|
.description('Sync schema examples to templates')
|
|
104
105
|
.argument('<space-name>', 'Space name')
|
|
105
106
|
.option('--create-missing', 'Create missing template files for schema types')
|
|
106
107
|
.option('--dry-run', 'Preview changes without writing files')
|
|
107
|
-
.action((spaceName, options) => {
|
|
108
|
+
.action(async (spaceName, options) => {
|
|
108
109
|
const context = buildSpaceContext(spaceName);
|
|
109
|
-
templateSync(context, options);
|
|
110
|
+
await templateSync(context, options);
|
|
110
111
|
});
|
|
111
112
|
program
|
|
112
113
|
.command('plugins')
|
|
@@ -116,13 +117,14 @@ const spacesCmd = new Command('spaces').description('List configured spaces');
|
|
|
116
117
|
spacesCmd
|
|
117
118
|
.command('list', { isDefault: true })
|
|
118
119
|
.description('List all configured spaces and their paths')
|
|
119
|
-
.
|
|
120
|
+
.option('--json', 'Output as JSON: config files, and each space with its path and schema')
|
|
121
|
+
.action((options) => listSpaces(options));
|
|
120
122
|
program.addCommand(spacesCmd);
|
|
121
123
|
const schemasCmd = new Command('schemas').alias('schema').description('List and inspect schemas');
|
|
122
124
|
schemasCmd
|
|
123
125
|
.command('list', { isDefault: true })
|
|
124
126
|
.description('List available schemas')
|
|
125
|
-
.action(() => listSchemas());
|
|
127
|
+
.action(async () => await listSchemas());
|
|
126
128
|
schemasCmd
|
|
127
129
|
.command('show')
|
|
128
130
|
.description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
|
|
@@ -130,7 +132,7 @@ schemasCmd
|
|
|
130
132
|
.option('--space <name>', 'Resolve schema from space config')
|
|
131
133
|
.option('--raw', 'Output raw JSON file content')
|
|
132
134
|
.option('--mermaid-erd', 'Output Mermaid Entity Relationship Diagram')
|
|
133
|
-
.action((file, options) => showSchema(file, {
|
|
135
|
+
.action(async (file, options) => await showSchema(file, {
|
|
134
136
|
space: options.space,
|
|
135
137
|
raw: options.raw ?? false,
|
|
136
138
|
mermaidErd: options.mermaidErd ?? false,
|
|
@@ -149,11 +151,25 @@ renderCmd
|
|
|
149
151
|
const context = spaceName ? buildSpaceContext(spaceName) : undefined;
|
|
150
152
|
await renderList(context);
|
|
151
153
|
});
|
|
154
|
+
function parseDataOptions(val, prev = {}) {
|
|
155
|
+
const eqIdx = val.indexOf('=');
|
|
156
|
+
if (eqIdx >= 0) {
|
|
157
|
+
prev[val.slice(0, eqIdx).trim()] = val.slice(eqIdx + 1).trim();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
prev[val.trim()] = 'true';
|
|
161
|
+
}
|
|
162
|
+
return prev;
|
|
163
|
+
}
|
|
152
164
|
renderCmd
|
|
153
165
|
.argument('<space-name>', 'Space name')
|
|
154
|
-
.argument('<format>', 'Render format (e.g. markdown.bullets)')
|
|
166
|
+
.argument('<format>', 'Render format (e.g. markdown.bullets, markdown.directory)')
|
|
155
167
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
156
|
-
.option('
|
|
168
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
169
|
+
.option('-o, --output <path>', 'Output file or directory path (default: stdout)')
|
|
170
|
+
.option('--out <path>', 'Output directory or file path')
|
|
171
|
+
.option('-f, --force', 'Overwrite existing files when writing to disk')
|
|
172
|
+
.option('-d, --data <key=value...>', 'Format-specific key=value options', parseDataOptions)
|
|
157
173
|
.action(async (spaceName, format, options) => {
|
|
158
174
|
await render(buildSpaceContext(spaceName), format, options);
|
|
159
175
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Config } from './config';
|
|
2
|
+
import { type SpaceGraph } from './space-graph';
|
|
3
|
+
import type { ReadSpaceResult, SpaceContext } from './types';
|
|
4
|
+
export interface AssembleSpaceGraphOptions {
|
|
5
|
+
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
6
|
+
filter?: string;
|
|
7
|
+
/** Name of the hierarchy to structure the graph by. Defaults to the schema's main hierarchy. */
|
|
8
|
+
hierarchy?: string;
|
|
9
|
+
/** Pre-loaded read result to assemble from, avoiding a re-read of the space. */
|
|
10
|
+
readResult?: ReadSpaceResult;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
14
|
+
* fail schema validation, build the graph for the selected hierarchy (the main
|
|
15
|
+
* hierarchy by default), and apply an optional view/filter expression.
|
|
16
|
+
*
|
|
17
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
18
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
19
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
20
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
21
|
+
*
|
|
22
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
23
|
+
* graph-assembly behaviour cannot drift.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assembleSpaceGraph(context: SpaceContext, options?: AssembleSpaceGraphOptions): Promise<SpaceGraph>;
|
|
26
|
+
export interface LoadSpaceGraphOptions extends AssembleSpaceGraphOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Directory to anchor relative plugin paths against. Required when config is
|
|
29
|
+
* hand-assembled (has no source file); see {@link createSpaceContext}.
|
|
30
|
+
*/
|
|
31
|
+
configDir?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
35
|
+
*
|
|
36
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
37
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
38
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
39
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
40
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadSpaceGraph(spaceName: string, config: Config, options?: LoadSpaceGraphOptions): Promise<SpaceGraph>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { filterNodes } from './filter/filter-nodes';
|
|
2
|
+
import { readSpace } from './read/read-space';
|
|
3
|
+
import { createSpaceContext } from './space-context';
|
|
4
|
+
import { buildSpaceGraph } from './space-graph';
|
|
5
|
+
/**
|
|
6
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
|
+
* fail schema validation, build the graph for the selected hierarchy (the main
|
|
8
|
+
* hierarchy by default), and apply an optional view/filter expression.
|
|
9
|
+
*
|
|
10
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
11
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
12
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
13
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
14
|
+
*
|
|
15
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
16
|
+
* graph-assembly behaviour cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
export async function assembleSpaceGraph(context, options = {}) {
|
|
19
|
+
const { nodes: allNodes } = options.readResult ?? (await readSpace(context));
|
|
20
|
+
const { schemaValidator } = context;
|
|
21
|
+
const validNodes = allNodes.filter((node) => schemaValidator(node.schemaData));
|
|
22
|
+
let graph = buildSpaceGraph(validNodes, selectHierarchy(context.schema.metadata, options.hierarchy));
|
|
23
|
+
if (options.filter) {
|
|
24
|
+
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
|
|
25
|
+
graph = await filterNodes(expression, graph);
|
|
26
|
+
}
|
|
27
|
+
return graph;
|
|
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
|
+
/**
|
|
42
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
43
|
+
*
|
|
44
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
45
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
46
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
47
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
48
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadSpaceGraph(spaceName, config, options = {}) {
|
|
51
|
+
const context = createSpaceContext(spaceName, config, { configDir: options.configDir });
|
|
52
|
+
return assembleSpaceGraph(context, options);
|
|
53
|
+
}
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { markdownPlugin } from './markdown';
|
|
2
|
+
import { mermaidPlugin } from './mermaid';
|
|
3
|
+
import { miroPlugin } from './miro';
|
|
2
4
|
/** All built-in plugins, in default load order. */
|
|
3
|
-
export const builtinPlugins = [markdownPlugin];
|
|
5
|
+
export const builtinPlugins = [markdownPlugin, mermaidPlugin, miroPlugin];
|
package/dist/plugins/loader.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import Ajv2020 from 'ajv/dist/2020';
|
|
4
4
|
import { getConfigSourceFiles } from '../config';
|
|
5
5
|
import { builtinPlugins } from '.';
|
|
6
|
-
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX } from './util';
|
|
6
|
+
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX, shortenPluginName, } from './util';
|
|
7
7
|
/**
|
|
8
8
|
* Walk a plugin's configSchema and resolve any string fields annotated with
|
|
9
9
|
* format:'path' relative to configDir.
|
|
@@ -71,7 +71,7 @@ export async function discoverPlugins() {
|
|
|
71
71
|
*/
|
|
72
72
|
export async function loadPlugins(pluginMap, configDir) {
|
|
73
73
|
const builtinsByName = new Map(builtinPlugins.map((p) => [p.name, p]));
|
|
74
|
-
const ajv = new
|
|
74
|
+
const ajv = new Ajv2020();
|
|
75
75
|
ajv.addFormat('path', (value) => value.length > 0 && !value.includes('\0'));
|
|
76
76
|
const loaded = [];
|
|
77
77
|
// External plugins: entries in the map that are not built-in names
|
|
@@ -92,7 +92,7 @@ export async function loadPlugins(pluginMap, configDir) {
|
|
|
92
92
|
}
|
|
93
93
|
// Built-in plugins: always loaded, config taken from map if declared (with or without prefix)
|
|
94
94
|
for (const builtin of builtinPlugins) {
|
|
95
|
-
const rawConfig = pluginMap[builtin.name] ?? pluginMap[builtin.name
|
|
95
|
+
const rawConfig = pluginMap[builtin.name] ?? pluginMap[shortenPluginName(builtin.name)] ?? {};
|
|
96
96
|
const pluginConfig = resolveConfigPaths(builtin.configSchema, rawConfig, configDir);
|
|
97
97
|
const validate = ajv.compile(builtin.configSchema);
|
|
98
98
|
if (!validate(pluginConfig)) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Node } from 'mdast';
|
|
2
|
+
import type { ContentLink, SchemaMetadata } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively walk an mdast subtree and collect all links.
|
|
5
|
+
* Handles standard markdown link/image nodes and scans text nodes for wikilinks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractLinksFromAstNode(node: Node): ContentLink[];
|
|
8
|
+
/**
|
|
9
|
+
* Extract all links from a raw markdown body string.
|
|
10
|
+
* Parses the full document and collects links from every node in the tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractLinksFromBody(body: string): ContentLink[];
|
|
13
|
+
/**
|
|
14
|
+
* Build the set of field names that serve as graph edges (hierarchy + relationship fields).
|
|
15
|
+
* Used to exclude edge fields from frontmatter link extraction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getEdgeFieldNames(metadata: SchemaMetadata): Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract links from frontmatter data fields, excluding known graph edge fields.
|
|
20
|
+
* Parses each string value as markdown (via mdast) to keep link extraction consistent
|
|
21
|
+
* with body content parsing — handles wikilinks, markdown links, images, and bare URLs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractLinksFromFrontmatter(data: Record<string, unknown>, edgeFields: Set<string>): ContentLink[];
|