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/commands/schemas.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { loadConfig
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { loadConfig } from '../config';
|
|
4
|
+
import { discoverPlugins } from '../plugins/loader';
|
|
5
|
+
import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
|
+
import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema, } from '../schema/schema';
|
|
7
|
+
import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
|
|
8
|
+
import { DEFAULT_SEVERITY_BY_CATEGORY } from '../schema/validate-rules';
|
|
9
|
+
import { createSpaceContext } from '../space-context';
|
|
6
10
|
function isBundledPath(schemaPath) {
|
|
7
11
|
return dirname(schemaPath) === bundledSchemasDir;
|
|
8
12
|
}
|
|
@@ -21,6 +25,7 @@ function extractRefs(obj, refs) {
|
|
|
21
25
|
function extractEntities(oneOf, schemaRefRegistry, schema) {
|
|
22
26
|
return oneOf.map((entry) => {
|
|
23
27
|
const entryObj = entry;
|
|
28
|
+
const resolvedObj = resolveRef(entryObj, schema, schemaRefRegistry) ?? entryObj;
|
|
24
29
|
const { properties, required } = mergeVariantProperties(entryObj, schema, schemaRefRegistry);
|
|
25
30
|
const typeDef = properties.type;
|
|
26
31
|
let types = [];
|
|
@@ -30,7 +35,7 @@ function extractEntities(oneOf, schemaRefRegistry, schema) {
|
|
|
30
35
|
types = typeDef.enum.map(String);
|
|
31
36
|
return {
|
|
32
37
|
types,
|
|
33
|
-
description: typeof
|
|
38
|
+
description: typeof resolvedObj.description === 'string' ? resolvedObj.description : undefined,
|
|
34
39
|
properties: Object.keys(properties).filter((k) => k !== 'type'),
|
|
35
40
|
required: required.filter((r) => r !== 'type'),
|
|
36
41
|
};
|
|
@@ -72,9 +77,14 @@ function showDefs(defs) {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
function showMetadata(metadata) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.log(
|
|
80
|
+
const hierarchies = metadata.hierarchies ?? [];
|
|
81
|
+
if (hierarchies.length > 0) {
|
|
82
|
+
console.log('');
|
|
83
|
+
for (const hierarchy of hierarchies) {
|
|
84
|
+
const parts = hierarchy.levels.map((l) => (l.selfRef ? `${l.type}(+)` : l.type));
|
|
85
|
+
const main = hierarchy === metadata.hierarchy ? ' (main)' : '';
|
|
86
|
+
console.log(`hierarchy "${hierarchy.name}"${main}: ${parts.join(' → ')}`);
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
else {
|
|
80
90
|
console.log('\nhierarchy: (none)');
|
|
@@ -87,15 +97,16 @@ function showMetadata(metadata) {
|
|
|
87
97
|
const groups = new Map();
|
|
88
98
|
for (const rule of metadata.rules) {
|
|
89
99
|
const rules = groups.get(rule.category) ?? [];
|
|
90
|
-
rules.push(
|
|
100
|
+
rules.push(rule);
|
|
91
101
|
groups.set(rule.category, rules);
|
|
92
102
|
}
|
|
93
103
|
if (groups.size > 0) {
|
|
94
104
|
console.log('\nRules:');
|
|
95
|
-
for (const [
|
|
96
|
-
console.log(` ${
|
|
97
|
-
for (const
|
|
98
|
-
|
|
105
|
+
for (const [category, rules] of groups) {
|
|
106
|
+
console.log(` ${category} (default severity: ${DEFAULT_SEVERITY_BY_CATEGORY[category]}):`);
|
|
107
|
+
for (const rule of rules) {
|
|
108
|
+
const override = rule.severity ? ` [severity: ${rule.severity}]` : '';
|
|
109
|
+
console.log(` ${rule.id}: ${rule.description}${override}`);
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
}
|
|
@@ -109,7 +120,7 @@ function showMetadata(metadata) {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
|
-
function showRegistry(schemaPath, schemaRefRegistry) {
|
|
123
|
+
function showRegistry(schemaPath, schemaRefRegistry, pluginSchemas) {
|
|
113
124
|
const bundledIds = new Set();
|
|
114
125
|
if (existsSync(bundledSchemasDir)) {
|
|
115
126
|
for (const file of readdirSync(bundledSchemasDir).filter((f) => f.endsWith('.json'))) {
|
|
@@ -118,9 +129,22 @@ function showRegistry(schemaPath, schemaRefRegistry) {
|
|
|
118
129
|
bundledIds.add(s.$id);
|
|
119
130
|
}
|
|
120
131
|
}
|
|
132
|
+
const pluginIds = new Set();
|
|
133
|
+
if (pluginSchemas) {
|
|
134
|
+
for (const { schemasDir } of pluginSchemas) {
|
|
135
|
+
if (!existsSync(schemasDir))
|
|
136
|
+
continue;
|
|
137
|
+
for (const file of readdirSync(schemasDir).filter((f) => f.endsWith('.json'))) {
|
|
138
|
+
const s = readRawSchema(join(schemasDir, file));
|
|
139
|
+
if (typeof s.$id === 'string')
|
|
140
|
+
pluginIds.add(s.$id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
121
144
|
console.log(`\nRegistry (${schemaPath}):`);
|
|
122
145
|
for (const [id] of schemaRefRegistry) {
|
|
123
|
-
|
|
146
|
+
const origin = bundledIds.has(id) ? 'bundled' : pluginIds.has(id) ? 'plugin' : 'local';
|
|
147
|
+
console.log(` [${origin}] ${id}`);
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
/**
|
|
@@ -129,7 +153,6 @@ function showRegistry(schemaPath, schemaRefRegistry) {
|
|
|
129
153
|
*/
|
|
130
154
|
function generateMermaidErd(metadata, entities) {
|
|
131
155
|
let mmd = 'erDiagram\n';
|
|
132
|
-
const hierarchyLevels = metadata.hierarchy?.levels ?? [];
|
|
133
156
|
// Generate entity definitions with properties
|
|
134
157
|
for (const entity of entities) {
|
|
135
158
|
const safeName = entity.type.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
@@ -144,8 +167,8 @@ function generateMermaidErd(metadata, entities) {
|
|
|
144
167
|
}
|
|
145
168
|
mmd += ' }\n';
|
|
146
169
|
}
|
|
147
|
-
// Generate relationships based on hierarchy
|
|
148
|
-
|
|
170
|
+
// Generate relationships based on the levels of each hierarchy
|
|
171
|
+
for (const { levels: hierarchyLevels } of metadata.hierarchies ?? []) {
|
|
149
172
|
for (let i = 0; i < hierarchyLevels.length - 1; i++) {
|
|
150
173
|
const current = hierarchyLevels[i];
|
|
151
174
|
const next = hierarchyLevels[i + 1];
|
|
@@ -165,7 +188,7 @@ function generateMermaidErd(metadata, entities) {
|
|
|
165
188
|
}
|
|
166
189
|
return mmd;
|
|
167
190
|
}
|
|
168
|
-
export function listSchemas() {
|
|
191
|
+
export async function listSchemas() {
|
|
169
192
|
const config = loadConfig();
|
|
170
193
|
// List all schemas known to config
|
|
171
194
|
if (existsSync(bundledSchemasDir)) {
|
|
@@ -181,6 +204,23 @@ export function listSchemas() {
|
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
}
|
|
207
|
+
// List schemas provided by discovered plugins
|
|
208
|
+
const plugins = await discoverPlugins();
|
|
209
|
+
for (const p of plugins) {
|
|
210
|
+
if (p.schemasDir && existsSync(p.schemasDir)) {
|
|
211
|
+
const files = readdirSync(p.schemasDir)
|
|
212
|
+
.filter((f) => f.endsWith('.json'))
|
|
213
|
+
.sort();
|
|
214
|
+
if (files.length > 0) {
|
|
215
|
+
console.log(`\nPlugin schemas (${p.name}):`);
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
const schema = readRawSchema(join(p.schemasDir, file));
|
|
218
|
+
const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
|
|
219
|
+
console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
184
224
|
const configured = [];
|
|
185
225
|
const seen = new Set();
|
|
186
226
|
if (config.schema && !isBundledPath(config.schema)) {
|
|
@@ -200,16 +240,23 @@ export function listSchemas() {
|
|
|
200
240
|
}
|
|
201
241
|
}
|
|
202
242
|
}
|
|
203
|
-
export function showSchema(file, options) {
|
|
243
|
+
export async function showSchema(file, options) {
|
|
204
244
|
const config = loadConfig();
|
|
205
245
|
let schemaPath;
|
|
246
|
+
let schema;
|
|
247
|
+
let schemaRefRegistry;
|
|
248
|
+
let pluginSchemas;
|
|
206
249
|
if (options.space) {
|
|
207
250
|
const space = config.spaces.find((s) => s.name === options.space);
|
|
208
251
|
if (!space) {
|
|
209
252
|
console.error(`Error: Unknown space "${options.space}"`);
|
|
210
253
|
process.exit(1);
|
|
211
254
|
}
|
|
212
|
-
|
|
255
|
+
const context = createSpaceContext(options.space, config);
|
|
256
|
+
schemaPath = context.resolvedSchemaPath;
|
|
257
|
+
schema = context.schema;
|
|
258
|
+
schemaRefRegistry = context.schemaRefRegistry;
|
|
259
|
+
pluginSchemas = resolveSpacePluginSchemas(context.space, config, context.configDir);
|
|
213
260
|
}
|
|
214
261
|
else if (!file) {
|
|
215
262
|
console.error('Error: specify a file argument or use --space');
|
|
@@ -219,7 +266,49 @@ export function showSchema(file, options) {
|
|
|
219
266
|
schemaPath = file;
|
|
220
267
|
}
|
|
221
268
|
else {
|
|
222
|
-
|
|
269
|
+
// Check discovered plugins
|
|
270
|
+
const discovered = await discoverPlugins();
|
|
271
|
+
pluginSchemas = discovered
|
|
272
|
+
.filter((p) => p.schemasDir)
|
|
273
|
+
.map((p) => ({ pluginName: shortenPluginName(p.name), schemasDir: p.schemasDir }));
|
|
274
|
+
let candidatePath;
|
|
275
|
+
if (file.includes('/')) {
|
|
276
|
+
const slashIdx = file.indexOf('/');
|
|
277
|
+
const prefix = file.slice(0, slashIdx);
|
|
278
|
+
const subPath = file.slice(slashIdx + 1);
|
|
279
|
+
const norm = normalizePluginName(prefix);
|
|
280
|
+
const short = shortenPluginName(prefix);
|
|
281
|
+
const pluginDir = resolvePluginSchemasDir(prefix, process.cwd());
|
|
282
|
+
if (pluginDir) {
|
|
283
|
+
candidatePath = resolve(pluginDir, subPath.endsWith('.json') ? subPath : `${subPath}.json`);
|
|
284
|
+
if (!pluginSchemas.some((ps) => ps.pluginName === short)) {
|
|
285
|
+
pluginSchemas.push({ pluginName: short, schemasDir: pluginDir });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const match = discovered.find((p) => (p.name === norm || p.name === short) && p.schemasDir);
|
|
290
|
+
if (match?.schemasDir) {
|
|
291
|
+
candidatePath = resolve(match.schemasDir, subPath.endsWith('.json') ? subPath : `${subPath}.json`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
for (const p of discovered) {
|
|
297
|
+
if (p.schemasDir) {
|
|
298
|
+
const c = resolve(p.schemasDir, file.endsWith('.json') ? file : `${file}.json`);
|
|
299
|
+
if (existsSync(c)) {
|
|
300
|
+
candidatePath = c;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (candidatePath && existsSync(candidatePath)) {
|
|
307
|
+
schemaPath = candidatePath;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
schemaPath = join(bundledSchemasDir, file.endsWith('.json') ? file : `${file}.json`);
|
|
311
|
+
}
|
|
223
312
|
}
|
|
224
313
|
if (!existsSync(schemaPath)) {
|
|
225
314
|
console.error(`Schema not found: ${schemaPath}`);
|
|
@@ -236,7 +325,11 @@ export function showSchema(file, options) {
|
|
|
236
325
|
process.stdout.write(content);
|
|
237
326
|
return;
|
|
238
327
|
}
|
|
239
|
-
|
|
328
|
+
if (!schema || !schemaRefRegistry) {
|
|
329
|
+
const loaded = loadSchema(schemaPath, pluginSchemas);
|
|
330
|
+
schema = loaded.schema;
|
|
331
|
+
schemaRefRegistry = loaded.schemaRefRegistry;
|
|
332
|
+
}
|
|
240
333
|
// Handle --mermaid-erd: generate ERD and exit
|
|
241
334
|
if (options.mermaidErd) {
|
|
242
335
|
const entityInfo = extractEntityInfo(schema, schemaRefRegistry);
|
|
@@ -264,5 +357,5 @@ export function showSchema(file, options) {
|
|
|
264
357
|
console.log(` ${ref}`);
|
|
265
358
|
}
|
|
266
359
|
}
|
|
267
|
-
showRegistry(schemaPath, schemaRefRegistry);
|
|
360
|
+
showRegistry(schemaPath, schemaRefRegistry, pluginSchemas);
|
|
268
361
|
}
|
package/dist/commands/show.d.ts
CHANGED
package/dist/commands/show.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { executeRender } from '../render/render';
|
|
2
2
|
export async function show(context, options) {
|
|
3
|
-
const result = await executeRender('markdown.bullets', context, {
|
|
3
|
+
const result = await executeRender('markdown.bullets', context, {
|
|
4
|
+
filter: options?.filter,
|
|
5
|
+
hierarchy: options?.hierarchy,
|
|
6
|
+
});
|
|
7
|
+
if (typeof result !== 'string') {
|
|
8
|
+
throw new Error('Expected string output from markdown.bullets renderer');
|
|
9
|
+
}
|
|
4
10
|
process.stdout.write(result);
|
|
5
11
|
if (!result.endsWith('\n'))
|
|
6
12
|
process.stdout.write('\n');
|
package/dist/commands/spaces.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
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}`);
|
|
14
23
|
console.log(` path: ${space.path}`);
|
|
15
24
|
const schema = space.schema ?? config.schema;
|
|
16
25
|
console.log(` schema: ${schema ? basename(schema) : '(none)'}`);
|
|
17
|
-
if (space.miroBoardId)
|
|
18
|
-
console.log(` miro: configured`);
|
|
19
26
|
const plugins = space.plugins ?? {};
|
|
20
27
|
if (Object.keys(plugins).length > 0) {
|
|
21
28
|
console.log(' plugins:');
|
|
@@ -21,19 +21,34 @@ export async function validateFileCommand(filePath, options = {}) {
|
|
|
21
21
|
else {
|
|
22
22
|
printHumanReadable(result);
|
|
23
23
|
}
|
|
24
|
-
return
|
|
24
|
+
return result.errorCount > 0 ? 1 : 0;
|
|
25
25
|
}
|
|
26
26
|
function printHumanReadable(result) {
|
|
27
27
|
const reset = '\x1b[0m';
|
|
28
28
|
const green = '\x1b[32m';
|
|
29
29
|
const red = '\x1b[31m';
|
|
30
|
-
|
|
30
|
+
const yellow = '\x1b[33m';
|
|
31
|
+
if (result.errorCount === 0 && result.warningCount === 0) {
|
|
31
32
|
console.log(`${green}✓${reset} ${result.label} (space: ${result.space})`);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (result.errorCount > 0) {
|
|
36
|
+
console.log(`\n${red}✗${reset} ${result.label} (space: ${result.space}) — ${result.errorCount} error(s)\n`);
|
|
37
|
+
for (const issue of Object.values(result.errors)) {
|
|
38
|
+
console.log(` [${issueLabel(issue)}] ${issue.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(`\n${green}✓${reset} ${result.label} (space: ${result.space})`);
|
|
43
|
+
}
|
|
44
|
+
if (result.warningCount > 0) {
|
|
45
|
+
console.log(`\n ${yellow}${result.warningCount} warning(s):${reset}`);
|
|
46
|
+
for (const issue of Object.values(result.warnings)) {
|
|
47
|
+
console.log(` ${yellow}[${issueLabel(issue)}]${reset} ${issue.message}`);
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
console.log('');
|
|
39
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,23 +34,28 @@ 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,
|
|
58
|
+
unresolvedContentLinks: result.unresolvedContentLinks,
|
|
50
59
|
}, null, 2));
|
|
51
60
|
return errorCount > 0 ? 1 : 0;
|
|
52
61
|
}
|
|
@@ -78,9 +87,12 @@ export async function validate(context, options = {}) {
|
|
|
78
87
|
console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
|
|
79
88
|
console.log(fmt(' Broken links', result.refErrors.length, true));
|
|
80
89
|
console.log(fmt(' Duplicate keys', result.duplicateErrors.length, true));
|
|
81
|
-
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));
|
|
82
93
|
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
83
94
|
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
95
|
+
console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
|
|
84
96
|
const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
85
97
|
console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
|
|
86
98
|
if (result.orphans.length > 0) {
|
|
@@ -88,17 +100,33 @@ export async function validate(context, options = {}) {
|
|
|
88
100
|
for (const node of result.orphans)
|
|
89
101
|
console.log(` ${node.label}`);
|
|
90
102
|
}
|
|
103
|
+
if (result.unresolvedContentLinks.length > 0) {
|
|
104
|
+
console.log(`\nUnresolved content links (may be outside-space or broken):`);
|
|
105
|
+
const byFile = new Map();
|
|
106
|
+
for (const { file, target } of result.unresolvedContentLinks) {
|
|
107
|
+
if (!byFile.has(file))
|
|
108
|
+
byFile.set(file, []);
|
|
109
|
+
byFile.get(file).push(target);
|
|
110
|
+
}
|
|
111
|
+
for (const [file, targets] of byFile) {
|
|
112
|
+
console.log(` ${file}:`);
|
|
113
|
+
for (const target of targets)
|
|
114
|
+
console.log(` ${target}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
91
117
|
if (result.parseIssues.length > 0) {
|
|
92
118
|
console.log(`\nExcluded during parsing:`);
|
|
93
119
|
for (const issue of result.parseIssues) {
|
|
120
|
+
const loc = issue.line !== undefined ? `:${issue.line}${issue.column !== undefined ? `:${issue.column}` : ''}` : '';
|
|
94
121
|
const detail = issue.message ? ` - ${issue.message}` : '';
|
|
95
|
-
console.log(` ${issue.file}: ${issue.severity} - ${issue.type}${detail}`);
|
|
122
|
+
console.log(` ${issue.file}${loc}: ${issue.severity} - ${issue.type}${detail}`);
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
if (result.nodeErrors.length > 0) {
|
|
99
126
|
console.log(`\nSchema validation errors:`);
|
|
100
|
-
result.nodeErrors.forEach(({ file, errors, nodeData }) => {
|
|
101
|
-
|
|
127
|
+
result.nodeErrors.forEach(({ file, line, errors, nodeData }) => {
|
|
128
|
+
const loc = line !== undefined ? `:${line}` : '';
|
|
129
|
+
console.log(`\n ${file}${loc}:`);
|
|
102
130
|
const formatted = formatErrors(errors, schema, schemaRefRegistry, nodeData);
|
|
103
131
|
formatted.forEach(({ message }) => {
|
|
104
132
|
console.log(` ${message}`);
|
|
@@ -107,8 +135,9 @@ export async function validate(context, options = {}) {
|
|
|
107
135
|
}
|
|
108
136
|
if (result.refErrors.length > 0) {
|
|
109
137
|
console.log(`\nBroken links:`);
|
|
110
|
-
result.refErrors.forEach(({ file, parent, error }) => {
|
|
111
|
-
|
|
138
|
+
result.refErrors.forEach(({ file, line, parent, error }) => {
|
|
139
|
+
const loc = line !== undefined ? `:${line}` : '';
|
|
140
|
+
console.log(` ${file}${loc}: ${parent} → ${error}`);
|
|
112
141
|
});
|
|
113
142
|
}
|
|
114
143
|
if (result.duplicateErrors.length > 0) {
|
|
@@ -122,16 +151,14 @@ export async function validate(context, options = {}) {
|
|
|
122
151
|
}
|
|
123
152
|
if (result.ruleViolations.length > 0) {
|
|
124
153
|
console.log(`\nRule violations:`);
|
|
125
|
-
const
|
|
126
|
-
for (const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for (const [category, violations] of byCategory) {
|
|
132
|
-
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}):`);
|
|
133
160
|
for (const v of violations) {
|
|
134
|
-
console.log(` ${v.file ? `${v.file}: ` : ''}${v.description}`);
|
|
161
|
+
console.log(` ${v.file ? `${v.file}: ` : ''}${v.description} [${v.category}: ${v.ruleId}]`);
|
|
135
162
|
}
|
|
136
163
|
}
|
|
137
164
|
}
|
|
@@ -145,7 +172,7 @@ export async function validate(context, options = {}) {
|
|
|
145
172
|
if (result.nodeErrorCount > 0 ||
|
|
146
173
|
result.refErrors.length > 0 ||
|
|
147
174
|
result.duplicateErrors.length > 0 ||
|
|
148
|
-
|
|
175
|
+
ruleCounts.error > 0 ||
|
|
149
176
|
result.hierarchyViolations.length > 0 ||
|
|
150
177
|
parseIssueErrorCount > 0) {
|
|
151
178
|
return 1;
|
|
@@ -161,6 +188,12 @@ export async function watchValidate(context) {
|
|
|
161
188
|
if (schemaDir !== bundledSchemasDir) {
|
|
162
189
|
schemaDirs.push(schemaDir);
|
|
163
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
|
+
}
|
|
164
197
|
console.log(`👀 Watching for changes...`);
|
|
165
198
|
console.log(` Config files: ${configFiles.join(', ')}`);
|
|
166
199
|
console.log(` Schema dirs: ${schemaDirs.join(', ')}`);
|
package/dist/config.d.ts
CHANGED
|
@@ -2,14 +2,18 @@ export type SpaceConfig = {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
schema?: string;
|
|
5
|
-
miroBoardId?: string;
|
|
6
|
-
miroFrameId?: string;
|
|
7
5
|
/** Plugin name → plugin config map. Overrides top-level plugins when set. */
|
|
8
6
|
plugins?: Record<string, Record<string, unknown>>;
|
|
9
7
|
/** Named filter views for this space. Keys are view names; values contain the filter expression. */
|
|
10
8
|
views?: Record<string, {
|
|
11
9
|
expression: string;
|
|
12
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Directory of the config file that defined this space, used to anchor relative plugin-config
|
|
13
|
+
* paths. Set by loadConfig; absent for a hand-assembled Config, whose callers must pass an
|
|
14
|
+
* explicit configDir to createSpaceContext. Not persisted by updateSpaceField.
|
|
15
|
+
*/
|
|
16
|
+
sourceDir?: string;
|
|
13
17
|
};
|
|
14
18
|
export type Config = {
|
|
15
19
|
spaces: SpaceConfig[];
|
|
@@ -20,19 +24,28 @@ export type Config = {
|
|
|
20
24
|
export declare function setConfigPath(path: string | undefined): void;
|
|
21
25
|
/** Get all config file paths that were loaded (main config + included configs). */
|
|
22
26
|
export declare function getConfigSourceFiles(): Set<string>;
|
|
23
|
-
/**
|
|
24
|
-
* Get the directory of the config file that defines a given space.
|
|
25
|
-
*/
|
|
26
|
-
export declare function getSpaceConfigDir(spaceName: string): string;
|
|
27
27
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export declare function updateSpaceField(spaceName: string, field: StringFields<SpaceConfig>, value: string): void;
|
|
38
|
-
export {};
|
|
47
|
+
export declare function resolveSchema(config: Config, space?: SpaceConfig, options?: {
|
|
48
|
+
configDir?: string;
|
|
49
|
+
}): string;
|
|
50
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
51
|
+
export declare function updateSpaceField(spaceName: string, field: string, value: string, plugin?: string): void;
|