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.
Files changed (107) hide show
  1. package/README.md +85 -25
  2. package/dist/api.d.ts +25 -5
  3. package/dist/api.js +22 -2
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/dump.d.ts +15 -2
  7. package/dist/commands/dump.js +18 -4
  8. package/dist/commands/miro-sync.d.ts +3 -0
  9. package/dist/commands/miro-sync.js +8 -0
  10. package/dist/commands/render.d.ts +4 -0
  11. package/dist/commands/render.js +64 -9
  12. package/dist/commands/schemas.d.ts +2 -2
  13. package/dist/commands/schemas.js +117 -24
  14. package/dist/commands/show.d.ts +1 -0
  15. package/dist/commands/show.js +7 -1
  16. package/dist/commands/spaces.d.ts +3 -1
  17. package/dist/commands/spaces.js +11 -4
  18. package/dist/commands/validate-file.js +20 -5
  19. package/dist/commands/validate.js +54 -21
  20. package/dist/config.d.ts +26 -13
  21. package/dist/config.js +104 -31
  22. package/dist/filter/augment-nodes.d.ts +3 -1
  23. package/dist/filter/augment-nodes.js +12 -7
  24. package/dist/filter/filter-nodes.js +1 -1
  25. package/dist/index.js +51 -35
  26. package/dist/load-space-graph.d.ts +42 -0
  27. package/dist/load-space-graph.js +53 -0
  28. package/dist/plugins/index.js +3 -1
  29. package/dist/plugins/loader.js +4 -4
  30. package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
  31. package/dist/plugins/markdown/extract-content-links.js +123 -0
  32. package/dist/plugins/markdown/index.js +5 -5
  33. package/dist/plugins/markdown/parse-embedded.d.ts +6 -0
  34. package/dist/plugins/markdown/parse-embedded.js +50 -7
  35. package/dist/plugins/markdown/read-space.js +41 -5
  36. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  37. package/dist/plugins/markdown/render-directory.js +130 -0
  38. package/dist/plugins/mermaid/index.d.ts +2 -0
  39. package/dist/plugins/mermaid/index.js +14 -0
  40. package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -15
  41. package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
  42. package/dist/plugins/miro/index.d.ts +2 -0
  43. package/dist/plugins/miro/index.js +27 -0
  44. package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
  45. package/dist/{integrations → plugins}/miro/layout.js +3 -16
  46. package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
  47. package/dist/plugins/miro/sync.d.ts +7 -0
  48. package/dist/{integrations → plugins}/miro/sync.js +22 -39
  49. package/dist/plugins/util.d.ts +48 -1
  50. package/dist/plugins/util.js +143 -0
  51. package/dist/read/resolve-graph-edges.d.ts +1 -1
  52. package/dist/read/resolve-graph-edges.js +78 -27
  53. package/dist/read/wikilink-utils.d.ts +17 -0
  54. package/dist/read/wikilink-utils.js +40 -0
  55. package/dist/render/render.d.ts +4 -1
  56. package/dist/render/render.js +20 -16
  57. package/dist/schema/evaluate-rule.js +1 -0
  58. package/dist/schema/meta-schemas.d.ts +13 -0
  59. package/dist/schema/meta-schemas.js +50 -0
  60. package/dist/schema/metadata-contract.d.ts +343 -187
  61. package/dist/schema/metadata-contract.js +68 -30
  62. package/dist/schema/schema.d.ts +17 -9
  63. package/dist/schema/schema.js +283 -200
  64. package/dist/schema/validate-graph.d.ts +2 -1
  65. package/dist/schema/validate-graph.js +40 -23
  66. package/dist/schema/validate-rules.d.ts +5 -1
  67. package/dist/schema/validate-rules.js +20 -1
  68. package/dist/schemas/_knowledge_wiki.json +21 -21
  69. package/dist/schemas/_ost_strict.json +2 -2
  70. package/dist/schemas/_sctx_base.json +2 -2
  71. package/dist/schemas/_strategy_general.json +38 -38
  72. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  73. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  74. package/dist/schemas/knowledge_wiki.json +10 -9
  75. package/dist/schemas/strategy_general.json +15 -14
  76. package/dist/schemas/strict_ost.json +17 -16
  77. package/dist/space-context.d.ts +27 -0
  78. package/dist/space-context.js +46 -0
  79. package/dist/space-graph.d.ts +9 -7
  80. package/dist/space-graph.js +7 -5
  81. package/dist/testing.d.ts +33 -0
  82. package/dist/testing.js +72 -0
  83. package/dist/types.d.ts +101 -8
  84. package/dist/validate.d.ts +15 -3
  85. package/dist/validate.js +55 -20
  86. package/docs/concepts.md +37 -4
  87. package/docs/config.md +10 -0
  88. package/docs/rules.md +49 -22
  89. package/docs/schemas.md +110 -35
  90. package/package.json +13 -8
  91. package/schemas/_knowledge_wiki.json +21 -21
  92. package/schemas/_ost_strict.json +2 -2
  93. package/schemas/_sctx_base.json +2 -2
  94. package/schemas/_strategy_general.json +38 -38
  95. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  96. package/schemas/generated/latest/_structured_context_schema_meta.json +229 -0
  97. package/schemas/knowledge_wiki.json +10 -9
  98. package/schemas/strategy_general.json +15 -14
  99. package/schemas/strict_ost.json +17 -16
  100. package/dist/integrations/miro/sync.d.ts +0 -8
  101. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  102. package/schemas/generated/_structured_context_schema_meta.json +0 -191
  103. /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
  104. /package/dist/{integrations → plugins}/miro/cache.js +0 -0
  105. /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
  106. /package/dist/{integrations → plugins}/miro/client.js +0 -0
  107. /package/dist/{integrations → plugins}/miro/styles.js +0 -0
@@ -1,8 +1,12 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { loadConfig, resolveSchema } from '../config';
4
- import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema } from '../schema/schema';
5
- import { mergeVariantProperties } from '../schema/schema-refs';
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 entryObj.description === 'string' ? entryObj.description : undefined,
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
- if (metadata.hierarchy?.levels.length) {
76
- const parts = metadata.hierarchy.levels.map((l) => (l.selfRef ? `${l.type}(+)` : l.type));
77
- console.log(`\nhierarchy: ${parts.join(' → ')}`);
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({ id: rule.id, description: rule.description });
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 [group, items] of groups) {
96
- console.log(` ${group}:`);
97
- for (const item of items) {
98
- console.log(` ${item.id}: ${item.description}`);
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
- console.log(` [${bundledIds.has(id) ? 'bundled' : 'local'}] ${id}`);
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 metadata
148
- if (hierarchyLevels.length > 0) {
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
- schemaPath = resolveSchema(config, space);
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
- schemaPath = join(bundledSchemasDir, file.endsWith('.json') ? file : `${file}.json`);
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
- const { schema, schemaRefRegistry } = loadSchema(schemaPath);
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
  }
@@ -1,4 +1,5 @@
1
1
  import type { SpaceContext } from '../types';
2
2
  export declare function show(context: SpaceContext, options?: {
3
3
  filter?: string;
4
+ hierarchy?: string;
4
5
  }): Promise<void>;
@@ -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, { filter: options?.filter });
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');
@@ -1 +1,3 @@
1
- export declare function listSpaces(): void;
1
+ export declare function listSpaces(options?: {
2
+ json?: boolean;
3
+ }): void;
@@ -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 Object.keys(result.errors).length > 0 ? 1 : 0;
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
- if (result.errorCount === 0) {
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
- console.log(`\n${red}✗${reset} ${result.label} (space: ${result.space}) — ${result.errorCount} error(s)\n`);
35
- for (const { kind, message } of Object.values(result.errors)) {
36
- console.log(` [${kind}] ${message}`);
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) + parseErrorCount;
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', result.ruleViolations.length, true));
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
- console.log(`\n ${file}:`);
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
- console.log(` ${file}: ${parent} → ${error}`);
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 byCategory = new Map();
126
- for (const v of result.ruleViolations) {
127
- if (!byCategory.has(v.category))
128
- byCategory.set(v.category, []);
129
- byCategory.get(v.category).push(v);
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
- result.ruleViolations.length > 0 ||
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): string;
33
- type StringFields<T> = {
34
- [K in keyof T]: T[K] extends string | undefined ? K : never;
35
- }[keyof T];
36
- /** Update a string field on a space entry and persist config. */
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;