structured-context 0.9.1 → 0.9.2

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 CHANGED
@@ -193,7 +193,7 @@ If no provider defines `hierarchy`, hierarchy-specific checks are skipped. Readi
193
193
  - **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
194
194
  - **Unique IDs**: To encourage clean namespacing, local partial schemas **must** have unique `$id`s that do not collide with the default schemas. If a collision is detected, validation will fail with an error.
195
195
 
196
- Schema resolution order: space config `schema` > global config `schema` > bundled `schemas/general.json`
196
+ Schema resolution order: space config `schema` > global config `schema`. Schema resolution fails if none is configured.
197
197
 
198
198
  **⚠️ Security Notice: Only use schemas and configuration files from trusted sources.**
199
199
 
@@ -322,7 +322,7 @@ Generates a Mermaid Entity Relationship Diagram from a schema:
322
322
 
323
323
  Example:
324
324
  ```bash
325
- sctx schemas show general --mermaid-erd
325
+ sctx schemas show strategy_general --mermaid-erd
326
326
  ```
327
327
 
328
328
  ### Sync space to Miro
@@ -1,5 +1,5 @@
1
1
  import { basename, resolve } from 'node:path';
2
- import { configPath, loadConfig, resolveSchema } from '../config';
2
+ import { configPath, loadConfig } from '../config';
3
3
  function renderConfigValue(v) {
4
4
  if (typeof v === 'object' && v !== null)
5
5
  return JSON.stringify(v);
@@ -12,7 +12,8 @@ export function listSpaces() {
12
12
  for (const space of config.spaces) {
13
13
  console.log(`${space.name}`);
14
14
  console.log(` path: ${space.path}`);
15
- console.log(` schema: ${basename(resolveSchema(config, space))}`);
15
+ const schema = space.schema ?? config.schema;
16
+ console.log(` schema: ${schema ? basename(schema) : '(none)'}`);
16
17
  if (space.miroBoardId)
17
18
  console.log(` miro: configured`);
18
19
  const plugins = space.plugins ?? {};
@@ -97,7 +97,7 @@ export async function validate(context, options = {}) {
97
97
  const { schema, schemaRefRegistry, schemaValidator } = context;
98
98
  const metadata = schema.metadata;
99
99
  const readResult = await readSpace(context);
100
- const { nodes, parseIgnored } = readResult;
100
+ const { nodes, parseIssues } = readResult;
101
101
  const result = {
102
102
  validCount: 0,
103
103
  nodeErrorCount: 0,
@@ -107,7 +107,7 @@ export async function validate(context, options = {}) {
107
107
  ruleViolations: [],
108
108
  hierarchyViolations: [],
109
109
  orphans: [],
110
- parseIgnored: parseIgnored || [],
110
+ parseIssues,
111
111
  };
112
112
  for (const node of nodes) {
113
113
  const valid = schemaValidator(node.schemaData);
@@ -180,7 +180,8 @@ export async function validate(context, options = {}) {
180
180
  for (const v of result.hierarchyViolations) {
181
181
  addError(v.file, `hierarchy:${v.description}`, 'hierarchy', v.description);
182
182
  }
183
- const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0);
183
+ const parseErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
184
+ const errorCount = Object.values(errorsByFile).reduce((sum, errs) => sum + Object.keys(errs).length, 0) + parseErrorCount;
184
185
  console.log(JSON.stringify({
185
186
  space: context.space.name,
186
187
  valid: errorCount === 0,
@@ -188,7 +189,7 @@ export async function validate(context, options = {}) {
188
189
  errorCount,
189
190
  errors: errorsByFile,
190
191
  orphanCount: result.orphans.length,
191
- parseIgnored: result.parseIgnored,
192
+ parseIssues: result.parseIssues,
192
193
  }, null, 2));
193
194
  return errorCount > 0 ? 1 : 0;
194
195
  }
@@ -224,16 +225,19 @@ export async function validate(context, options = {}) {
224
225
  console.log(fmt(' Rule violations', result.ruleViolations.length, true));
225
226
  console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
226
227
  console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
227
- console.log(fmt(' Ignored during parsing', result.parseIgnored.length, true, true));
228
+ const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
229
+ console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
228
230
  if (result.orphans.length > 0) {
229
231
  console.log(`\nOrphans (hierarchy nodes - no parent):`);
230
232
  for (const node of result.orphans)
231
233
  console.log(` ${node.label}`);
232
234
  }
233
- if (result.parseIgnored.length > 0) {
234
- console.log(`\nIgnored during parsing:`);
235
- for (const f of result.parseIgnored)
236
- console.log(` ${f}`);
235
+ if (result.parseIssues.length > 0) {
236
+ console.log(`\nExcluded during parsing:`);
237
+ for (const issue of result.parseIssues) {
238
+ const detail = issue.message ? ` - ${issue.message}` : '';
239
+ console.log(` ${issue.file}: ${issue.severity} - ${issue.type}${detail}`);
240
+ }
237
241
  }
238
242
  if (result.nodeErrors.length > 0) {
239
243
  console.log(`\nSchema validation errors:`);
@@ -290,7 +294,8 @@ export async function validate(context, options = {}) {
290
294
  result.refErrors.length > 0 ||
291
295
  result.duplicateErrors.length > 0 ||
292
296
  result.ruleViolations.length > 0 ||
293
- result.hierarchyViolations.length > 0) {
297
+ result.hierarchyViolations.length > 0 ||
298
+ parseIssueErrorCount > 0) {
294
299
  return 1;
295
300
  }
296
301
  return 0;
package/dist/config.d.ts CHANGED
@@ -28,7 +28,7 @@ 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
- /** Resolve schema path: CLI arg > space-level config > global config > hardcoded default. */
31
+ /** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
32
32
  export declare function resolveSchema(config: Config, space?: SpaceConfig): string;
33
33
  type StringFields<T> = {
34
34
  [K in keyof T]: T[K] extends string | undefined ? K : never;
package/dist/config.js CHANGED
@@ -170,9 +170,13 @@ export function getSpaceConfig(name, config) {
170
170
  }
171
171
  return space;
172
172
  }
173
- /** Resolve schema path: CLI arg > space-level config > global config > hardcoded default. */
173
+ /** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
174
174
  export function resolveSchema(config, space) {
175
- return space?.schema ?? config.schema ?? join(bundledSchemasDir, 'general.json');
175
+ const schema = space?.schema ?? config.schema;
176
+ if (!schema) {
177
+ throw new Error('No schema configured. Set "schema" in the space config or at the top level of the config file.');
178
+ }
179
+ return schema;
176
180
  }
177
181
  /** Update a string field on a space entry and persist config. */
178
182
  export function updateSpaceField(spaceName, field, value) {
@@ -9,4 +9,4 @@ export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
9
9
  export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
10
10
  export type { SharedEmbeddingFields } from './schema/metadata-contract';
11
11
  export type { SpaceGraph } from './space-graph';
12
- export type { BaseNode, EdgeDefinition, HierarchyLevel, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, } from './types';
12
+ export type { BaseNode, EdgeDefinition, HierarchyLevel, ParseIssue, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, } from './types';
@@ -25,7 +25,13 @@ export function readSpaceOnAPage(context) {
25
25
  pageType: 'space_on_a_page',
26
26
  metadata,
27
27
  });
28
- return { nodes, parseIgnored: terminatedHeadings, diagnostics: { kind: 'page', preambleNodeCount } };
28
+ const parseIssues = terminatedHeadings.map((heading) => ({
29
+ file: heading,
30
+ severity: 'warning',
31
+ type: 'terminated',
32
+ message: 'Ignored headings detected beyond end of hierarchy.',
33
+ }));
34
+ return { nodes, parseIssues, diagnostics: { kind: 'page', preambleNodeCount } };
29
35
  }
30
36
  export async function readSpaceDirectory(context, options) {
31
37
  const { space, schema: { metadata }, } = context;
@@ -38,17 +44,33 @@ export async function readSpaceDirectory(context, options) {
38
44
  const knownTypes = typeInferenceCfg?.mode !== 'off' ? extractSchemaTypeNames(context.schema, context.schemaRefRegistry) : undefined;
39
45
  const files = await Array.fromAsync(new Glob('**/*.md').scan({ cwd: directory, followSymlinks: true }));
40
46
  const nodes = [];
41
- const skipped = [];
42
- const nonSpace = [];
47
+ const parseIssues = [];
43
48
  for (const file of files) {
44
49
  const absoluteFilePath = resolve(directory, file);
45
50
  if (absoluteTemplateDir && absoluteFilePath.startsWith(absoluteTemplateDir)) {
46
51
  continue;
47
52
  }
48
53
  const content = readFileSync(join(directory, file), 'utf-8');
49
- const parsed = matter(content);
54
+ let parsed;
55
+ try {
56
+ parsed = matter(content);
57
+ }
58
+ catch (err) {
59
+ // gray-matter caches parsed YAML and has a known bug where a caught exception
60
+ // corrupts its internal cache, causing subsequent parses to silently return {}.
61
+ // Clear the cache after any parse error to avoid stale state.
62
+ // See: https://github.com/jonschlinkert/gray-matter/issues/166
63
+ matter.clearCache();
64
+ parseIssues.push({
65
+ file,
66
+ severity: 'error',
67
+ type: 'parse',
68
+ message: err instanceof Error ? err.message : String(err),
69
+ });
70
+ continue;
71
+ }
50
72
  if (!parsed.data || Object.keys(parsed.data).length === 0) {
51
- skipped.push(file);
73
+ parseIssues.push({ file, severity: 'warning', type: 'empty' });
52
74
  continue;
53
75
  }
54
76
  const data = coerceDates(applyFieldMap(parsed.data, fieldMap));
@@ -56,7 +78,7 @@ export async function readSpaceDirectory(context, options) {
56
78
  data.type = inferTypeFromPath(file, typeInferenceCfg, knownTypes, context.schema.metadata.typeAliases);
57
79
  }
58
80
  if (!data.type) {
59
- nonSpace.push(file);
81
+ parseIssues.push({ file, severity: 'warning', type: 'no-type' });
60
82
  continue;
61
83
  }
62
84
  if (ON_A_PAGE_TYPES.includes(data.type) && !options?.includeOnAPageFiles) {
@@ -81,9 +103,14 @@ export async function readSpaceDirectory(context, options) {
81
103
  });
82
104
  nodes.push(...embedded);
83
105
  for (const heading of terminatedHeadings) {
84
- nonSpace.push(`${file} > ${heading}`);
106
+ parseIssues.push({
107
+ file: `${file} > ${heading}`,
108
+ severity: 'warning',
109
+ type: 'terminated',
110
+ message: 'Ignored headings detected beyond end of hierarchy.',
111
+ });
85
112
  }
86
113
  }
87
114
  }
88
- return { nodes, parseIgnored: [...skipped, ...nonSpace], diagnostics: { kind: 'directory' } };
115
+ return { nodes, parseIssues, diagnostics: { kind: 'directory' } };
89
116
  }
@@ -1,6 +1,6 @@
1
1
  import type { AnySchemaObject } from 'ajv';
2
2
  import type { SpaceGraph } from '../space-graph';
3
- import type { BaseNode, SpaceContext } from '../types';
3
+ import type { BaseNode, ParseIssue, SpaceContext } from '../types';
4
4
  export declare const PLUGIN_PREFIX = "sctx-";
5
5
  export declare const CONFIG_PLUGINS_DIR = "plugins";
6
6
  /** Normalize a plugin name to its canonical prefixed form. */
@@ -11,8 +11,8 @@ export type PluginContext = SpaceContext & {
11
11
  };
12
12
  export type ParseResult = {
13
13
  nodes: BaseNode[];
14
- /** Paths/items the plugin skipped during parsing, for any reason. */
15
- parseIgnored: string[];
14
+ /** Issues encountered while parsing files — excluded files with reason and severity. */
15
+ parseIssues: ParseIssue[];
16
16
  /** Plugin diagnostics: keyed scalar or list values. */
17
17
  diagnostics: Record<string, number | string | string[]>;
18
18
  };
@@ -12,7 +12,7 @@ export async function readSpace(context) {
12
12
  return {
13
13
  nodes,
14
14
  source: plugin.name,
15
- parseIgnored: result.parseIgnored,
15
+ parseIssues: result.parseIssues,
16
16
  diagnostics: result.diagnostics,
17
17
  unresolvedRefs,
18
18
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json",
3
- "$id": "sctx://general",
3
+ "$id": "sctx://strategy_general",
4
4
  "title": "Strategy and opportunity ladder",
5
5
  "description": "Validates frontmatter for space node files",
6
6
  "$metadata": {
package/dist/types.d.ts CHANGED
@@ -86,6 +86,18 @@ export type SpaceNode = BaseNode & {
86
86
  */
87
87
  resolvedParents: ResolvedParentRef[];
88
88
  };
89
+ export type ParseIssue = {
90
+ /** Source identifier of the file or item (filename or heading title). */
91
+ file: string;
92
+ /** error: prevented the file from producing nodes; warning: file was silently excluded. */
93
+ severity: 'error' | 'warning';
94
+ /** empty: no content; no-type: no type field;
95
+ * parse: parse failure (syntax issue or unexpected content);
96
+ * plugins may choose to provide additional types. */
97
+ type: 'empty' | 'no-type' | 'parse' | string;
98
+ /** Human-readable detail. */
99
+ message?: string;
100
+ };
89
101
  /** Rule categories for organizing executable validation rules */
90
102
  export type RuleCategory = 'validation' | 'coherence' | 'workflow' | 'best-practice';
91
103
  export type { Rule } from './schema/metadata-contract';
@@ -118,8 +130,8 @@ export type SchemaWithMetadata = SchemaObject & {
118
130
  export type ReadSpaceResult = {
119
131
  /** Fully resolved nodes produced by the plugin and enriched by core. */
120
132
  nodes: SpaceNode[];
121
- /** Paths/items the plugin skipped during parsing, for any reason. */
122
- parseIgnored: string[];
133
+ /** Issues encountered while parsing files — excluded files with reason and severity. */
134
+ parseIssues: ParseIssue[];
123
135
  /** Plugin diagnostics: keyed scalar or list values. */
124
136
  diagnostics: Record<string, number | string | string[]>;
125
137
  /** Name of the plugin that produced the nodes. */
package/docs/config.md CHANGED
@@ -20,7 +20,7 @@ A space is a named directory or single file registered in the config. Example:
20
20
  {
21
21
  "name": "ProductX",
22
22
  "path": "/path/to/space",
23
- "schema": "general.json"
23
+ "schema": "strategy_general.json"
24
24
  }
25
25
  ]
26
26
  }
package/docs/schemas.md CHANGED
@@ -20,11 +20,11 @@ Set `schema` in the space config entry:
20
20
  }
21
21
  ```
22
22
 
23
- Resolution order: space `schema` > global `schema` > bundled `schemas/general.json`.
23
+ Resolution order: space `schema` > global `schema`. Schema resolution fails if none is configured.
24
24
 
25
25
  ## Bundled schemas
26
26
 
27
- ### `general.json` (default)
27
+ ### `strategy_general.json`
28
28
 
29
29
  Flexible planning schema spanning strategy + OST-like flow.
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Tools for working with Opportunity Solution Tree structures and other product management and strategy frameworks",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json",
3
- "$id": "sctx://general",
3
+ "$id": "sctx://strategy_general",
4
4
  "title": "Strategy and opportunity ladder",
5
5
  "description": "Validates frontmatter for space node files",
6
6
  "$metadata": {