structured-context 0.11.0 → 0.13.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 (138) hide show
  1. package/README.md +52 -198
  2. package/dist/api.d.ts +17 -15
  3. package/dist/api.js +12 -9
  4. package/dist/commands/diagram.d.ts +1 -0
  5. package/dist/commands/diagram.js +7 -1
  6. package/dist/commands/docs.js +1 -0
  7. package/dist/commands/dump.d.ts +15 -2
  8. package/dist/commands/dump.js +18 -4
  9. package/dist/commands/providers.d.ts +4 -0
  10. package/dist/commands/providers.js +52 -0
  11. package/dist/commands/render.d.ts +4 -0
  12. package/dist/commands/render.js +64 -9
  13. package/dist/commands/schemas.d.ts +2 -2
  14. package/dist/commands/schemas.js +133 -22
  15. package/dist/commands/show.d.ts +1 -0
  16. package/dist/commands/show.js +7 -1
  17. package/dist/commands/spaces.d.ts +3 -1
  18. package/dist/commands/spaces.js +11 -2
  19. package/dist/commands/validate-file.d.ts +3 -0
  20. package/dist/commands/validate-file.js +24 -6
  21. package/dist/commands/validate.d.ts +4 -0
  22. package/dist/commands/validate.js +79 -23
  23. package/dist/config.d.ts +25 -1
  24. package/dist/config.js +106 -12
  25. package/dist/filter/augment-nodes.d.ts +3 -1
  26. package/dist/filter/augment-nodes.js +12 -7
  27. package/dist/filter/filter-nodes.js +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +96 -31
  30. package/dist/judge/context.d.ts +20 -0
  31. package/dist/judge/context.js +105 -0
  32. package/dist/judge/factory.d.ts +44 -0
  33. package/dist/judge/factory.js +148 -0
  34. package/dist/judge/index.d.ts +13 -0
  35. package/dist/judge/index.js +8 -0
  36. package/dist/judge/judge-node.d.ts +27 -0
  37. package/dist/judge/judge-node.js +122 -0
  38. package/dist/judge/prompt.d.ts +10 -0
  39. package/dist/judge/prompt.js +92 -0
  40. package/dist/judge/provider/anthropic.d.ts +17 -0
  41. package/dist/judge/provider/anthropic.js +43 -0
  42. package/dist/judge/provider/fake.d.ts +26 -0
  43. package/dist/judge/provider/fake.js +56 -0
  44. package/dist/judge/provider/jev.d.ts +37 -0
  45. package/dist/judge/provider/jev.js +169 -0
  46. package/dist/judge/provider/openai-decisions.d.ts +21 -0
  47. package/dist/judge/provider/openai-decisions.js +84 -0
  48. package/dist/judge/provider/openai.d.ts +20 -0
  49. package/dist/judge/provider/openai.js +53 -0
  50. package/dist/judge/provider/per-rule.d.ts +10 -0
  51. package/dist/judge/provider/per-rule.js +18 -0
  52. package/dist/judge/trim.d.ts +27 -0
  53. package/dist/judge/trim.js +62 -0
  54. package/dist/judge/types.d.ts +92 -0
  55. package/dist/judge/types.js +9 -0
  56. package/dist/judge/verdicts.d.ts +24 -0
  57. package/dist/judge/verdicts.js +82 -0
  58. package/dist/load-space-graph.d.ts +7 -5
  59. package/dist/load-space-graph.js +19 -8
  60. package/dist/plugins/loader.js +3 -3
  61. package/dist/plugins/markdown/extract-content-links.js +8 -29
  62. package/dist/plugins/markdown/index.js +8 -2
  63. package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
  64. package/dist/plugins/markdown/parse-embedded.js +195 -24
  65. package/dist/plugins/markdown/read-space.js +42 -14
  66. package/dist/plugins/markdown/render-directory.d.ts +11 -0
  67. package/dist/plugins/markdown/render-directory.js +130 -0
  68. package/dist/plugins/markdown/template-sync.js +3 -4
  69. package/dist/plugins/markdown/util.d.ts +26 -5
  70. package/dist/plugins/markdown/util.js +56 -15
  71. package/dist/plugins/mermaid/render-mermaid.js +2 -1
  72. package/dist/plugins/miro/sync.js +7 -6
  73. package/dist/plugins/util.d.ts +39 -1
  74. package/dist/plugins/util.js +139 -0
  75. package/dist/read/resolve-graph-edges.d.ts +1 -1
  76. package/dist/read/resolve-graph-edges.js +46 -27
  77. package/dist/read/wikilink-utils.d.ts +17 -0
  78. package/dist/read/wikilink-utils.js +40 -0
  79. package/dist/render/render.d.ts +3 -1
  80. package/dist/render/render.js +2 -2
  81. package/dist/schema/evaluate-rule.d.ts +33 -2
  82. package/dist/schema/evaluate-rule.js +58 -4
  83. package/dist/schema/gating.d.ts +69 -0
  84. package/dist/schema/gating.js +253 -0
  85. package/dist/schema/meta-schemas.d.ts +13 -0
  86. package/dist/schema/meta-schemas.js +50 -0
  87. package/dist/schema/metadata-contract.d.ts +952 -183
  88. package/dist/schema/metadata-contract.js +177 -33
  89. package/dist/schema/rule-filter.d.ts +12 -0
  90. package/dist/schema/rule-filter.js +46 -0
  91. package/dist/schema/rule-severity.d.ts +13 -0
  92. package/dist/schema/rule-severity.js +33 -0
  93. package/dist/schema/schema.d.ts +14 -8
  94. package/dist/schema/schema.js +308 -199
  95. package/dist/schema/validate-graph.d.ts +2 -1
  96. package/dist/schema/validate-graph.js +40 -23
  97. package/dist/schema/validate-rules.d.ts +16 -3
  98. package/dist/schema/validate-rules.js +91 -29
  99. package/dist/schemas/_knowledge_wiki.json +24 -24
  100. package/dist/schemas/_okf.json +172 -0
  101. package/dist/schemas/_ost_strict.json +17 -8
  102. package/dist/schemas/_sctx_base.json +3 -3
  103. package/dist/schemas/_strategy_general.json +38 -38
  104. package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  105. package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  106. package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  107. package/dist/schemas/knowledge_wiki.json +10 -9
  108. package/dist/schemas/okf.json +8 -0
  109. package/dist/schemas/strategy_general.json +15 -14
  110. package/dist/schemas/strict_ost.json +17 -16
  111. package/dist/space-context.d.ts +5 -2
  112. package/dist/space-context.js +9 -4
  113. package/dist/space-graph.d.ts +9 -7
  114. package/dist/space-graph.js +7 -5
  115. package/dist/testing.d.ts +33 -0
  116. package/dist/testing.js +72 -0
  117. package/dist/types.d.ts +64 -16
  118. package/dist/validate.d.ts +62 -9
  119. package/dist/validate.js +229 -114
  120. package/docs/concepts.md +65 -9
  121. package/docs/config.md +78 -1
  122. package/docs/rules.md +168 -50
  123. package/docs/schemas.md +152 -52
  124. package/package.json +17 -11
  125. package/schemas/_knowledge_wiki.json +24 -24
  126. package/schemas/_okf.json +172 -0
  127. package/schemas/_ost_strict.json +17 -8
  128. package/schemas/_sctx_base.json +3 -3
  129. package/schemas/_strategy_general.json +38 -38
  130. package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
  131. package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
  132. package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
  133. package/schemas/knowledge_wiki.json +10 -9
  134. package/schemas/okf.json +8 -0
  135. package/schemas/strategy_general.json +15 -14
  136. package/schemas/strict_ost.json +17 -16
  137. package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
  138. package/schemas/generated/_structured_context_schema_meta.json +0 -191
@@ -1,18 +1,29 @@
1
1
  import { dirname } from 'node:path';
2
2
  import chokidar from 'chokidar';
3
3
  import { getConfigSourceFiles } from '../config';
4
+ import { judgeForRun } from '../judge/factory';
5
+ import { resolveSpacePluginSchemas } from '../plugins/util';
6
+ import { parseRuleFilter } from '../schema/rule-filter';
4
7
  import { bundledSchemasDir } from '../schema/schema';
5
- import { formatErrors, validateSpace } from '../validate';
8
+ import { formatErrors, ruleViolationMessage, validateSpace } from '../validate';
6
9
  export async function validate(context, options = {}) {
7
10
  const { schema, schemaRefRegistry } = context;
8
- const result = await validateSpace(context);
11
+ const result = await validateSpace(context, {
12
+ depth: options.depth,
13
+ includeGated: options.includeGated,
14
+ judge: judgeForRun(context.config, context.space, options.semantic),
15
+ ruleFilter: options.ruleFilter && parseRuleFilter(options.ruleFilter),
16
+ });
17
+ const ruleCounts = { error: 0, warning: 0, info: 0 };
18
+ for (const v of result.ruleViolations)
19
+ ruleCounts[v.severity]++;
9
20
  // JSON output mode
10
21
  if (options.json) {
11
22
  const errorsByFile = {};
12
- const addError = (file, key, kind, message) => {
23
+ const addError = (file, key, kind, message, severity) => {
13
24
  if (!errorsByFile[file])
14
25
  errorsByFile[file] = {};
15
- errorsByFile[file][key] = { kind, message };
26
+ errorsByFile[file][key] = severity ? { kind, message, severity } : { kind, message };
16
27
  };
17
28
  for (const { file, errors: ajvErrors, nodeData } of result.nodeErrors) {
18
29
  const formatted = formatErrors(ajvErrors, schema, schemaRefRegistry, nodeData);
@@ -30,24 +41,32 @@ export async function validate(context, options = {}) {
30
41
  }
31
42
  }
32
43
  for (const v of result.ruleViolations) {
33
- if (v.file) {
34
- addError(v.file, `rule:${v.ruleId}`, 'rule', `[${v.ruleId}] ${v.description}`);
44
+ if (v.file && v.severity === 'error') {
45
+ addError(v.file, `rule:${v.ruleId}`, 'rule', ruleViolationMessage(v), v.severity);
35
46
  }
36
47
  }
48
+ const globalRuleErrorCount = result.ruleViolations.filter((v) => !v.file && v.severity === 'error').length;
37
49
  for (const v of result.hierarchyViolations) {
38
50
  addError(v.file, `hierarchy:${v.description}`, 'hierarchy', v.description);
39
51
  }
40
52
  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;
53
+ const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0) +
54
+ parseErrorCount +
55
+ globalRuleErrorCount;
42
56
  console.log(JSON.stringify({
43
57
  space: context.space.name,
44
58
  valid: errorCount === 0,
45
59
  validCount: result.validCount,
46
60
  errorCount,
47
61
  errors: errorsByFile,
62
+ ruleViolations: result.ruleViolations,
48
63
  orphanCount: result.orphans.length,
49
64
  parseIssues: result.parseIssues,
50
65
  unresolvedContentLinks: result.unresolvedContentLinks,
66
+ coverageGates: result.coverageGates,
67
+ contextTrimmed: result.contextTrimmed,
68
+ skippedByDepth: result.skippedByDepth,
69
+ depthLimit: result.depthLimit,
51
70
  }, null, 2));
52
71
  return errorCount > 0 ? 1 : 0;
53
72
  }
@@ -79,12 +98,19 @@ export async function validate(context, options = {}) {
79
98
  console.log(fmt(' Schema validation errors', result.nodeErrorCount, true));
80
99
  console.log(fmt(' Broken links', result.refErrors.length, true));
81
100
  console.log(fmt(' Duplicate keys', result.duplicateErrors.length, true));
82
- console.log(fmt(' Rule violations', result.ruleViolations.length, true));
101
+ console.log(fmt(' Rule violations (error)', ruleCounts.error, true));
102
+ console.log(fmt(' Rule violations (warning)', ruleCounts.warning, true, true));
103
+ console.log(fmt(' Rule violations (info)', ruleCounts.info, true, true));
83
104
  console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
84
105
  console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
85
106
  console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
86
107
  const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
87
108
  console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
109
+ if (result.depthLimit !== undefined) {
110
+ console.log(fmt(' Skipped by depth limit', result.skippedByDepth.length, true, true));
111
+ }
112
+ const gatedFileCount = result.coverageGates.reduce((sum, g) => sum + g.gatedFiles.length, 0);
113
+ console.log(fmt(' Rule checks gated (cascade)', gatedFileCount, true, true));
88
114
  if (result.orphans.length > 0) {
89
115
  console.log(`\nOrphans (hierarchy nodes - no parent):`);
90
116
  for (const node of result.orphans)
@@ -107,14 +133,16 @@ export async function validate(context, options = {}) {
107
133
  if (result.parseIssues.length > 0) {
108
134
  console.log(`\nExcluded during parsing:`);
109
135
  for (const issue of result.parseIssues) {
136
+ const loc = issue.line !== undefined ? `:${issue.line}${issue.column !== undefined ? `:${issue.column}` : ''}` : '';
110
137
  const detail = issue.message ? ` - ${issue.message}` : '';
111
- console.log(` ${issue.file}: ${issue.severity} - ${issue.type}${detail}`);
138
+ console.log(` ${issue.file}${loc}: ${issue.severity} - ${issue.type}${detail}`);
112
139
  }
113
140
  }
114
141
  if (result.nodeErrors.length > 0) {
115
142
  console.log(`\nSchema validation errors:`);
116
- result.nodeErrors.forEach(({ file, errors, nodeData }) => {
117
- console.log(`\n ${file}:`);
143
+ result.nodeErrors.forEach(({ file, line, errors, nodeData }) => {
144
+ const loc = line !== undefined ? `:${line}` : '';
145
+ console.log(`\n ${file}${loc}:`);
118
146
  const formatted = formatErrors(errors, schema, schemaRefRegistry, nodeData);
119
147
  formatted.forEach(({ message }) => {
120
148
  console.log(` ${message}`);
@@ -123,8 +151,9 @@ export async function validate(context, options = {}) {
123
151
  }
124
152
  if (result.refErrors.length > 0) {
125
153
  console.log(`\nBroken links:`);
126
- result.refErrors.forEach(({ file, parent, error }) => {
127
- console.log(` ${file}: ${parent} → ${error}`);
154
+ result.refErrors.forEach(({ file, line, parent, error }) => {
155
+ const loc = line !== undefined ? `:${line}` : '';
156
+ console.log(` ${file}${loc}: ${parent} → ${error}`);
128
157
  });
129
158
  }
130
159
  if (result.duplicateErrors.length > 0) {
@@ -138,16 +167,20 @@ export async function validate(context, options = {}) {
138
167
  }
139
168
  if (result.ruleViolations.length > 0) {
140
169
  console.log(`\nRule violations:`);
141
- const byCategory = new Map();
142
- for (const v of result.ruleViolations) {
143
- if (!byCategory.has(v.category))
144
- byCategory.set(v.category, []);
145
- byCategory.get(v.category).push(v);
146
- }
147
- for (const [category, violations] of byCategory) {
148
- console.log(` ${category.toUpperCase()} (${violations.length}):`);
170
+ const severityColor = { error: red, warning: yellow, info: reset };
171
+ for (const severity of ['error', 'warning', 'info']) {
172
+ const violations = result.ruleViolations.filter((v) => v.severity === severity);
173
+ if (violations.length === 0)
174
+ continue;
175
+ console.log(` ${severityColor[severity]}${severity.toUpperCase()}${reset} (${violations.length}):`);
149
176
  for (const v of violations) {
150
- console.log(` ${v.file ? `${v.file}: ` : ''}${v.description}`);
177
+ console.log(` ${v.file ? `${v.file}: ` : ''}${v.description} [${v.category}: ${v.ruleId}]`);
178
+ if (v.evidence) {
179
+ console.log(` Evidence: ${v.evidence}`);
180
+ }
181
+ if (v.guidance) {
182
+ console.log(` Guidance: ${v.guidance}`);
183
+ }
151
184
  }
152
185
  }
153
186
  }
@@ -157,11 +190,28 @@ export async function validate(context, options = {}) {
157
190
  console.log(` ${v.file}: ${v.description}`);
158
191
  }
159
192
  }
193
+ if (result.coverageGates.length > 0) {
194
+ console.log(`\nCoverage — rule checks gated by cascade (use --include-gated to evaluate anyway):`);
195
+ for (const gate of result.coverageGates) {
196
+ const cause = gate.reason === 'rule' ? `blocking rule "${gate.ruleId}"` : `${gate.reason} failure`;
197
+ console.log(` ${gate.file} (${gate.nodeTitle}) — ${cause} gated ${gate.gatedFiles.length} node(s):`);
198
+ for (const file of gate.gatedFiles)
199
+ console.log(` ${file}`);
200
+ }
201
+ }
202
+ if (result.skippedByDepth.length > 0) {
203
+ console.log(`\nSkipped by depth limit (--depth ${result.depthLimit}):`);
204
+ for (const { file } of result.skippedByDepth)
205
+ console.log(` ${file}`);
206
+ }
207
+ if (result.contextTrimmed.nodeCount > 0) {
208
+ console.log(`\nContext trimmed for ${result.contextTrimmed.nodeCount} node(s) across rules ${result.contextTrimmed.ruleIds.join(', ')}.`);
209
+ }
160
210
  console.log(`\n`);
161
211
  if (result.nodeErrorCount > 0 ||
162
212
  result.refErrors.length > 0 ||
163
213
  result.duplicateErrors.length > 0 ||
164
- result.ruleViolations.length > 0 ||
214
+ ruleCounts.error > 0 ||
165
215
  result.hierarchyViolations.length > 0 ||
166
216
  parseIssueErrorCount > 0) {
167
217
  return 1;
@@ -177,6 +227,12 @@ export async function watchValidate(context) {
177
227
  if (schemaDir !== bundledSchemasDir) {
178
228
  schemaDirs.push(schemaDir);
179
229
  }
230
+ const pluginSources = resolveSpacePluginSchemas(context.space, context.config, context.configDir);
231
+ for (const ps of pluginSources) {
232
+ if (!schemaDirs.includes(ps.schemasDir)) {
233
+ schemaDirs.push(ps.schemasDir);
234
+ }
235
+ }
180
236
  console.log(`👀 Watching for changes...`);
181
237
  console.log(` Config files: ${configFiles.join(', ')}`);
182
238
  console.log(` Schema dirs: ${schemaDirs.join(', ')}`);
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type JudgeConfig, type ModelProviderConfig } from './judge/types.js';
1
2
  export type SpaceConfig = {
2
3
  name: string;
3
4
  path: string;
@@ -8,6 +9,8 @@ export type SpaceConfig = {
8
9
  views?: Record<string, {
9
10
  expression: string;
10
11
  }>;
12
+ /** Model provider for this space's semantic rules; overrides the top-level `semantic`. */
13
+ semantic?: JudgeConfig;
11
14
  /**
12
15
  * Directory of the config file that defined this space, used to anchor relative plugin-config
13
16
  * paths. Set by loadConfig; absent for a hand-assembled Config, whose callers must pass an
@@ -19,6 +22,10 @@ export type Config = {
19
22
  spaces: SpaceConfig[];
20
23
  schema?: string;
21
24
  includeSpacesFrom?: string[];
25
+ /** Model providers by name: how to reach each model. */
26
+ modelProviders?: Record<string, ModelProviderConfig>;
27
+ /** Model provider for semantic rules (a provider name). A space's `semantic` overrides it. */
28
+ semantic?: JudgeConfig;
22
29
  };
23
30
  /** Override the config file path used by loadConfig/updateSpaceField. */
24
31
  export declare function setConfigPath(path: string | undefined): void;
@@ -28,7 +35,24 @@ export declare function configPath(): string;
28
35
  export declare function loadConfig(): Config;
29
36
  /** Get the full space config entry by name. Throws if not found. */
30
37
  export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
38
+ /**
39
+ * Resolve a schema identifier or path to an absolute file path.
40
+ *
41
+ * Precedence:
42
+ * 1. Absolute path or URL → used directly.
43
+ * 2. Explicit relative path (./... or ../...) → resolved relative to configDir.
44
+ * 3. Plugin-qualified path (e.g. "sctx-wardley-mapping/wardley_map.json" or "wardley-mapping/wardley_map.json"):
45
+ * resolved against the matching loaded plugin's schemasDir.
46
+ * 4. Simple filename without directory (e.g. "wardley_map.json" or "strategy_general.json"):
47
+ * - configDir/filename if it exists
48
+ * - plugin.schemasDir/filename for any loaded plugin with schemasDir
49
+ * - bundledSchemasDir/filename if it exists
50
+ * - fallback: configDir/filename
51
+ */
52
+ export declare function resolveSchemaPath(rawSchema: string | undefined, configDir: string, space?: SpaceConfig, config?: Config): string;
31
53
  /** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
32
- export declare function resolveSchema(config: Config, space?: SpaceConfig): string;
54
+ export declare function resolveSchema(config: Config, space?: SpaceConfig, options?: {
55
+ configDir?: string;
56
+ }): string;
33
57
  /** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
34
58
  export declare function updateSpaceField(spaceName: string, field: string, value: string, plugin?: string): void;
package/dist/config.js CHANGED
@@ -1,11 +1,31 @@
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 Ajv from 'ajv';
4
+ import Ajv2020 from 'ajv/dist/2020';
5
5
  import JSON5 from 'json5';
6
- import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
7
- import { normalizePluginName, shortenPluginName } from './plugins/util';
8
- import { bundledSchemasDir } from './schema/schema';
6
+ import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants.js';
7
+ import { MODEL_PROVIDER_APIS } from './judge/types.js';
8
+ import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from './plugins/util.js';
9
+ import { bundledSchemasDir } from './schema/schema.js';
10
+ const MODEL_PROVIDER_SCHEMA = {
11
+ type: 'object',
12
+ properties: {
13
+ api: { type: 'string', enum: MODEL_PROVIDER_APIS },
14
+ model: { type: 'string' },
15
+ modelEnv: { type: 'string' },
16
+ endpoint: { type: 'string' },
17
+ endpointEnv: { type: 'string' },
18
+ apiKey: { type: 'string' },
19
+ apiKeyEnv: { type: 'string' },
20
+ },
21
+ required: ['api'],
22
+ additionalProperties: false,
23
+ };
24
+ const SEMANTIC_CONFIG_SCHEMA = {
25
+ type: 'object',
26
+ properties: { provider: { type: 'string', minLength: 1 } },
27
+ additionalProperties: false,
28
+ };
9
29
  const CONFIG_SCHEMA = {
10
30
  type: 'object',
11
31
  properties: {
@@ -27,6 +47,7 @@ const CONFIG_SCHEMA = {
27
47
  additionalProperties: false,
28
48
  },
29
49
  },
50
+ semantic: SEMANTIC_CONFIG_SCHEMA,
30
51
  },
31
52
  required: ['name', 'path'],
32
53
  additionalProperties: false,
@@ -34,6 +55,8 @@ const CONFIG_SCHEMA = {
34
55
  },
35
56
  schema: { type: 'string' },
36
57
  includeSpacesFrom: { type: 'array', items: { type: 'string' } },
58
+ modelProviders: { type: 'object', additionalProperties: MODEL_PROVIDER_SCHEMA },
59
+ semantic: SEMANTIC_CONFIG_SCHEMA,
37
60
  },
38
61
  anyOf: [{ required: ['spaces'] }, { required: ['includeSpacesFrom'] }],
39
62
  additionalProperties: false,
@@ -87,13 +110,10 @@ function resolveRelativePaths(config, configDir) {
87
110
  const relSchema = (p) => {
88
111
  if (!p || isAbsolute(p) || isUrl(p))
89
112
  return p;
90
- if (!p.includes('/') && !p.includes('\\')) {
91
- const localPath = resolve(configDir, p);
92
- if (existsSync(localPath))
93
- return localPath;
94
- return join(bundledSchemasDir, p);
113
+ if (p.startsWith('./') || p.startsWith('../')) {
114
+ return resolve(configDir, p);
95
115
  }
96
- return resolve(configDir, p);
116
+ return p;
97
117
  };
98
118
  return {
99
119
  ...config,
@@ -111,7 +131,7 @@ function _loadConfig(path) {
111
131
  throw new Error(`Config file not found: ${path}`);
112
132
  }
113
133
  const config = JSON5.parse(readFileSync(path, 'utf-8'));
114
- const ajv = new Ajv();
134
+ const ajv = new Ajv2020();
115
135
  const validate = ajv.compile(CONFIG_SCHEMA);
116
136
  if (!validate(config)) {
117
137
  throw new Error(`Invalid config in ${path}: ${JSON.stringify(validate.errors)}`);
@@ -162,12 +182,86 @@ export function getSpaceConfig(name, config) {
162
182
  }
163
183
  return space;
164
184
  }
185
+ /**
186
+ * Resolve a schema identifier or path to an absolute file path.
187
+ *
188
+ * Precedence:
189
+ * 1. Absolute path or URL → used directly.
190
+ * 2. Explicit relative path (./... or ../...) → resolved relative to configDir.
191
+ * 3. Plugin-qualified path (e.g. "sctx-wardley-mapping/wardley_map.json" or "wardley-mapping/wardley_map.json"):
192
+ * resolved against the matching loaded plugin's schemasDir.
193
+ * 4. Simple filename without directory (e.g. "wardley_map.json" or "strategy_general.json"):
194
+ * - configDir/filename if it exists
195
+ * - plugin.schemasDir/filename for any loaded plugin with schemasDir
196
+ * - bundledSchemasDir/filename if it exists
197
+ * - fallback: configDir/filename
198
+ */
199
+ export function resolveSchemaPath(rawSchema, configDir, space, config) {
200
+ if (!rawSchema) {
201
+ throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
202
+ }
203
+ if (isAbsolute(rawSchema) || isUrl(rawSchema)) {
204
+ return rawSchema;
205
+ }
206
+ if (rawSchema.startsWith('./') || rawSchema.startsWith('../')) {
207
+ return resolve(configDir, rawSchema);
208
+ }
209
+ if (rawSchema.includes('/') || rawSchema.includes('\\')) {
210
+ const normalizedSlash = rawSchema.replace(/\\/g, '/');
211
+ const slashIdx = normalizedSlash.indexOf('/');
212
+ const prefix = normalizedSlash.slice(0, slashIdx);
213
+ const subPath = normalizedSlash.slice(slashIdx + 1);
214
+ const pluginSchemasDir = resolvePluginSchemasDir(prefix, configDir);
215
+ if (pluginSchemasDir) {
216
+ const candidate = resolve(pluginSchemasDir, subPath);
217
+ if (existsSync(candidate)) {
218
+ return candidate;
219
+ }
220
+ }
221
+ // Check if relative to configDir exists
222
+ const localCandidate = resolve(configDir, rawSchema);
223
+ if (existsSync(localCandidate)) {
224
+ return localCandidate;
225
+ }
226
+ // If plugin schemas dir was found, return candidate under it
227
+ if (pluginSchemasDir) {
228
+ return resolve(pluginSchemasDir, subPath);
229
+ }
230
+ return localCandidate;
231
+ }
232
+ // Simple filename without directory:
233
+ // 1. configDir/filename
234
+ const localCandidate = resolve(configDir, rawSchema);
235
+ if (existsSync(localCandidate)) {
236
+ return localCandidate;
237
+ }
238
+ // 2. Plugin schemas from declared space / config plugins
239
+ if (space || config) {
240
+ const pluginSources = resolveSpacePluginSchemas(space, config ?? { spaces: [] }, configDir);
241
+ for (const source of pluginSources) {
242
+ const pluginCandidate = resolve(source.schemasDir, rawSchema);
243
+ if (existsSync(pluginCandidate)) {
244
+ return pluginCandidate;
245
+ }
246
+ }
247
+ }
248
+ // 3. Bundled schemas
249
+ const bundledCandidate = join(bundledSchemasDir, rawSchema);
250
+ if (existsSync(bundledCandidate)) {
251
+ return bundledCandidate;
252
+ }
253
+ return localCandidate;
254
+ }
165
255
  /** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
166
- export function resolveSchema(config, space) {
256
+ export function resolveSchema(config, space, options) {
167
257
  const schema = space?.schema ?? config.schema;
168
258
  if (!schema) {
169
259
  throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
170
260
  }
261
+ const configDir = options?.configDir ?? space?.sourceDir;
262
+ if (configDir) {
263
+ return resolveSchemaPath(schema, configDir, space, config);
264
+ }
171
265
  return schema;
172
266
  }
173
267
  /** 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
- _field: item.ref.field,
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
- _field: item.ref.field,
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) {
@@ -69,5 +69,5 @@ export async function filterNodes(expression, graph) {
69
69
  else {
70
70
  matchedNodes = matched;
71
71
  }
72
- return buildSpaceGraph(matchedNodes, graph.levels);
72
+ return buildSpaceGraph(matchedNodes, graph.hierarchy);
73
73
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
- import type { SpaceContext } from './types';
2
+ import type { SpaceContext } from './types.js';
3
3
  export declare function buildSpaceContext(spaceName: string): SpaceContext;