structured-context 0.9.2 → 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/README.md +8 -4
- package/dist/api.d.ts +15 -0
- package/dist/api.js +9 -0
- package/dist/commands/dump.js +1 -1
- package/dist/commands/validate-file.d.ts +5 -24
- package/dist/commands/validate-file.js +10 -104
- package/dist/commands/validate.d.ts +1 -12
- package/dist/commands/validate.js +6 -157
- package/dist/config.js +1 -1
- package/dist/index.js +2 -2
- package/dist/plugins/markdown/index.d.ts +42 -32
- package/dist/plugins/markdown/index.js +9 -8
- package/dist/plugins/markdown/parse-embedded.d.ts +1 -1
- package/dist/plugins/markdown/read-space.js +3 -3
- package/dist/plugins/markdown/template-sync.d.ts +1 -1
- package/dist/plugins/markdown/template-sync.js +2 -2
- package/dist/schema/schema.js +45 -8
- package/dist/schemas/_knowledge_wiki.json +246 -0
- package/dist/schemas/_strategy_general.json +262 -0
- package/dist/schemas/knowledge_wiki.json +7 -237
- package/dist/schemas/strategy_general.json +12 -254
- package/dist/types.d.ts +20 -2
- package/dist/validate.d.ts +50 -0
- package/dist/validate.js +291 -0
- package/docs/schemas.md +6 -3
- package/package.json +11 -5
- package/schemas/_knowledge_wiki.json +246 -0
- package/schemas/_strategy_general.json +262 -0
- package/schemas/knowledge_wiki.json +7 -237
- package/schemas/strategy_general.json +12 -254
- package/dist/plugin-api.d.ts +0 -12
- package/dist/plugin-api.js +0 -7
package/README.md
CHANGED
|
@@ -55,12 +55,13 @@ Install globally (recommended):
|
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
bun install -g structured-context
|
|
58
|
+
sctx --help
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
Or use directly via `bunx`:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
|
-
bunx structured-context
|
|
64
|
+
bunx structured-context --help
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Setup for AI Agents
|
|
@@ -179,15 +180,18 @@ Bare wikilink items (`- [[Existing Node]]`) in any embedding section populate a
|
|
|
179
180
|
With `fieldOn: "parent"`, embedded child nodes (parsed from a matching heading's list or table) are appended as wikilinks to the parent's `field` array, rather than receiving a `parent` field. This matches schemas where the content model naturally lists children on the parent (e.g. `activity.tasks: ["[[Task A]]"]`).
|
|
180
181
|
|
|
181
182
|
Metadata is composable across `$ref` graphs:
|
|
182
|
-
-
|
|
183
|
+
- `hierarchy`: multiple schema files may define a hierarchy; **last one wins** (root schema overrides partials)
|
|
183
184
|
- `aliases` are shallow-merged (later wins)
|
|
184
185
|
- `rules` merge by `id`; conflicts error unless the later rule sets `override: true`
|
|
185
186
|
- `$metadata.rules` supports `$ref` imports for reusable rule packs
|
|
187
|
+
- `relationships` are collected from all files
|
|
186
188
|
|
|
187
|
-
If no
|
|
189
|
+
If no file defines `hierarchy`, hierarchy-specific checks are skipped.
|
|
188
190
|
|
|
189
191
|
**Customizing Schemas:**
|
|
190
|
-
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`) are loaded and used to resolve references (using `$ref`).
|
|
192
|
+
- **Partial schemas**: Files starting with an underscore (like `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`) are loaded and used to resolve references (using `$ref`).
|
|
193
|
+
- **Partials as entity libraries**: Partials can define reusable entity types in `$defs` that composing schemas reference via `$ref`. Bundled partials like `_strategy_general` and `_knowledge_wiki` provide common entity sets for strategy and wiki content.
|
|
194
|
+
- **Partials can carry metadata**: Unlike plain JSON Schema, partials may include `$metadata` (hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
|
|
191
195
|
- **No-metadata partials**: If a partial has no `$metadata`, prefer `$schema: "http://json-schema.org/draft-07/schema#"` so it validates standalone as plain JSON Schema.
|
|
192
196
|
- **Loading priority**: Partial schemas are loaded from both the default schema directory and the directory of your specified target schema.
|
|
193
197
|
- **Transitive resolution**: `$ref` chains are resolved recursively across files/schemas (including nested `allOf` usage in partials).
|
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';
|
package/dist/commands/dump.js
CHANGED
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
export
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
7
|
+
export declare function validateFileCommand(filePath: string, options?: {
|
|
27
8
|
json?: boolean;
|
|
28
9
|
}): Promise<number>;
|
|
@@ -1,121 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
9
|
+
export async function validateFileCommand(filePath, options = {}) {
|
|
35
10
|
const config = loadConfig();
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
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 {
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
|
98
|
-
const
|
|
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
|
-
//
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
14
|
-
properties: {
|
|
15
|
-
templateDir: {
|
|
16
|
-
type: string;
|
|
17
|
-
format:
|
|
20
|
+
readonly type: "object";
|
|
21
|
+
readonly properties: {
|
|
22
|
+
readonly templateDir: {
|
|
23
|
+
readonly type: "string";
|
|
24
|
+
readonly format: "path";
|
|
18
25
|
};
|
|
19
|
-
fieldMap: {
|
|
20
|
-
type:
|
|
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:
|
|
30
|
-
properties: {
|
|
31
|
-
mode: {
|
|
32
|
-
type: string;
|
|
33
|
-
enum:
|
|
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:
|
|
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:
|
|
49
|
+
readonly additionalProperties: false;
|
|
43
50
|
};
|
|
44
51
|
};
|
|
45
|
-
additionalProperties:
|
|
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 '../../
|
|
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 = {
|