structured-context 0.11.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 +63 -22
- package/dist/api.d.ts +7 -5
- package/dist/api.js +4 -1
- 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/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 +114 -22
- 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 -2
- package/dist/commands/validate-file.js +7 -4
- package/dist/commands/validate.js +38 -21
- package/dist/config.d.ts +18 -1
- package/dist/config.js +81 -10
- 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 +39 -15
- package/dist/load-space-graph.d.ts +4 -2
- package/dist/load-space-graph.js +15 -4
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
- package/dist/plugins/markdown/parse-embedded.js +41 -7
- package/dist/plugins/markdown/read-space.js +38 -5
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- 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 +14 -8
- package/dist/schema/schema.js +277 -198
- 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 +20 -20
- 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 +3 -0
- package/dist/space-context.js +7 -2
- 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 +54 -13
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +19 -9
- package/docs/concepts.md +14 -4
- package/docs/config.md +8 -0
- package/docs/rules.md +49 -22
- package/docs/schemas.md +110 -35
- package/package.json +13 -8
- package/schemas/_knowledge_wiki.json +20 -20
- 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/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
package/dist/commands/spaces.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { basename, resolve } from 'node:path';
|
|
2
|
-
import { configPath, loadConfig } from '../config';
|
|
2
|
+
import { configPath, getConfigSourceFiles, loadConfig } from '../config';
|
|
3
3
|
function renderConfigValue(v) {
|
|
4
4
|
if (typeof v === 'object' && v !== null)
|
|
5
5
|
return JSON.stringify(v);
|
|
6
6
|
return String(v);
|
|
7
7
|
}
|
|
8
|
-
export function listSpaces() {
|
|
8
|
+
export function listSpaces(options = {}) {
|
|
9
9
|
const path = resolve(configPath());
|
|
10
10
|
const config = loadConfig();
|
|
11
|
+
if (options.json) {
|
|
12
|
+
const spaces = config.spaces.map((space) => ({
|
|
13
|
+
name: space.name,
|
|
14
|
+
path: space.path,
|
|
15
|
+
schema: space.schema ?? config.schema,
|
|
16
|
+
}));
|
|
17
|
+
console.log(JSON.stringify({ configFiles: [...getConfigSourceFiles()], spaces }));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
11
20
|
console.log(`Config: ${path}\n`);
|
|
12
21
|
for (const space of config.spaces) {
|
|
13
22
|
console.log(`${space.name}`);
|
|
@@ -34,8 +34,8 @@ function printHumanReadable(result) {
|
|
|
34
34
|
}
|
|
35
35
|
if (result.errorCount > 0) {
|
|
36
36
|
console.log(`\n${red}✗${reset} ${result.label} (space: ${result.space}) — ${result.errorCount} error(s)\n`);
|
|
37
|
-
for (const
|
|
38
|
-
console.log(` [${
|
|
37
|
+
for (const issue of Object.values(result.errors)) {
|
|
38
|
+
console.log(` [${issueLabel(issue)}] ${issue.message}`);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
@@ -43,9 +43,12 @@ function printHumanReadable(result) {
|
|
|
43
43
|
}
|
|
44
44
|
if (result.warningCount > 0) {
|
|
45
45
|
console.log(`\n ${yellow}${result.warningCount} warning(s):${reset}`);
|
|
46
|
-
for (const
|
|
47
|
-
console.log(` ${yellow}[${
|
|
46
|
+
for (const issue of Object.values(result.warnings)) {
|
|
47
|
+
console.log(` ${yellow}[${issueLabel(issue)}]${reset} ${issue.message}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
console.log('');
|
|
51
51
|
}
|
|
52
|
+
function issueLabel({ kind, severity }) {
|
|
53
|
+
return severity ? `${kind}:${severity}` : kind;
|
|
54
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { dirname } from 'node:path';
|
|
2
2
|
import chokidar from 'chokidar';
|
|
3
3
|
import { getConfigSourceFiles } from '../config';
|
|
4
|
+
import { resolveSpacePluginSchemas } from '../plugins/util';
|
|
4
5
|
import { bundledSchemasDir } from '../schema/schema';
|
|
5
6
|
import { formatErrors, validateSpace } from '../validate';
|
|
6
7
|
export async function validate(context, options = {}) {
|
|
7
8
|
const { schema, schemaRefRegistry } = context;
|
|
8
9
|
const result = await validateSpace(context);
|
|
10
|
+
const ruleCounts = { error: 0, warning: 0, info: 0 };
|
|
11
|
+
for (const v of result.ruleViolations)
|
|
12
|
+
ruleCounts[v.severity]++;
|
|
9
13
|
// JSON output mode
|
|
10
14
|
if (options.json) {
|
|
11
15
|
const errorsByFile = {};
|
|
12
|
-
const addError = (file, key, kind, message) => {
|
|
16
|
+
const addError = (file, key, kind, message, severity) => {
|
|
13
17
|
if (!errorsByFile[file])
|
|
14
18
|
errorsByFile[file] = {};
|
|
15
|
-
errorsByFile[file][key] = { kind, message };
|
|
19
|
+
errorsByFile[file][key] = severity ? { kind, message, severity } : { kind, message };
|
|
16
20
|
};
|
|
17
21
|
for (const { file, errors: ajvErrors, nodeData } of result.nodeErrors) {
|
|
18
22
|
const formatted = formatErrors(ajvErrors, schema, schemaRefRegistry, nodeData);
|
|
@@ -30,21 +34,25 @@ export async function validate(context, options = {}) {
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
for (const v of result.ruleViolations) {
|
|
33
|
-
if (v.file) {
|
|
34
|
-
addError(v.file, `rule:${v.ruleId}`, 'rule', `[${v.ruleId}] ${v.description}
|
|
37
|
+
if (v.file && v.severity === 'error') {
|
|
38
|
+
addError(v.file, `rule:${v.ruleId}`, 'rule', `[${v.ruleId}] ${v.description}`, v.severity);
|
|
35
39
|
}
|
|
36
40
|
}
|
|
41
|
+
const globalRuleErrorCount = result.ruleViolations.filter((v) => !v.file && v.severity === 'error').length;
|
|
37
42
|
for (const v of result.hierarchyViolations) {
|
|
38
43
|
addError(v.file, `hierarchy:${v.description}`, 'hierarchy', v.description);
|
|
39
44
|
}
|
|
40
45
|
const parseErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
41
|
-
const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0) +
|
|
46
|
+
const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0) +
|
|
47
|
+
parseErrorCount +
|
|
48
|
+
globalRuleErrorCount;
|
|
42
49
|
console.log(JSON.stringify({
|
|
43
50
|
space: context.space.name,
|
|
44
51
|
valid: errorCount === 0,
|
|
45
52
|
validCount: result.validCount,
|
|
46
53
|
errorCount,
|
|
47
54
|
errors: errorsByFile,
|
|
55
|
+
ruleViolations: result.ruleViolations,
|
|
48
56
|
orphanCount: result.orphans.length,
|
|
49
57
|
parseIssues: result.parseIssues,
|
|
50
58
|
unresolvedContentLinks: result.unresolvedContentLinks,
|
|
@@ -79,7 +87,9 @@ export async function validate(context, options = {}) {
|
|
|
79
87
|
console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
|
|
80
88
|
console.log(fmt(' Broken links', result.refErrors.length, true));
|
|
81
89
|
console.log(fmt(' Duplicate keys', result.duplicateErrors.length, true));
|
|
82
|
-
console.log(fmt(' Rule violations',
|
|
90
|
+
console.log(fmt(' Rule violations (error)', ruleCounts.error, true));
|
|
91
|
+
console.log(fmt(' Rule violations (warning)', ruleCounts.warning, true, true));
|
|
92
|
+
console.log(fmt(' Rule violations (info)', ruleCounts.info, true, true));
|
|
83
93
|
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
84
94
|
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
85
95
|
console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
|
|
@@ -107,14 +117,16 @@ export async function validate(context, options = {}) {
|
|
|
107
117
|
if (result.parseIssues.length > 0) {
|
|
108
118
|
console.log(`\nExcluded during parsing:`);
|
|
109
119
|
for (const issue of result.parseIssues) {
|
|
120
|
+
const loc = issue.line !== undefined ? `:${issue.line}${issue.column !== undefined ? `:${issue.column}` : ''}` : '';
|
|
110
121
|
const detail = issue.message ? ` - ${issue.message}` : '';
|
|
111
|
-
console.log(` ${issue.file}: ${issue.severity} - ${issue.type}${detail}`);
|
|
122
|
+
console.log(` ${issue.file}${loc}: ${issue.severity} - ${issue.type}${detail}`);
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
125
|
if (result.nodeErrors.length > 0) {
|
|
115
126
|
console.log(`\nSchema validation errors:`);
|
|
116
|
-
result.nodeErrors.forEach(({ file, errors, nodeData }) => {
|
|
117
|
-
|
|
127
|
+
result.nodeErrors.forEach(({ file, line, errors, nodeData }) => {
|
|
128
|
+
const loc = line !== undefined ? `:${line}` : '';
|
|
129
|
+
console.log(`\n ${file}${loc}:`);
|
|
118
130
|
const formatted = formatErrors(errors, schema, schemaRefRegistry, nodeData);
|
|
119
131
|
formatted.forEach(({ message }) => {
|
|
120
132
|
console.log(` ${message}`);
|
|
@@ -123,8 +135,9 @@ export async function validate(context, options = {}) {
|
|
|
123
135
|
}
|
|
124
136
|
if (result.refErrors.length > 0) {
|
|
125
137
|
console.log(`\nBroken links:`);
|
|
126
|
-
result.refErrors.forEach(({ file, parent, error }) => {
|
|
127
|
-
|
|
138
|
+
result.refErrors.forEach(({ file, line, parent, error }) => {
|
|
139
|
+
const loc = line !== undefined ? `:${line}` : '';
|
|
140
|
+
console.log(` ${file}${loc}: ${parent} → ${error}`);
|
|
128
141
|
});
|
|
129
142
|
}
|
|
130
143
|
if (result.duplicateErrors.length > 0) {
|
|
@@ -138,16 +151,14 @@ export async function validate(context, options = {}) {
|
|
|
138
151
|
}
|
|
139
152
|
if (result.ruleViolations.length > 0) {
|
|
140
153
|
console.log(`\nRule violations:`);
|
|
141
|
-
const
|
|
142
|
-
for (const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (const [category, violations] of byCategory) {
|
|
148
|
-
console.log(` ${category.toUpperCase()} (${violations.length}):`);
|
|
154
|
+
const severityColor = { error: red, warning: yellow, info: reset };
|
|
155
|
+
for (const severity of ['error', 'warning', 'info']) {
|
|
156
|
+
const violations = result.ruleViolations.filter((v) => v.severity === severity);
|
|
157
|
+
if (violations.length === 0)
|
|
158
|
+
continue;
|
|
159
|
+
console.log(` ${severityColor[severity]}${severity.toUpperCase()}${reset} (${violations.length}):`);
|
|
149
160
|
for (const v of violations) {
|
|
150
|
-
console.log(` ${v.file ? `${v.file}: ` : ''}${v.description}`);
|
|
161
|
+
console.log(` ${v.file ? `${v.file}: ` : ''}${v.description} [${v.category}: ${v.ruleId}]`);
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
}
|
|
@@ -161,7 +172,7 @@ export async function validate(context, options = {}) {
|
|
|
161
172
|
if (result.nodeErrorCount > 0 ||
|
|
162
173
|
result.refErrors.length > 0 ||
|
|
163
174
|
result.duplicateErrors.length > 0 ||
|
|
164
|
-
|
|
175
|
+
ruleCounts.error > 0 ||
|
|
165
176
|
result.hierarchyViolations.length > 0 ||
|
|
166
177
|
parseIssueErrorCount > 0) {
|
|
167
178
|
return 1;
|
|
@@ -177,6 +188,12 @@ export async function watchValidate(context) {
|
|
|
177
188
|
if (schemaDir !== bundledSchemasDir) {
|
|
178
189
|
schemaDirs.push(schemaDir);
|
|
179
190
|
}
|
|
191
|
+
const pluginSources = resolveSpacePluginSchemas(context.space, context.config, context.configDir);
|
|
192
|
+
for (const ps of pluginSources) {
|
|
193
|
+
if (!schemaDirs.includes(ps.schemasDir)) {
|
|
194
|
+
schemaDirs.push(ps.schemasDir);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
180
197
|
console.log(`👀 Watching for changes...`);
|
|
181
198
|
console.log(` Config files: ${configFiles.join(', ')}`);
|
|
182
199
|
console.log(` Schema dirs: ${schemaDirs.join(', ')}`);
|
package/dist/config.d.ts
CHANGED
|
@@ -28,7 +28,24 @@ export declare function configPath(): string;
|
|
|
28
28
|
export declare function loadConfig(): Config;
|
|
29
29
|
/** Get the full space config entry by name. Throws if not found. */
|
|
30
30
|
export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a schema identifier or path to an absolute file path.
|
|
33
|
+
*
|
|
34
|
+
* Precedence:
|
|
35
|
+
* 1. Absolute path or URL → used directly.
|
|
36
|
+
* 2. Explicit relative path (./... or ../...) → resolved relative to configDir.
|
|
37
|
+
* 3. Plugin-qualified path (e.g. "sctx-wardley-mapping/wardley_map.json" or "wardley-mapping/wardley_map.json"):
|
|
38
|
+
* resolved against the matching loaded plugin's schemasDir.
|
|
39
|
+
* 4. Simple filename without directory (e.g. "wardley_map.json" or "strategy_general.json"):
|
|
40
|
+
* - configDir/filename if it exists
|
|
41
|
+
* - plugin.schemasDir/filename for any loaded plugin with schemasDir
|
|
42
|
+
* - bundledSchemasDir/filename if it exists
|
|
43
|
+
* - fallback: configDir/filename
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveSchemaPath(rawSchema: string | undefined, configDir: string, space?: SpaceConfig, config?: Config): string;
|
|
31
46
|
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
32
|
-
export declare function resolveSchema(config: Config, space?: SpaceConfig
|
|
47
|
+
export declare function resolveSchema(config: Config, space?: SpaceConfig, options?: {
|
|
48
|
+
configDir?: string;
|
|
49
|
+
}): string;
|
|
33
50
|
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
34
51
|
export declare function updateSpaceField(spaceName: string, field: string, value: string, plugin?: string): void;
|
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, shortenPluginName } 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',
|
|
@@ -87,13 +87,10 @@ function resolveRelativePaths(config, configDir) {
|
|
|
87
87
|
const relSchema = (p) => {
|
|
88
88
|
if (!p || isAbsolute(p) || isUrl(p))
|
|
89
89
|
return p;
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
if (existsSync(localPath))
|
|
93
|
-
return localPath;
|
|
94
|
-
return join(bundledSchemasDir, p);
|
|
90
|
+
if (p.startsWith('./') || p.startsWith('../')) {
|
|
91
|
+
return resolve(configDir, p);
|
|
95
92
|
}
|
|
96
|
-
return
|
|
93
|
+
return p;
|
|
97
94
|
};
|
|
98
95
|
return {
|
|
99
96
|
...config,
|
|
@@ -111,7 +108,7 @@ function _loadConfig(path) {
|
|
|
111
108
|
throw new Error(`Config file not found: ${path}`);
|
|
112
109
|
}
|
|
113
110
|
const config = JSON5.parse(readFileSync(path, 'utf-8'));
|
|
114
|
-
const ajv = new
|
|
111
|
+
const ajv = new Ajv2020();
|
|
115
112
|
const validate = ajv.compile(CONFIG_SCHEMA);
|
|
116
113
|
if (!validate(config)) {
|
|
117
114
|
throw new Error(`Invalid config in ${path}: ${JSON.stringify(validate.errors)}`);
|
|
@@ -162,12 +159,86 @@ export function getSpaceConfig(name, config) {
|
|
|
162
159
|
}
|
|
163
160
|
return space;
|
|
164
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
|
+
}
|
|
165
232
|
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
166
|
-
export function resolveSchema(config, space) {
|
|
233
|
+
export function resolveSchema(config, space, options) {
|
|
167
234
|
const schema = space?.schema ?? config.schema;
|
|
168
235
|
if (!schema) {
|
|
169
236
|
throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
|
|
170
237
|
}
|
|
238
|
+
const configDir = options?.configDir ?? space?.sourceDir;
|
|
239
|
+
if (configDir) {
|
|
240
|
+
return resolveSchemaPath(schema, configDir, space, config);
|
|
241
|
+
}
|
|
171
242
|
return schema;
|
|
172
243
|
}
|
|
173
244
|
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
@@ -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,5 +1,6 @@
|
|
|
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';
|
|
@@ -16,6 +17,11 @@ import { validateFileCommand } from './commands/validate-file';
|
|
|
16
17
|
import { loadConfig, setConfigPath } from './config';
|
|
17
18
|
import { CLI_NAME } from './constants';
|
|
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
26
|
try {
|
|
21
27
|
return createSpaceContext(spaceName, loadConfig());
|
|
@@ -45,8 +51,7 @@ program
|
|
|
45
51
|
.argument('<path>', 'Path to the file to validate')
|
|
46
52
|
.option('--json', 'Output results as JSON (machine-readable, for hooks)')
|
|
47
53
|
.action(async (filePath, options) => {
|
|
48
|
-
|
|
49
|
-
process.exit(exitCode);
|
|
54
|
+
process.exitCode = await validateFileCommand(filePath, { json: options.json });
|
|
50
55
|
});
|
|
51
56
|
program
|
|
52
57
|
.command('validate')
|
|
@@ -60,8 +65,8 @@ program
|
|
|
60
65
|
await watchValidate(context);
|
|
61
66
|
}
|
|
62
67
|
else {
|
|
63
|
-
|
|
64
|
-
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 });
|
|
65
70
|
}
|
|
66
71
|
});
|
|
67
72
|
program
|
|
@@ -69,19 +74,23 @@ program
|
|
|
69
74
|
.description('Generate mermaid diagram from space')
|
|
70
75
|
.argument('<space-name>', 'Space name')
|
|
71
76
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
77
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
72
78
|
.option('-o, --output <path>', 'Output file path (default: stdout)')
|
|
73
|
-
.action((spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
79
|
+
.action(async (spaceName, options) => diagram(buildSpaceContext(spaceName), options));
|
|
74
80
|
program
|
|
75
81
|
.command('show')
|
|
76
82
|
.description('Print space graph as a bullet list')
|
|
77
83
|
.argument('<space-name>', 'Space name')
|
|
78
84
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
79
|
-
.
|
|
85
|
+
.option('--hierarchy <name>', 'Hierarchy to render (default: the main hierarchy)')
|
|
86
|
+
.action(async (spaceName, options) => show(buildSpaceContext(spaceName), options));
|
|
80
87
|
program
|
|
81
88
|
.command('dump')
|
|
82
89
|
.description('Dump parsed space nodes as JSON')
|
|
83
90
|
.argument('<space-name>', 'Space name')
|
|
84
|
-
.
|
|
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));
|
|
85
94
|
program
|
|
86
95
|
.command('miro-sync')
|
|
87
96
|
.description('Sync space to a Miro board')
|
|
@@ -89,16 +98,16 @@ program
|
|
|
89
98
|
.option('--new-frame <title>', 'Create a new frame on the board and sync into it')
|
|
90
99
|
.option('--dry-run', 'Show what would change without touching Miro')
|
|
91
100
|
.option('-v, --verbose', 'Detailed output')
|
|
92
|
-
.action((spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
101
|
+
.action(async (spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
93
102
|
program
|
|
94
103
|
.command('template-sync')
|
|
95
104
|
.description('Sync schema examples to templates')
|
|
96
105
|
.argument('<space-name>', 'Space name')
|
|
97
106
|
.option('--create-missing', 'Create missing template files for schema types')
|
|
98
107
|
.option('--dry-run', 'Preview changes without writing files')
|
|
99
|
-
.action((spaceName, options) => {
|
|
108
|
+
.action(async (spaceName, options) => {
|
|
100
109
|
const context = buildSpaceContext(spaceName);
|
|
101
|
-
templateSync(context, options);
|
|
110
|
+
await templateSync(context, options);
|
|
102
111
|
});
|
|
103
112
|
program
|
|
104
113
|
.command('plugins')
|
|
@@ -108,13 +117,14 @@ const spacesCmd = new Command('spaces').description('List configured spaces');
|
|
|
108
117
|
spacesCmd
|
|
109
118
|
.command('list', { isDefault: true })
|
|
110
119
|
.description('List all configured spaces and their paths')
|
|
111
|
-
.
|
|
120
|
+
.option('--json', 'Output as JSON: config files, and each space with its path and schema')
|
|
121
|
+
.action((options) => listSpaces(options));
|
|
112
122
|
program.addCommand(spacesCmd);
|
|
113
123
|
const schemasCmd = new Command('schemas').alias('schema').description('List and inspect schemas');
|
|
114
124
|
schemasCmd
|
|
115
125
|
.command('list', { isDefault: true })
|
|
116
126
|
.description('List available schemas')
|
|
117
|
-
.action(() => listSchemas());
|
|
127
|
+
.action(async () => await listSchemas());
|
|
118
128
|
schemasCmd
|
|
119
129
|
.command('show')
|
|
120
130
|
.description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
|
|
@@ -122,7 +132,7 @@ schemasCmd
|
|
|
122
132
|
.option('--space <name>', 'Resolve schema from space config')
|
|
123
133
|
.option('--raw', 'Output raw JSON file content')
|
|
124
134
|
.option('--mermaid-erd', 'Output Mermaid Entity Relationship Diagram')
|
|
125
|
-
.action((file, options) => showSchema(file, {
|
|
135
|
+
.action(async (file, options) => await showSchema(file, {
|
|
126
136
|
space: options.space,
|
|
127
137
|
raw: options.raw ?? false,
|
|
128
138
|
mermaidErd: options.mermaidErd ?? false,
|
|
@@ -141,11 +151,25 @@ renderCmd
|
|
|
141
151
|
const context = spaceName ? buildSpaceContext(spaceName) : undefined;
|
|
142
152
|
await renderList(context);
|
|
143
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
|
+
}
|
|
144
164
|
renderCmd
|
|
145
165
|
.argument('<space-name>', 'Space name')
|
|
146
|
-
.argument('<format>', 'Render format (e.g. markdown.bullets)')
|
|
166
|
+
.argument('<format>', 'Render format (e.g. markdown.bullets, markdown.directory)')
|
|
147
167
|
.option('--filter <filter>', 'Filter view name (from config) or inline filter expression')
|
|
148
|
-
.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)
|
|
149
173
|
.action(async (spaceName, format, options) => {
|
|
150
174
|
await render(buildSpaceContext(spaceName), format, options);
|
|
151
175
|
});
|
|
@@ -4,13 +4,15 @@ import type { ReadSpaceResult, SpaceContext } from './types';
|
|
|
4
4
|
export interface AssembleSpaceGraphOptions {
|
|
5
5
|
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
6
6
|
filter?: string;
|
|
7
|
+
/** Name of the hierarchy to structure the graph by. Defaults to the schema's main hierarchy. */
|
|
8
|
+
hierarchy?: string;
|
|
7
9
|
/** Pre-loaded read result to assemble from, avoiding a re-read of the space. */
|
|
8
10
|
readResult?: ReadSpaceResult;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
12
|
-
* fail schema validation, build the
|
|
13
|
-
* view/filter expression.
|
|
14
|
+
* fail schema validation, build the graph for the selected hierarchy (the main
|
|
15
|
+
* hierarchy by default), and apply an optional view/filter expression.
|
|
14
16
|
*
|
|
15
17
|
* Nodes that fail schema validation are silently excluded so the graph is
|
|
16
18
|
* well-formed enough to render and traverse. This is NOT a validation pass —
|
package/dist/load-space-graph.js
CHANGED
|
@@ -4,8 +4,8 @@ import { createSpaceContext } from './space-context';
|
|
|
4
4
|
import { buildSpaceGraph } from './space-graph';
|
|
5
5
|
/**
|
|
6
6
|
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
|
-
* fail schema validation, build the
|
|
8
|
-
* view/filter expression.
|
|
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
9
|
*
|
|
10
10
|
* Nodes that fail schema validation are silently excluded so the graph is
|
|
11
11
|
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
@@ -19,14 +19,25 @@ export async function assembleSpaceGraph(context, options = {}) {
|
|
|
19
19
|
const { nodes: allNodes } = options.readResult ?? (await readSpace(context));
|
|
20
20
|
const { schemaValidator } = context;
|
|
21
21
|
const validNodes = allNodes.filter((node) => schemaValidator(node.schemaData));
|
|
22
|
-
|
|
23
|
-
let graph = buildSpaceGraph(validNodes, levels);
|
|
22
|
+
let graph = buildSpaceGraph(validNodes, selectHierarchy(context.schema.metadata, options.hierarchy));
|
|
24
23
|
if (options.filter) {
|
|
25
24
|
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
|
|
26
25
|
graph = await filterNodes(expression, graph);
|
|
27
26
|
}
|
|
28
27
|
return graph;
|
|
29
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
|
+
}
|
|
30
41
|
/**
|
|
31
42
|
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
32
43
|
*
|
package/dist/plugins/loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX, shortenPluginName, } from './util';
|
|
@@ -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
|