structured-context 0.9.3 → 0.10.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/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public API for structured-context.
3
+ *
4
+ * Import from this module to use structured-context as a library:
5
+ *
6
+ * import { validateFile } from 'structured-context/api';
7
+ * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
8
+ */
9
+ export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
10
+ export type { Config, SpaceConfig } from './config';
11
+ export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
12
+ export type { SharedEmbeddingFields } from './schema/metadata-contract';
13
+ export type { SpaceGraph } from './space-graph';
14
+ export type { BaseNode, EdgeDefinition, FileNotInSpaceResult, FileValidationResult, HierarchyLevel, ParseIssue, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
15
+ export { validateFile } from './validate';
package/dist/api.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public API for structured-context.
3
+ *
4
+ * Import from this module to use structured-context as a library:
5
+ *
6
+ * import { validateFile } from 'structured-context/api';
7
+ * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
8
+ */
9
+ export { validateFile } from './validate';
@@ -1,4 +1,4 @@
1
- import { JSON5 } from 'bun';
1
+ import JSON5 from 'json5';
2
2
  import { readSpace } from '../read/read-space';
3
3
  export async function dump(context) {
4
4
  const { nodes, source, diagnostics } = await readSpace(context);
@@ -1,28 +1,9 @@
1
- export interface FileValidationResult {
2
- file: string;
3
- label: string;
4
- space: string;
5
- /** Errors keyed by composite id (e.g. `schema:/status:enum:active`, `rule:my-rule-id`). */
6
- errors: Record<string, {
7
- kind: string;
8
- message: string;
9
- }>;
10
- errorCount: number;
11
- inSpace: true;
12
- }
13
- export interface FileNotInSpaceResult {
14
- file: string;
15
- inSpace: false;
16
- message: string;
17
- }
18
- export type ValidateFileOutput = FileValidationResult | FileNotInSpaceResult;
1
+ export { validateFile } from '../validate';
19
2
  /**
20
- * Validate a single file within its space.
21
- *
22
- * Reads the full space (required for graph correctness) but filters all reported
23
- * errors to only those attributable to the target file. Exits 0 if the file is
24
- * not in any configured space (not an error — hooks call this on all file writes).
3
+ * CLI command: validate a file and print results to stdout.
4
+ * Loads config automatically from the environment, outputs human-readable or JSON,
5
+ * and returns an exit code (0 = valid, 1 = errors found).
25
6
  */
26
- export declare function validateFile(filePath: string, options?: {
7
+ export declare function validateFileCommand(filePath: string, options?: {
27
8
  json?: boolean;
28
9
  }): Promise<number>;
@@ -1,121 +1,27 @@
1
- import { isAbsolute, relative, resolve } from 'node:path';
2
- import { getSpaceConfigDir, loadConfig, resolveSchema } from '../config';
3
- import { readSpace } from '../read/read-space';
4
- import { loadSchema } from '../schema/schema';
5
- import { validateGraph } from '../schema/validate-graph';
6
- import { validateRules } from '../schema/validate-rules';
7
- import { formatErrors } from './validate';
8
- /** Find which space a file belongs to by checking directory containment. */
9
- function resolveFileSpace(filePath, config) {
10
- const absFile = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
11
- for (const space of config.spaces) {
12
- const absSpace = isAbsolute(space.path) ? space.path : resolve(process.cwd(), space.path);
13
- // Trailing slash prevents prefix-match false positives (e.g. /foo matching /foobar/)
14
- const spaceDir = absSpace.endsWith('/') ? absSpace : `${absSpace}/`;
15
- if (absFile.startsWith(spaceDir) || absFile === absSpace) {
16
- return { space, label: relative(absSpace, absFile) };
17
- }
18
- }
19
- return null;
20
- }
21
- function buildContextForSpace(space, config) {
22
- const resolvedSchemaPath = resolveSchema(config, space);
23
- const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
24
- const configDir = getSpaceConfigDir(space.name);
25
- return { space, config, resolvedSchemaPath, schema, schemaRefRegistry, schemaValidator, configDir };
26
- }
1
+ import { loadConfig } from '../config';
2
+ import { validateFile } from '../validate';
3
+ export { validateFile } from '../validate';
27
4
  /**
28
- * Validate a single file within its space.
29
- *
30
- * Reads the full space (required for graph correctness) but filters all reported
31
- * errors to only those attributable to the target file. Exits 0 if the file is
32
- * not in any configured space (not an error — hooks call this on all file writes).
5
+ * CLI command: validate a file and print results to stdout.
6
+ * Loads config automatically from the environment, outputs human-readable or JSON,
7
+ * and returns an exit code (0 = valid, 1 = errors found).
33
8
  */
34
- export async function validateFile(filePath, options = {}) {
9
+ export async function validateFileCommand(filePath, options = {}) {
35
10
  const config = loadConfig();
36
- const resolved = resolveFileSpace(filePath, config);
37
- if (!resolved) {
38
- const result = {
39
- file: filePath,
40
- inSpace: false,
41
- message: 'File does not belong to any configured space.',
42
- };
11
+ const result = await validateFile(filePath, config);
12
+ if (!result.inSpace) {
43
13
  if (options.json) {
44
14
  console.log(JSON.stringify(result, null, 2));
45
15
  }
46
16
  return 0;
47
17
  }
48
- const { space, label } = resolved;
49
- const context = buildContextForSpace(space, config);
50
- const { schema, schemaRefRegistry, schemaValidator } = context;
51
- const metadata = schema.metadata;
52
- const readResult = await readSpace(context);
53
- const { nodes } = readResult;
54
- const errors = {};
55
- // Schema validation errors for this node
56
- for (const node of nodes) {
57
- if (node.label !== label)
58
- continue;
59
- const valid = schemaValidator(node.schemaData);
60
- if (!valid) {
61
- const formatted = formatErrors(schemaValidator.errors ?? [], schema, schemaRefRegistry, node.schemaData);
62
- for (const { message, dedupeKey } of formatted) {
63
- errors[`schema:${dedupeKey}`] = { kind: 'schema', message };
64
- }
65
- }
66
- }
67
- // Duplicate key errors — include if this file is one of the duplicates
68
- const titleToFiles = new Map();
69
- for (const node of nodes) {
70
- if (!titleToFiles.has(node.title))
71
- titleToFiles.set(node.title, []);
72
- titleToFiles.get(node.title).push(node.label);
73
- }
74
- for (const [title, files] of titleToFiles) {
75
- if (files.length > 1 && files.includes(label)) {
76
- const others = files.filter((f) => f !== label);
77
- errors[`duplicate:${title}`] = {
78
- kind: 'duplicate',
79
- message: `Duplicate title "${title}" also exists in: ${others.join(', ')}`,
80
- };
81
- }
82
- }
83
- // Broken links and hierarchy violations from this file
84
- const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
85
- for (const { file, parent, error } of hierarchyValidation.refErrors) {
86
- if (file === label) {
87
- errors[`broken-link:${parent}`] = { kind: 'broken-link', message: `${parent} → ${error}` };
88
- }
89
- }
90
- for (const v of hierarchyValidation.violations) {
91
- if (v.file === label) {
92
- errors[`hierarchy:${v.description}`] = { kind: 'hierarchy', message: v.description };
93
- }
94
- }
95
- // Rule violations for this node
96
- if (metadata.rules) {
97
- const ruleViolations = await validateRules(nodes, metadata.rules);
98
- for (const v of ruleViolations) {
99
- if (v.file === label) {
100
- errors[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}` };
101
- }
102
- }
103
- }
104
- const result = {
105
- file: isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath),
106
- label,
107
- space: space.name,
108
- errors,
109
- errorCount: Object.keys(errors).length,
110
- inSpace: true,
111
- };
112
18
  if (options.json) {
113
19
  console.log(JSON.stringify(result, null, 2));
114
20
  }
115
21
  else {
116
22
  printHumanReadable(result);
117
23
  }
118
- return Object.keys(errors).length > 0 ? 1 : 0;
24
+ return Object.keys(result.errors).length > 0 ? 1 : 0;
119
25
  }
120
26
  function printHumanReadable(result) {
121
27
  const reset = '\x1b[0m';
@@ -1,15 +1,4 @@
1
- import type { ErrorObject } from 'ajv';
2
- import { extractEntityInfo } from '../schema/schema';
3
- import type { SchemaWithMetadata, SpaceContext } from '../types';
4
- export interface FormattedError {
5
- message: string;
6
- dedupeKey: string;
7
- }
8
- /**
9
- * Format AJV errors for better readability.
10
- * Groups related errors and extracts helpful context like allowed values.
11
- */
12
- export declare function formatErrors(errors: ErrorObject[], schema: SchemaWithMetadata, schemaRefRegistry: Parameters<typeof extractEntityInfo>[1], nodeData: Record<string, unknown>): FormattedError[];
1
+ import type { SpaceContext } from '../types';
13
2
  export declare function validate(context: SpaceContext, options?: {
14
3
  json?: boolean;
15
4
  }): Promise<number>;
@@ -1,154 +1,11 @@
1
1
  import { dirname } from 'node:path';
2
2
  import chokidar from 'chokidar';
3
3
  import { getConfigSourceFiles } from '../config';
4
- import { readSpace } from '../read/read-space';
5
- import { bundledSchemasDir, extractEntityInfo } from '../schema/schema';
6
- import { validateGraph } from '../schema/validate-graph';
7
- import { validateRules } from '../schema/validate-rules';
8
- import { buildSpaceGraph } from '../space-graph';
9
- /**
10
- * Format AJV errors for better readability.
11
- * Groups related errors and extracts helpful context like allowed values.
12
- */
13
- export function formatErrors(errors, schema, schemaRefRegistry, nodeData) {
14
- const formatted = [];
15
- const seen = new Set();
16
- // Group errors by instancePath
17
- const byPath = new Map();
18
- for (const err of errors) {
19
- const path = err.instancePath || 'root';
20
- if (!byPath.has(path)) {
21
- byPath.set(path, []);
22
- }
23
- byPath.get(path).push(err);
24
- }
25
- for (const [path, pathErrors] of byPath) {
26
- // Check if this is a oneOf failure at root - extract valid types from schema
27
- const isRootOneOf = path === 'root' || path === '/type';
28
- let hasOneOfContext = false;
29
- if (isRootOneOf && pathErrors.length > 1) {
30
- hasOneOfContext = Array.isArray(schema.oneOf);
31
- if (hasOneOfContext) {
32
- const entities = extractEntityInfo(schema, schemaRefRegistry);
33
- const validTypes = entities.map((e) => e.type).sort();
34
- if (validTypes.length > 0) {
35
- const actualValue = nodeData.type;
36
- // Only show type error if the actual type is NOT in the valid types list
37
- if (actualValue !== undefined && !validTypes.includes(String(actualValue))) {
38
- const message = `Invalid type "${actualValue}". Valid types are: ${validTypes.join(', ')}`;
39
- const key = `type:${validTypes.join(',')}`;
40
- if (!seen.has(key)) {
41
- seen.add(key);
42
- formatted.push({ message, dedupeKey: key });
43
- }
44
- }
45
- }
46
- }
47
- }
48
- // Handle individual errors
49
- for (const err of pathErrors) {
50
- // Skip individual type const/enum errors when in oneOf context (we handle it above)
51
- if (hasOneOfContext &&
52
- (err.keyword === 'const' || err.keyword === 'enum') &&
53
- (err.instancePath === '' || err.instancePath === '/type')) {
54
- continue;
55
- }
56
- const parts = err.instancePath.split('/').filter(Boolean);
57
- const fieldName = parts.length > 0 ? parts[parts.length - 1] : 'root';
58
- let message = err.message;
59
- let key = `${err.instancePath}:${err.keyword}`;
60
- // Enhance const errors
61
- if (err.keyword === 'const' && err.params?.allowedValue !== undefined) {
62
- const actual = err.data !== undefined ? `"${err.data}"` : 'missing value';
63
- const expected = `"${err.params.allowedValue}"`;
64
- message = `${fieldName}: expected ${expected}, got ${actual}`;
65
- key = `${err.instancePath}:const:${err.params.allowedValue}`;
66
- }
67
- // Enhance enum errors
68
- else if (err.keyword === 'enum' && err.params?.allowedValues && Array.isArray(err.params.allowedValues)) {
69
- let actual = err.data !== undefined ? `"${err.data}"` : null;
70
- // If err.data is undefined, try to get the value from nodeData using the field name
71
- if (actual === null && fieldName !== 'root') {
72
- const actualValue = nodeData[fieldName];
73
- if (actualValue !== undefined) {
74
- actual = `"${actualValue}"`;
75
- }
76
- }
77
- if (actual === null) {
78
- actual = 'missing value';
79
- }
80
- const allowed = err.params.allowedValues.map((v) => `"${v}"`).join(', ');
81
- message = `${fieldName}: ${actual} is not valid. Allowed: ${allowed}`;
82
- key = `${err.instancePath}:enum:${err.params.allowedValues.join(',')}`;
83
- }
84
- // Generic message with path
85
- else {
86
- message = `${fieldName}: ${err.message}`;
87
- }
88
- if (!seen.has(key)) {
89
- seen.add(key);
90
- formatted.push({ message, dedupeKey: key });
91
- }
92
- }
93
- }
94
- return formatted;
95
- }
4
+ import { bundledSchemasDir } from '../schema/schema';
5
+ import { formatErrors, validateSpace } from '../validate';
96
6
  export async function validate(context, options = {}) {
97
- const { schema, schemaRefRegistry, schemaValidator } = context;
98
- const metadata = schema.metadata;
99
- const readResult = await readSpace(context);
100
- const { nodes, parseIssues } = readResult;
101
- const result = {
102
- validCount: 0,
103
- nodeErrorCount: 0,
104
- nodeErrors: [],
105
- refErrors: [],
106
- duplicateErrors: [],
107
- ruleViolations: [],
108
- hierarchyViolations: [],
109
- orphans: [],
110
- parseIssues,
111
- };
112
- for (const node of nodes) {
113
- const valid = schemaValidator(node.schemaData);
114
- if (valid) {
115
- result.validCount++;
116
- }
117
- else {
118
- result.nodeErrorCount++;
119
- result.nodeErrors.push({
120
- file: node.label,
121
- errors: schemaValidator.errors || [],
122
- nodeData: node.schemaData,
123
- });
124
- }
125
- }
126
- // Detect duplicate node keys (titles)
127
- const titleToFiles = new Map();
128
- for (const node of nodes) {
129
- const title = node.title;
130
- if (!titleToFiles.has(title)) {
131
- titleToFiles.set(title, []);
132
- }
133
- titleToFiles.get(title).push(node.label);
134
- }
135
- for (const [title, files] of titleToFiles) {
136
- if (files.length > 1) {
137
- result.duplicateErrors.push({ title, files });
138
- }
139
- }
140
- // Validate all hierarchy constraints (field references and structure)
141
- const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
142
- result.refErrors.push(...hierarchyValidation.refErrors);
143
- result.hierarchyViolations = [...hierarchyValidation.violations];
144
- // Calculate orphan count (informational, not a validation error)
145
- if (metadata.hierarchy) {
146
- result.orphans = [...buildSpaceGraph(nodes, metadata.hierarchy.levels).orphans];
147
- }
148
- // Load and execute rules validation if schema defines rules
149
- if (metadata.rules) {
150
- result.ruleViolations = await validateRules(nodes, metadata.rules);
151
- }
7
+ const { schema, schemaRefRegistry } = context;
8
+ const result = await validateSpace(context);
152
9
  // JSON output mode
153
10
  if (options.json) {
154
11
  const errorsByFile = {};
@@ -193,7 +50,7 @@ export async function validate(context, options = {}) {
193
50
  }, null, 2));
194
51
  return errorCount > 0 ? 1 : 0;
195
52
  }
196
- // Report
53
+ // Human-readable output
197
54
  const reset = '\x1b[0m';
198
55
  const green = '\x1b[32m';
199
56
  const red = '\x1b[31m';
@@ -209,7 +66,6 @@ export async function validate(context, options = {}) {
209
66
  color = colorFor(count, isWarning);
210
67
  }
211
68
  else {
212
- // For non-error items (like "Valid"), green if count > 0, red if 0
213
69
  color = count > 0 ? green : red;
214
70
  }
215
71
  const countStr = String(count).padStart(3);
@@ -266,15 +122,12 @@ export async function validate(context, options = {}) {
266
122
  }
267
123
  if (result.ruleViolations.length > 0) {
268
124
  console.log(`\nRule violations:`);
269
- // Group by category
270
125
  const byCategory = new Map();
271
126
  for (const v of result.ruleViolations) {
272
- if (!byCategory.has(v.category)) {
127
+ if (!byCategory.has(v.category))
273
128
  byCategory.set(v.category, []);
274
- }
275
129
  byCategory.get(v.category).push(v);
276
130
  }
277
- // Report each category
278
131
  for (const [category, violations] of byCategory) {
279
132
  console.log(` ${category.toUpperCase()} (${violations.length}):`);
280
133
  for (const v of violations) {
@@ -289,7 +142,6 @@ export async function validate(context, options = {}) {
289
142
  }
290
143
  }
291
144
  console.log(`\n`);
292
- // Return exit code (0 for success, 1 for validation failures)
293
145
  if (result.nodeErrorCount > 0 ||
294
146
  result.refErrors.length > 0 ||
295
147
  result.duplicateErrors.length > 0 ||
@@ -314,7 +166,6 @@ export async function watchValidate(context) {
314
166
  console.log(` Schema dirs: ${schemaDirs.join(', ')}`);
315
167
  console.log(` Space: ${spacePath}`);
316
168
  console.log(` Press Ctrl+C to stop\n`);
317
- // Save cursor position after header (for clearing later)
318
169
  process.stdout.write('\x1b[s');
319
170
  let exitCode = 0;
320
171
  const innerValidate = async () => {
@@ -336,7 +187,6 @@ export async function watchValidate(context) {
336
187
  },
337
188
  });
338
189
  const handleFileChange = async (filePath, action) => {
339
- // Restore cursor to header position and clear everything below
340
190
  process.stdout.write('\x1b[u\x1b[0J');
341
191
  console.log(`🔄 ${filePath} ${action}, re-validating...\n`);
342
192
  await innerValidate();
@@ -347,7 +197,6 @@ export async function watchValidate(context) {
347
197
  watcher.on('error', (error) => {
348
198
  console.error(`Watcher error: ${error}`);
349
199
  });
350
- // Keep process alive
351
200
  return new Promise((_, reject) => {
352
201
  process.on('SIGINT', () => {
353
202
  console.log('\n\n👋 Stopping watch mode...');
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ 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
4
  import Ajv from 'ajv';
5
- import { JSON5 } from 'bun';
5
+ import JSON5 from 'json5';
6
6
  import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
7
7
  import { normalizePluginName } from './plugins/util';
8
8
  import { bundledSchemasDir } from './schema/schema';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { show } from './commands/show';
11
11
  import { listSpaces } from './commands/spaces';
12
12
  import { templateSync } from './commands/template-sync';
13
13
  import { validate, watchValidate } from './commands/validate';
14
- import { validateFile } from './commands/validate-file';
14
+ import { validateFileCommand } from './commands/validate-file';
15
15
  import { getSpaceConfigDir, loadConfig, resolveSchema, setConfigPath } from './config';
16
16
  import { CLI_NAME } from './constants';
17
17
  import { miroSync } from './integrations/miro/sync';
@@ -53,7 +53,7 @@ program
53
53
  .argument('<path>', 'Path to the file to validate')
54
54
  .option('--json', 'Output results as JSON (machine-readable, for hooks)')
55
55
  .action(async (filePath, options) => {
56
- const exitCode = await validateFile(filePath, { json: options.json });
56
+ const exitCode = await validateFileCommand(filePath, { json: options.json });
57
57
  process.exit(exitCode);
58
58
  });
59
59
  program
@@ -1,48 +1,58 @@
1
+ import type { FromSchema } from 'json-schema-to-ts';
1
2
  import type { StructuredContextPlugin } from '../util';
2
- export type TypeInferenceConfig = {
3
- mode?: 'folder-name' | 'off';
4
- folderMap?: Record<string, string>;
5
- };
6
- export type MarkdownPluginConfig = {
7
- templateDir?: string;
8
- fieldMap?: Record<string, string>;
9
- templatePrefix?: string;
10
- typeInference?: TypeInferenceConfig;
3
+ declare const TYPE_INFERENCE_CONFIG_SCHEMA: {
4
+ readonly type: "object";
5
+ readonly properties: {
6
+ readonly mode: {
7
+ readonly type: "string";
8
+ readonly enum: readonly ["folder-name", "off"];
9
+ };
10
+ readonly folderMap: {
11
+ readonly type: "object";
12
+ readonly additionalProperties: {
13
+ readonly type: "string";
14
+ };
15
+ };
16
+ };
17
+ readonly additionalProperties: false;
11
18
  };
12
19
  export declare const MARKDOWN_CONFIG_SCHEMA: {
13
- type: string;
14
- properties: {
15
- templateDir: {
16
- type: string;
17
- format: string;
20
+ readonly type: "object";
21
+ readonly properties: {
22
+ readonly templateDir: {
23
+ readonly type: "string";
24
+ readonly format: "path";
18
25
  };
19
- fieldMap: {
20
- type: string;
21
- additionalProperties: {
22
- type: string;
26
+ readonly fieldMap: {
27
+ readonly type: "object";
28
+ readonly additionalProperties: {
29
+ readonly type: "string";
23
30
  };
24
31
  };
25
- templatePrefix: {
26
- type: string;
32
+ readonly templatePrefix: {
33
+ readonly type: "string";
27
34
  };
28
- typeInference: {
29
- type: string;
30
- properties: {
31
- mode: {
32
- type: string;
33
- enum: string[];
35
+ readonly typeInference: {
36
+ readonly type: "object";
37
+ readonly properties: {
38
+ readonly mode: {
39
+ readonly type: "string";
40
+ readonly enum: readonly ["folder-name", "off"];
34
41
  };
35
- folderMap: {
36
- type: string;
37
- additionalProperties: {
38
- type: string;
42
+ readonly folderMap: {
43
+ readonly type: "object";
44
+ readonly additionalProperties: {
45
+ readonly type: "string";
39
46
  };
40
47
  };
41
48
  };
42
- additionalProperties: boolean;
49
+ readonly additionalProperties: false;
43
50
  };
44
51
  };
45
- additionalProperties: boolean;
52
+ readonly additionalProperties: false;
46
53
  };
54
+ export type MarkdownPluginConfig = FromSchema<typeof MARKDOWN_CONFIG_SCHEMA>;
55
+ export type TypeInferenceConfig = FromSchema<typeof TYPE_INFERENCE_CONFIG_SCHEMA>;
47
56
  export declare function getMarkdownConfig(plugins?: Record<string, Record<string, unknown>>): MarkdownPluginConfig;
48
57
  export declare const markdownPlugin: StructuredContextPlugin;
58
+ export {};
@@ -4,20 +4,21 @@ import { readSpaceDirectory, readSpaceOnAPage } from './read-space';
4
4
  import { renderBullets } from './render-bullets';
5
5
  import { renderMermaid } from './render-mermaid';
6
6
  import { templateSync } from './template-sync';
7
+ const TYPE_INFERENCE_CONFIG_SCHEMA = {
8
+ type: 'object',
9
+ properties: {
10
+ mode: { type: 'string', enum: ['folder-name', 'off'] },
11
+ folderMap: { type: 'object', additionalProperties: { type: 'string' } },
12
+ },
13
+ additionalProperties: false,
14
+ };
7
15
  export const MARKDOWN_CONFIG_SCHEMA = {
8
16
  type: 'object',
9
17
  properties: {
10
18
  templateDir: { type: 'string', format: 'path' }, // format is hint to config loader to resolve relative directories
11
19
  fieldMap: { type: 'object', additionalProperties: { type: 'string' } },
12
20
  templatePrefix: { type: 'string' },
13
- typeInference: {
14
- type: 'object',
15
- properties: {
16
- mode: { type: 'string', enum: ['folder-name', 'off'] },
17
- folderMap: { type: 'object', additionalProperties: { type: 'string' } },
18
- },
19
- additionalProperties: false,
20
- },
21
+ typeInference: TYPE_INFERENCE_CONFIG_SCHEMA,
21
22
  },
22
23
  additionalProperties: false,
23
24
  };
@@ -1,4 +1,4 @@
1
- import type { BaseNode, Relationship, SchemaMetadata } from '../../plugin-api';
1
+ import type { BaseNode, Relationship, SchemaMetadata } from '../../api';
2
2
  /** Type values that identify a space_on_a_page container (not themselves space nodes). */
3
3
  export declare const ON_A_PAGE_TYPES: string[];
4
4
  type StackEntry = {
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename, join, resolve } from 'node:path';
3
- import { Glob } from 'bun';
3
+ import fg from 'fast-glob';
4
4
  import matter from 'gray-matter';
5
5
  import { extractSchemaTypeNames } from '../../schema/schema';
6
6
  import { extractEmbeddedNodes, ON_A_PAGE_TYPES } from './parse-embedded';
@@ -42,7 +42,7 @@ export async function readSpaceDirectory(context, options) {
42
42
  const absoluteTemplateDir = templateDir ? resolve(templateDir) : undefined;
43
43
  const typeInferenceCfg = mdCfg.typeInference;
44
44
  const knownTypes = typeInferenceCfg?.mode !== 'off' ? extractSchemaTypeNames(context.schema, context.schemaRefRegistry) : undefined;
45
- const files = await Array.fromAsync(new Glob('**/*.md').scan({ cwd: directory, followSymlinks: true }));
45
+ const files = await fg('**/*.md', { cwd: directory, followSymbolicLinks: true });
46
46
  const nodes = [];
47
47
  const parseIssues = [];
48
48
  for (const file of files) {
@@ -70,7 +70,7 @@ export async function readSpaceDirectory(context, options) {
70
70
  continue;
71
71
  }
72
72
  if (!parsed.data || Object.keys(parsed.data).length === 0) {
73
- parseIssues.push({ file, severity: 'warning', type: 'empty' });
73
+ parseIssues.push({ file, severity: 'warning', type: 'no-type', message: 'No front-matter or type specified' });
74
74
  continue;
75
75
  }
76
76
  const data = coerceDates(applyFieldMap(parsed.data, fieldMap));
@@ -1,5 +1,5 @@
1
1
  import type { AnySchemaObject } from 'ajv';
2
- import type { HierarchyLevel, Relationship, SchemaWithMetadata } from '../../plugin-api';
2
+ import type { HierarchyLevel, Relationship, SchemaWithMetadata } from '../../api';
3
3
  import type { PluginContext, TemplateSyncOptions } from '../util';
4
4
  export interface TypeVariant {
5
5
  required: string[];
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { Glob } from 'bun';
3
+ import fg from 'fast-glob';
4
4
  import matter from 'gray-matter';
5
5
  import yaml from 'js-yaml';
6
6
  import { mergeVariantProperties, resolveRef } from '../../schema/schema-refs';
@@ -190,7 +190,7 @@ export async function templateSync(context, options) {
190
190
  const { schema, schemaRefRegistry } = context;
191
191
  const typeVariants = getTypeVariants(schema, schemaRefRegistry);
192
192
  const matchedTypes = new Set();
193
- const files = await Array.fromAsync(new Glob('*.md').scan({ cwd: templateDir, absolute: true }));
193
+ const files = await fg('*.md', { cwd: templateDir, absolute: true });
194
194
  const dryRun = options.dryRun ?? false;
195
195
  let filesModified = 0;
196
196
  let filesCreated = 0;
@@ -3,7 +3,7 @@ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { isDeepStrictEqual } from 'node:util';
5
5
  import Ajv, {} from 'ajv';
6
- import { JSON5 } from 'bun';
6
+ import JSON5 from 'json5';
7
7
  import { DIALECT_META_SCHEMA, METADATA_SCHEMA, SCHEMA_META_ID, } from './metadata-contract';
8
8
  import { isObject, mergeVariantProperties, resolveJsonPointer } from './schema-refs';
9
9
  const packageDir = dirname(fileURLToPath(import.meta.url));
package/dist/types.d.ts CHANGED
@@ -91,10 +91,10 @@ export type ParseIssue = {
91
91
  file: string;
92
92
  /** error: prevented the file from producing nodes; warning: file was silently excluded. */
93
93
  severity: 'error' | 'warning';
94
- /** empty: no content; no-type: no type field;
94
+ /** no-type: no type field;
95
95
  * parse: parse failure (syntax issue or unexpected content);
96
96
  * plugins may choose to provide additional types. */
97
- type: 'empty' | 'no-type' | 'parse' | string;
97
+ type: 'no-type' | 'parse' | string;
98
98
  /** Human-readable detail. */
99
99
  message?: string;
100
100
  };
@@ -155,3 +155,21 @@ export type SpaceContext = {
155
155
  /** Directory of the config file that defines this space. Used for resolving relative paths in plugin configs. */
156
156
  configDir: string;
157
157
  };
158
+ export interface FileValidationResult {
159
+ file: string;
160
+ label: string;
161
+ space: string;
162
+ /** Errors keyed by composite id (e.g. `schema:/status:enum:active`, `rule:my-rule-id`). */
163
+ errors: Record<string, {
164
+ kind: string;
165
+ message: string;
166
+ }>;
167
+ errorCount: number;
168
+ inSpace: true;
169
+ }
170
+ export interface FileNotInSpaceResult {
171
+ file: string;
172
+ inSpace: false;
173
+ message: string;
174
+ }
175
+ export type ValidateFileOutput = FileValidationResult | FileNotInSpaceResult;
@@ -0,0 +1,50 @@
1
+ import type { ErrorObject } from 'ajv';
2
+ import type { Config } from './config';
3
+ import { extractEntityInfo } from './schema/schema';
4
+ import type { GraphViolation, ParseIssue, RuleViolation, SchemaWithMetadata, SpaceContext, SpaceNode, ValidateFileOutput } from './types';
5
+ export interface FormattedError {
6
+ message: string;
7
+ dedupeKey: string;
8
+ }
9
+ export interface ValidationResult {
10
+ validCount: number;
11
+ nodeErrorCount: number;
12
+ nodeErrors: Array<{
13
+ file: string;
14
+ errors: ErrorObject[];
15
+ nodeData: Record<string, unknown>;
16
+ }>;
17
+ refErrors: Array<{
18
+ file: string;
19
+ parent: string;
20
+ error: string;
21
+ }>;
22
+ duplicateErrors: Array<{
23
+ title: string;
24
+ files: string[];
25
+ }>;
26
+ ruleViolations: RuleViolation[];
27
+ hierarchyViolations: GraphViolation[];
28
+ orphans: SpaceNode[];
29
+ parseIssues: ParseIssue[];
30
+ }
31
+ /**
32
+ * Format AJV errors for better readability.
33
+ * Groups related errors and extracts helpful context like allowed values.
34
+ */
35
+ export declare function formatErrors(errors: ErrorObject[], schema: SchemaWithMetadata, schemaRefRegistry: Parameters<typeof extractEntityInfo>[1], nodeData: Record<string, unknown>): FormattedError[];
36
+ /**
37
+ * Run full validation on a space and return a structured result.
38
+ * Does not output to console or call process.exit.
39
+ */
40
+ export declare function validateSpace(context: SpaceContext): Promise<ValidationResult>;
41
+ /**
42
+ * Validate a single file within its space.
43
+ *
44
+ * Reads the full space (required for graph correctness) but filters all reported
45
+ * errors to only those attributable to the target file.
46
+ *
47
+ * Returns a result object — does not output to console or call process.exit.
48
+ * If the file is not in any configured space, returns a result with `inSpace: false`.
49
+ */
50
+ export declare function validateFile(filePath: string, config: Config): Promise<ValidateFileOutput>;
@@ -0,0 +1,291 @@
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
+ import { getSpaceConfigDir, resolveSchema } from './config';
3
+ import { readSpace } from './read/read-space';
4
+ import { extractEntityInfo, loadSchema } from './schema/schema';
5
+ import { validateGraph } from './schema/validate-graph';
6
+ import { validateRules } from './schema/validate-rules';
7
+ import { buildSpaceGraph } from './space-graph';
8
+ /**
9
+ * Format AJV errors for better readability.
10
+ * Groups related errors and extracts helpful context like allowed values.
11
+ */
12
+ export function formatErrors(errors, schema, schemaRefRegistry, nodeData) {
13
+ const formatted = [];
14
+ const seen = new Set();
15
+ // Group errors by instancePath
16
+ const byPath = new Map();
17
+ for (const err of errors) {
18
+ const path = err.instancePath || 'root';
19
+ if (!byPath.has(path)) {
20
+ byPath.set(path, []);
21
+ }
22
+ byPath.get(path).push(err);
23
+ }
24
+ for (const [path, pathErrors] of byPath) {
25
+ // Check if this is a oneOf failure at root - extract valid types from schema
26
+ const isRootOneOf = path === 'root' || path === '/type';
27
+ let hasOneOfContext = false;
28
+ if (isRootOneOf && pathErrors.length > 1) {
29
+ hasOneOfContext = Array.isArray(schema.oneOf);
30
+ if (hasOneOfContext) {
31
+ const entities = extractEntityInfo(schema, schemaRefRegistry);
32
+ const validTypes = entities.map((e) => e.type).sort();
33
+ if (validTypes.length > 0) {
34
+ const actualValue = nodeData.type;
35
+ // Only show type error if the actual type is NOT in the valid types list
36
+ if (actualValue !== undefined && !validTypes.includes(String(actualValue))) {
37
+ const message = `Invalid type "${actualValue}". Valid types are: ${validTypes.join(', ')}`;
38
+ const key = `type:${validTypes.join(',')}`;
39
+ if (!seen.has(key)) {
40
+ seen.add(key);
41
+ formatted.push({ message, dedupeKey: key });
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
47
+ // Handle individual errors
48
+ for (const err of pathErrors) {
49
+ // Skip individual type const/enum errors when in oneOf context (we handle it above)
50
+ if (hasOneOfContext &&
51
+ (err.keyword === 'const' || err.keyword === 'enum') &&
52
+ (err.instancePath === '' || err.instancePath === '/type')) {
53
+ continue;
54
+ }
55
+ const parts = err.instancePath.split('/').filter(Boolean);
56
+ const fieldName = parts.length > 0 ? parts[parts.length - 1] : 'root';
57
+ let message = err.message;
58
+ let key = `${err.instancePath}:${err.keyword}`;
59
+ // Enhance const errors
60
+ if (err.keyword === 'const' && err.params?.allowedValue !== undefined) {
61
+ const actual = err.data !== undefined ? `"${err.data}"` : 'missing value';
62
+ const expected = `"${err.params.allowedValue}"`;
63
+ message = `${fieldName}: expected ${expected}, got ${actual}`;
64
+ key = `${err.instancePath}:const:${err.params.allowedValue}`;
65
+ }
66
+ // Enhance enum errors
67
+ else if (err.keyword === 'enum' && err.params?.allowedValues && Array.isArray(err.params.allowedValues)) {
68
+ let actual = err.data !== undefined ? `"${err.data}"` : null;
69
+ // If err.data is undefined, try to get the value from nodeData using the field name
70
+ if (actual === null && fieldName !== 'root') {
71
+ const actualValue = nodeData[fieldName];
72
+ if (actualValue !== undefined) {
73
+ actual = `"${actualValue}"`;
74
+ }
75
+ }
76
+ if (actual === null) {
77
+ actual = 'missing value';
78
+ }
79
+ const allowed = err.params.allowedValues.map((v) => `"${v}"`).join(', ');
80
+ message = `${fieldName}: ${actual} is not valid. Allowed: ${allowed}`;
81
+ key = `${err.instancePath}:enum:${err.params.allowedValues.join(',')}`;
82
+ }
83
+ // Generic message with path
84
+ else {
85
+ message = `${fieldName}: ${err.message}`;
86
+ }
87
+ if (!seen.has(key)) {
88
+ seen.add(key);
89
+ formatted.push({ message, dedupeKey: key });
90
+ }
91
+ }
92
+ }
93
+ return formatted;
94
+ }
95
+ /**
96
+ * Run full validation on a space and return a structured result.
97
+ * Does not output to console or call process.exit.
98
+ */
99
+ export async function validateSpace(context) {
100
+ const { schema, schemaRefRegistry, schemaValidator } = context;
101
+ const metadata = schema.metadata;
102
+ const readResult = await readSpace(context);
103
+ const { nodes, parseIssues } = readResult;
104
+ // Pre-extract valid types for early type validation
105
+ const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
106
+ const result = {
107
+ validCount: 0,
108
+ nodeErrorCount: 0,
109
+ nodeErrors: [],
110
+ refErrors: [],
111
+ duplicateErrors: [],
112
+ ruleViolations: [],
113
+ hierarchyViolations: [],
114
+ orphans: [],
115
+ parseIssues,
116
+ };
117
+ for (const node of nodes) {
118
+ // Early type validation - check before full AJV validation to prevent cascading errors
119
+ const nodeType = node.schemaData.type;
120
+ if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
121
+ result.nodeErrorCount++;
122
+ result.nodeErrors.push({
123
+ file: node.label,
124
+ errors: [
125
+ {
126
+ instancePath: '/type',
127
+ keyword: 'enum',
128
+ message: `Invalid type "${nodeType}". Valid types are: ${validTypes.sort().join(', ')}`,
129
+ params: { allowedValues: validTypes },
130
+ schemaPath: '#/oneOf',
131
+ data: nodeType,
132
+ },
133
+ ],
134
+ nodeData: node.schemaData,
135
+ });
136
+ continue;
137
+ }
138
+ const valid = schemaValidator(node.schemaData);
139
+ if (valid) {
140
+ result.validCount++;
141
+ }
142
+ else {
143
+ result.nodeErrorCount++;
144
+ result.nodeErrors.push({
145
+ file: node.label,
146
+ errors: schemaValidator.errors || [],
147
+ nodeData: node.schemaData,
148
+ });
149
+ }
150
+ }
151
+ // Detect duplicate node keys (titles)
152
+ const titleToFiles = new Map();
153
+ for (const node of nodes) {
154
+ const title = node.title;
155
+ if (!titleToFiles.has(title)) {
156
+ titleToFiles.set(title, []);
157
+ }
158
+ titleToFiles.get(title).push(node.label);
159
+ }
160
+ for (const [title, files] of titleToFiles) {
161
+ if (files.length > 1) {
162
+ result.duplicateErrors.push({ title, files });
163
+ }
164
+ }
165
+ // Validate all hierarchy constraints (field references and structure)
166
+ const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
167
+ result.refErrors.push(...hierarchyValidation.refErrors);
168
+ result.hierarchyViolations = [...hierarchyValidation.violations];
169
+ // Calculate orphan count (informational, not a validation error)
170
+ if (metadata.hierarchy) {
171
+ result.orphans = [...buildSpaceGraph(nodes, metadata.hierarchy.levels).orphans];
172
+ }
173
+ // Load and execute rules validation if schema defines rules
174
+ if (metadata.rules) {
175
+ result.ruleViolations = await validateRules(nodes, metadata.rules);
176
+ }
177
+ return result;
178
+ }
179
+ /** Find which space a file belongs to by checking directory containment. */
180
+ function resolveFileSpace(filePath, config) {
181
+ const absFile = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
182
+ for (const space of config.spaces) {
183
+ const absSpace = isAbsolute(space.path) ? space.path : resolve(process.cwd(), space.path);
184
+ // Trailing slash prevents prefix-match false positives (e.g. /foo matching /foobar/)
185
+ const spaceDir = absSpace.endsWith('/') ? absSpace : `${absSpace}/`;
186
+ if (absFile.startsWith(spaceDir) || absFile === absSpace) {
187
+ return { space, label: relative(absSpace, absFile) };
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ function buildContextForSpace(space, config) {
193
+ const resolvedSchemaPath = resolveSchema(config, space);
194
+ const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
195
+ const configDir = getSpaceConfigDir(space.name);
196
+ return { space, config, resolvedSchemaPath, schema, schemaRefRegistry, schemaValidator, configDir };
197
+ }
198
+ /**
199
+ * Validate a single file within its space.
200
+ *
201
+ * Reads the full space (required for graph correctness) but filters all reported
202
+ * errors to only those attributable to the target file.
203
+ *
204
+ * Returns a result object — does not output to console or call process.exit.
205
+ * If the file is not in any configured space, returns a result with `inSpace: false`.
206
+ */
207
+ export async function validateFile(filePath, config) {
208
+ const resolved = resolveFileSpace(filePath, config);
209
+ if (!resolved) {
210
+ return {
211
+ file: filePath,
212
+ inSpace: false,
213
+ message: 'File does not belong to any configured space.',
214
+ };
215
+ }
216
+ const { space, label } = resolved;
217
+ const context = buildContextForSpace(space, config);
218
+ const { schema, schemaRefRegistry, schemaValidator } = context;
219
+ const metadata = schema.metadata;
220
+ const readResult = await readSpace(context);
221
+ const { nodes } = readResult;
222
+ // Pre-extract valid types for early type validation
223
+ const validTypes = Array.isArray(schema.oneOf) ? extractEntityInfo(schema, schemaRefRegistry).map((e) => e.type) : [];
224
+ const errors = {};
225
+ // Schema validation errors for this node
226
+ for (const node of nodes) {
227
+ if (node.label !== label)
228
+ continue;
229
+ // Early type validation - check before full AJV validation to prevent cascading errors
230
+ const nodeType = node.schemaData.type;
231
+ if (nodeType !== undefined && validTypes.length > 0 && !validTypes.includes(nodeType)) {
232
+ errors[`schema:type:${validTypes.join(',')}`] = {
233
+ kind: 'schema',
234
+ message: `Invalid type "${nodeType}". Valid types are: ${validTypes.sort().join(', ')}`,
235
+ };
236
+ continue;
237
+ }
238
+ const valid = schemaValidator(node.schemaData);
239
+ if (!valid) {
240
+ const formatted = formatErrors(schemaValidator.errors ?? [], schema, schemaRefRegistry, node.schemaData);
241
+ for (const { message, dedupeKey } of formatted) {
242
+ errors[`schema:${dedupeKey}`] = { kind: 'schema', message };
243
+ }
244
+ }
245
+ }
246
+ // Duplicate key errors — include if this file is one of the duplicates
247
+ const titleToFiles = new Map();
248
+ for (const node of nodes) {
249
+ if (!titleToFiles.has(node.title))
250
+ titleToFiles.set(node.title, []);
251
+ titleToFiles.get(node.title).push(node.label);
252
+ }
253
+ for (const [title, files] of titleToFiles) {
254
+ if (files.length > 1 && files.includes(label)) {
255
+ const others = files.filter((f) => f !== label);
256
+ errors[`duplicate:${title}`] = {
257
+ kind: 'duplicate',
258
+ message: `Duplicate title "${title}" also exists in: ${others.join(', ')}`,
259
+ };
260
+ }
261
+ }
262
+ // Broken links and hierarchy violations from this file
263
+ const hierarchyValidation = validateGraph(nodes, metadata, readResult.unresolvedRefs);
264
+ for (const { file, parent, error } of hierarchyValidation.refErrors) {
265
+ if (file === label) {
266
+ errors[`broken-link:${parent}`] = { kind: 'broken-link', message: `${parent} → ${error}` };
267
+ }
268
+ }
269
+ for (const v of hierarchyValidation.violations) {
270
+ if (v.file === label) {
271
+ errors[`hierarchy:${v.description}`] = { kind: 'hierarchy', message: v.description };
272
+ }
273
+ }
274
+ // Rule violations for this node
275
+ if (metadata.rules) {
276
+ const ruleViolations = await validateRules(nodes, metadata.rules);
277
+ for (const v of ruleViolations) {
278
+ if (v.file === label) {
279
+ errors[`rule:${v.ruleId}`] = { kind: 'rule', message: `[${v.ruleId}] ${v.description}` };
280
+ }
281
+ }
282
+ }
283
+ return {
284
+ file: isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath),
285
+ label,
286
+ space: space.name,
287
+ errors,
288
+ errorCount: Object.keys(errors).length,
289
+ inSpace: true,
290
+ };
291
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.9.3",
4
- "description": "Tools for working with Opportunity Solution Tree structures and other product management and strategy frameworks",
3
+ "version": "0.10.0",
4
+ "description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -18,9 +18,13 @@
18
18
  ],
19
19
  "exports": {
20
20
  ".": "./dist/index.js",
21
- "./plugin-api": {
22
- "types": "./dist/plugin-api.d.ts",
23
- "default": "./dist/plugin-api.js"
21
+ "./api": {
22
+ "types": "./dist/api.d.ts",
23
+ "default": "./dist/api.js"
24
+ },
25
+ "./types": {
26
+ "types": "./dist/types.d.ts",
27
+ "default": "./dist/types.js"
24
28
  }
25
29
  },
26
30
  "scripts": {
@@ -57,8 +61,10 @@
57
61
  "ajv": "^8.18.0",
58
62
  "chokidar": "^5.0.0",
59
63
  "commander": "^14.0.3",
64
+ "fast-glob": "^3.3.3",
60
65
  "gray-matter": "^4.0.3",
61
66
  "js-yaml": "^4.1.1",
67
+ "json5": "^2.2.3",
62
68
  "jsonata": "^2.1.0",
63
69
  "mdast-util-to-string": "^4.0.0",
64
70
  "remark-gfm": "^4.0.1",
@@ -1,12 +0,0 @@
1
- /**
2
- * Public API for external structured-context plugins.
3
- *
4
- * Import from this module to get the types needed to implement a StructuredContextPlugin:
5
- *
6
- * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/plugin-api';
7
- */
8
- export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
9
- export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
10
- export type { SharedEmbeddingFields } from './schema/metadata-contract';
11
- export type { SpaceGraph } from './space-graph';
12
- export type { BaseNode, EdgeDefinition, HierarchyLevel, ParseIssue, Relationship, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, } from './types';
@@ -1,7 +0,0 @@
1
- /**
2
- * Public API for external structured-context plugins.
3
- *
4
- * Import from this module to get the types needed to implement a StructuredContextPlugin:
5
- *
6
- * import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/plugin-api';
7
- */