structured-context 0.9.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 +348 -0
- package/dist/commands/diagram.d.ts +5 -0
- package/dist/commands/diagram.js +12 -0
- package/dist/commands/docs.d.ts +1 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/dump.d.ts +2 -0
- package/dist/commands/dump.js +6 -0
- package/dist/commands/plugins.d.ts +1 -0
- package/dist/commands/plugins.js +23 -0
- package/dist/commands/render.d.ts +6 -0
- package/dist/commands/render.js +35 -0
- package/dist/commands/schemas.d.ts +6 -0
- package/dist/commands/schemas.js +268 -0
- package/dist/commands/show.d.ts +4 -0
- package/dist/commands/show.js +7 -0
- package/dist/commands/spaces.d.ts +1 -0
- package/dist/commands/spaces.js +36 -0
- package/dist/commands/template-sync.d.ts +3 -0
- package/dist/commands/template-sync.js +13 -0
- package/dist/commands/validate-file.d.ts +28 -0
- package/dist/commands/validate-file.js +133 -0
- package/dist/commands/validate.d.ts +16 -0
- package/dist/commands/validate.js +349 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +179 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/filter/augment-nodes.d.ts +23 -0
- package/dist/filter/augment-nodes.js +95 -0
- package/dist/filter/expand-include.d.ts +62 -0
- package/dist/filter/expand-include.js +181 -0
- package/dist/filter/filter-nodes.d.ts +21 -0
- package/dist/filter/filter-nodes.js +73 -0
- package/dist/filter/parse-expression.d.ts +20 -0
- package/dist/filter/parse-expression.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +161 -0
- package/dist/integrations/miro/cache.d.ts +21 -0
- package/dist/integrations/miro/cache.js +55 -0
- package/dist/integrations/miro/client.d.ts +99 -0
- package/dist/integrations/miro/client.js +118 -0
- package/dist/integrations/miro/layout.d.ts +28 -0
- package/dist/integrations/miro/layout.js +72 -0
- package/dist/integrations/miro/styles.d.ts +11 -0
- package/dist/integrations/miro/styles.js +65 -0
- package/dist/integrations/miro/sync.d.ts +8 -0
- package/dist/integrations/miro/sync.js +347 -0
- package/dist/plugin-api.d.ts +12 -0
- package/dist/plugin-api.js +7 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.js +3 -0
- package/dist/plugins/loader.d.ts +21 -0
- package/dist/plugins/loader.js +104 -0
- package/dist/plugins/markdown/index.d.ts +48 -0
- package/dist/plugins/markdown/index.js +51 -0
- package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
- package/dist/plugins/markdown/parse-embedded.js +663 -0
- package/dist/plugins/markdown/read-space.d.ts +7 -0
- package/dist/plugins/markdown/read-space.js +89 -0
- package/dist/plugins/markdown/render-bullets.d.ts +2 -0
- package/dist/plugins/markdown/render-bullets.js +42 -0
- package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
- package/dist/plugins/markdown/render-mermaid.js +57 -0
- package/dist/plugins/markdown/template-sync.d.ts +16 -0
- package/dist/plugins/markdown/template-sync.js +294 -0
- package/dist/plugins/markdown/util.d.ts +19 -0
- package/dist/plugins/markdown/util.js +80 -0
- package/dist/plugins/util.d.ts +60 -0
- package/dist/plugins/util.js +7 -0
- package/dist/read/read-space.d.ts +2 -0
- package/dist/read/read-space.js +22 -0
- package/dist/read/resolve-graph-edges.d.ts +11 -0
- package/dist/read/resolve-graph-edges.js +201 -0
- package/dist/read/wikilink-utils.d.ts +16 -0
- package/dist/read/wikilink-utils.js +38 -0
- package/dist/render/registry.d.ts +13 -0
- package/dist/render/registry.js +22 -0
- package/dist/render/render.d.ts +4 -0
- package/dist/render/render.js +28 -0
- package/dist/schema/evaluate-rule.d.ts +30 -0
- package/dist/schema/evaluate-rule.js +82 -0
- package/dist/schema/metadata-contract.d.ts +538 -0
- package/dist/schema/metadata-contract.js +115 -0
- package/dist/schema/schema-refs.d.ts +22 -0
- package/dist/schema/schema-refs.js +168 -0
- package/dist/schema/schema.d.ts +27 -0
- package/dist/schema/schema.js +378 -0
- package/dist/schema/validate-graph.d.ts +24 -0
- package/dist/schema/validate-graph.js +141 -0
- package/dist/schema/validate-rules.d.ts +10 -0
- package/dist/schema/validate-rules.js +51 -0
- package/dist/schemas/_ost_strict.json +81 -0
- package/dist/schemas/_sctx_base.json +72 -0
- package/dist/schemas/general.json +261 -0
- package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/dist/schemas/knowledge_wiki.json +206 -0
- package/dist/schemas/strict_ost.json +97 -0
- package/dist/space-graph.d.ts +28 -0
- package/dist/space-graph.js +82 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +0 -0
- package/docs/concepts.md +391 -0
- package/docs/config.md +140 -0
- package/docs/rules.md +120 -0
- package/docs/schemas.md +340 -0
- package/package.json +69 -0
- package/schemas/_ost_strict.json +81 -0
- package/schemas/_sctx_base.json +72 -0
- package/schemas/general.json +261 -0
- package/schemas/generated/_structured_context_schema_meta.json +191 -0
- package/schemas/knowledge_wiki.json +206 -0
- package/schemas/strict_ost.json +97 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { loadConfig, resolveSchema } from '../config';
|
|
4
|
+
import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema } from '../schema/schema';
|
|
5
|
+
import { mergeVariantProperties } from '../schema/schema-refs';
|
|
6
|
+
function isBundledPath(schemaPath) {
|
|
7
|
+
return dirname(schemaPath) === bundledSchemasDir;
|
|
8
|
+
}
|
|
9
|
+
function extractRefs(obj, refs) {
|
|
10
|
+
if (typeof obj !== 'object' || obj === null)
|
|
11
|
+
return;
|
|
12
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
13
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
14
|
+
refs.add(value);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
extractRefs(value, refs);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function extractEntities(oneOf, schemaRefRegistry, schema) {
|
|
22
|
+
return oneOf.map((entry) => {
|
|
23
|
+
const entryObj = entry;
|
|
24
|
+
const { properties, required } = mergeVariantProperties(entryObj, schema, schemaRefRegistry);
|
|
25
|
+
const typeDef = properties.type;
|
|
26
|
+
let types = [];
|
|
27
|
+
if (typeDef?.const)
|
|
28
|
+
types = [String(typeDef.const)];
|
|
29
|
+
else if (Array.isArray(typeDef?.enum))
|
|
30
|
+
types = typeDef.enum.map(String);
|
|
31
|
+
return {
|
|
32
|
+
types,
|
|
33
|
+
description: typeof entryObj.description === 'string' ? entryObj.description : undefined,
|
|
34
|
+
properties: Object.keys(properties).filter((k) => k !== 'type'),
|
|
35
|
+
required: required.filter((r) => r !== 'type'),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function showEntities(oneOf, schemaRefRegistry, schema) {
|
|
40
|
+
const entities = extractEntities(oneOf, schemaRefRegistry, schema);
|
|
41
|
+
console.log('\nEntities:');
|
|
42
|
+
for (const { types, description, properties, required } of entities) {
|
|
43
|
+
const label = types.length > 0 ? types.join(', ') : '(unknown)';
|
|
44
|
+
if (properties.length === 0) {
|
|
45
|
+
console.log(` ${label}${description ? ` — ${description}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const propList = properties.map((p) => (required.includes(p) ? `${p}*` : p)).join(' ');
|
|
49
|
+
console.log(` ${label}${description ? ` — ${description}` : ''}`);
|
|
50
|
+
console.log(` ${propList}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function showDefs(defs) {
|
|
55
|
+
const keys = Object.keys(defs).filter((k) => !k.startsWith('_'));
|
|
56
|
+
if (keys.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
console.log('\nDefinitions:');
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
const def = defs[key];
|
|
61
|
+
const props = def.properties ? Object.keys(def.properties) : [];
|
|
62
|
+
const desc = def.description ? ` — ${def.description}` : '';
|
|
63
|
+
if (props.length > 0) {
|
|
64
|
+
console.log(` ${key}${desc}`);
|
|
65
|
+
console.log(` properties: ${props.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const typeInfo = def.type ? ` (${def.type})` : '';
|
|
69
|
+
const enumInfo = Array.isArray(def.enum) ? ` [${def.enum.join(', ')}]` : '';
|
|
70
|
+
console.log(` ${key}${typeInfo}${enumInfo}${desc}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function showMetadata(metadata) {
|
|
75
|
+
if (metadata.hierarchy?.levels.length) {
|
|
76
|
+
const parts = metadata.hierarchy.levels.map((l) => (l.selfRef ? `${l.type}(+)` : l.type));
|
|
77
|
+
console.log(`\nhierarchy: ${parts.join(' → ')}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log('\nhierarchy: (none)');
|
|
81
|
+
}
|
|
82
|
+
if (metadata.typeAliases && Object.keys(metadata.typeAliases).length > 0) {
|
|
83
|
+
const aliasParts = Object.entries(metadata.typeAliases).map(([k, v]) => `${k} → ${v}`);
|
|
84
|
+
console.log(`type aliases: ${aliasParts.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
if (metadata.rules) {
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
for (const rule of metadata.rules) {
|
|
89
|
+
const rules = groups.get(rule.category) ?? [];
|
|
90
|
+
rules.push({ id: rule.id, description: rule.description });
|
|
91
|
+
groups.set(rule.category, rules);
|
|
92
|
+
}
|
|
93
|
+
if (groups.size > 0) {
|
|
94
|
+
console.log('\nRules:');
|
|
95
|
+
for (const [group, items] of groups) {
|
|
96
|
+
console.log(` ${group}:`);
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
console.log(` ${item.id}: ${item.description}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (metadata.relationships && metadata.relationships.length > 0) {
|
|
104
|
+
console.log('\nRelationships:');
|
|
105
|
+
for (const rel of metadata.relationships) {
|
|
106
|
+
const fields = rel.embeddedTemplateFields?.length ? ` [fields: ${rel.embeddedTemplateFields.join(', ')}]` : '';
|
|
107
|
+
const matchers = rel.matchers?.length ? ` (matches: ${rel.matchers.join(', ')})` : '';
|
|
108
|
+
console.log(` ${rel.parent} → ${rel.type} [${rel.templateFormat ?? 'page'}]${fields}${matchers}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function showRegistry(schemaPath, schemaRefRegistry) {
|
|
113
|
+
const bundledIds = new Set();
|
|
114
|
+
if (existsSync(bundledSchemasDir)) {
|
|
115
|
+
for (const file of readdirSync(bundledSchemasDir).filter((f) => f.endsWith('.json'))) {
|
|
116
|
+
const s = readRawSchema(join(bundledSchemasDir, file));
|
|
117
|
+
if (typeof s.$id === 'string')
|
|
118
|
+
bundledIds.add(s.$id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log(`\nRegistry (${schemaPath}):`);
|
|
122
|
+
for (const [id] of schemaRefRegistry) {
|
|
123
|
+
console.log(` [${bundledIds.has(id) ? 'bundled' : 'local'}] ${id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate Mermaid ERD from schema metadata.
|
|
128
|
+
* Shows entity types, properties, and parent-child relationships.
|
|
129
|
+
*/
|
|
130
|
+
function generateMermaidErd(metadata, entities) {
|
|
131
|
+
let mmd = 'erDiagram\n';
|
|
132
|
+
const hierarchyLevels = metadata.hierarchy?.levels ?? [];
|
|
133
|
+
// Generate entity definitions with properties
|
|
134
|
+
for (const entity of entities) {
|
|
135
|
+
const safeName = entity.type.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
136
|
+
mmd += ` ${safeName} {\n`;
|
|
137
|
+
mmd += ` string title PK\n`;
|
|
138
|
+
for (const prop of entity.properties) {
|
|
139
|
+
if (prop === 'title')
|
|
140
|
+
continue; // Skip title since it's already declared as PK
|
|
141
|
+
const isRequired = entity.required.includes(prop);
|
|
142
|
+
const optional = isRequired ? '' : ' "optional"';
|
|
143
|
+
mmd += ` string ${prop}${optional}\n`;
|
|
144
|
+
}
|
|
145
|
+
mmd += ' }\n';
|
|
146
|
+
}
|
|
147
|
+
// Generate relationships based on hierarchy metadata
|
|
148
|
+
if (hierarchyLevels.length > 0) {
|
|
149
|
+
for (let i = 0; i < hierarchyLevels.length - 1; i++) {
|
|
150
|
+
const current = hierarchyLevels[i];
|
|
151
|
+
const next = hierarchyLevels[i + 1];
|
|
152
|
+
if (!current || !next)
|
|
153
|
+
continue;
|
|
154
|
+
const currentSafe = current.type.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
155
|
+
const nextSafe = next.type.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
156
|
+
// Determine cardinality based on selfRef
|
|
157
|
+
// If next.selfRef is true, the child type can have parents of the same type
|
|
158
|
+
if (next.selfRef) {
|
|
159
|
+
mmd += ` ${currentSafe} ||--|{ ${nextSafe} : "parent"\n`;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
mmd += ` ${currentSafe} ||--o{ ${nextSafe} : "parent"\n`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return mmd;
|
|
167
|
+
}
|
|
168
|
+
export function listSchemas() {
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
// List all schemas known to config
|
|
171
|
+
if (existsSync(bundledSchemasDir)) {
|
|
172
|
+
const files = readdirSync(bundledSchemasDir)
|
|
173
|
+
.filter((f) => f.endsWith('.json'))
|
|
174
|
+
.sort();
|
|
175
|
+
if (files.length > 0) {
|
|
176
|
+
console.log('Bundled schemas:');
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
const schema = readRawSchema(join(bundledSchemasDir, file));
|
|
179
|
+
const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
|
|
180
|
+
console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const configured = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
if (config.schema && !isBundledPath(config.schema)) {
|
|
187
|
+
configured.push({ source: 'global', path: config.schema });
|
|
188
|
+
seen.add(config.schema);
|
|
189
|
+
}
|
|
190
|
+
for (const space of config.spaces) {
|
|
191
|
+
if (space.schema && !seen.has(space.schema) && !isBundledPath(space.schema)) {
|
|
192
|
+
configured.push({ source: space.name, path: space.schema });
|
|
193
|
+
seen.add(space.schema);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (configured.length > 0) {
|
|
197
|
+
console.log('\nConfigured schemas:');
|
|
198
|
+
for (const { source, path } of configured) {
|
|
199
|
+
console.log(` ${source}: ${path}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export function showSchema(file, options) {
|
|
204
|
+
const config = loadConfig();
|
|
205
|
+
let schemaPath;
|
|
206
|
+
if (options.space) {
|
|
207
|
+
const space = config.spaces.find((s) => s.name === options.space);
|
|
208
|
+
if (!space) {
|
|
209
|
+
console.error(`Error: Unknown space "${options.space}"`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
schemaPath = resolveSchema(config, space);
|
|
213
|
+
}
|
|
214
|
+
else if (!file) {
|
|
215
|
+
console.error('Error: specify a file argument or use --space');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
else if (file.startsWith('/') || file.startsWith('./')) {
|
|
219
|
+
schemaPath = file;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
schemaPath = join(bundledSchemasDir, file.endsWith('.json') ? file : `${file}.json`);
|
|
223
|
+
}
|
|
224
|
+
if (!existsSync(schemaPath)) {
|
|
225
|
+
console.error(`Schema not found: ${schemaPath}`);
|
|
226
|
+
console.error('\nHint: Use ./ or / prefix for local paths (e.g. ./schemas/my_schema.json),');
|
|
227
|
+
console.error(" or use --space to show a space's configured schema.");
|
|
228
|
+
const matchingSpace = config.spaces.find((s) => s.schema?.includes(file || ''));
|
|
229
|
+
if (matchingSpace) {
|
|
230
|
+
console.error(` Did you mean: --space ${matchingSpace.name}?`);
|
|
231
|
+
}
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const content = readFileSync(schemaPath, 'utf-8');
|
|
235
|
+
if (options.raw) {
|
|
236
|
+
process.stdout.write(content);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { schema, schemaRefRegistry } = loadSchema(schemaPath);
|
|
240
|
+
// Handle --mermaid-erd: generate ERD and exit
|
|
241
|
+
if (options.mermaidErd) {
|
|
242
|
+
const entityInfo = extractEntityInfo(schema, schemaRefRegistry);
|
|
243
|
+
const mermaid = generateMermaidErd(schema.metadata, entityInfo);
|
|
244
|
+
process.stdout.write(mermaid);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
console.log(`$id: ${schema.$id ?? '(none)'}`);
|
|
248
|
+
if (schema.title)
|
|
249
|
+
console.log(`title: ${schema.title}`);
|
|
250
|
+
if (schema.description)
|
|
251
|
+
console.log(`description: ${schema.description}`);
|
|
252
|
+
showMetadata(schema.metadata);
|
|
253
|
+
if (Array.isArray(schema.oneOf)) {
|
|
254
|
+
showEntities(schema.oneOf, schemaRefRegistry, schema);
|
|
255
|
+
}
|
|
256
|
+
const defs = schema.$defs;
|
|
257
|
+
if (defs)
|
|
258
|
+
showDefs(defs);
|
|
259
|
+
const refs = new Set();
|
|
260
|
+
extractRefs(schema, refs);
|
|
261
|
+
if (refs.size > 0) {
|
|
262
|
+
console.log('\n$refs:');
|
|
263
|
+
for (const ref of [...refs].sort()) {
|
|
264
|
+
console.log(` ${ref}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
showRegistry(schemaPath, schemaRefRegistry);
|
|
268
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { executeRender } from '../render/render';
|
|
2
|
+
export async function show(context, options) {
|
|
3
|
+
const result = await executeRender('markdown.bullets', context, { filter: options?.filter });
|
|
4
|
+
process.stdout.write(result);
|
|
5
|
+
if (!result.endsWith('\n'))
|
|
6
|
+
process.stdout.write('\n');
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listSpaces(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path';
|
|
2
|
+
import { configPath, loadConfig, resolveSchema } from '../config';
|
|
3
|
+
function renderConfigValue(v) {
|
|
4
|
+
if (typeof v === 'object' && v !== null)
|
|
5
|
+
return JSON.stringify(v);
|
|
6
|
+
return String(v);
|
|
7
|
+
}
|
|
8
|
+
export function listSpaces() {
|
|
9
|
+
const path = resolve(configPath());
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
console.log(`Config: ${path}\n`);
|
|
12
|
+
for (const space of config.spaces) {
|
|
13
|
+
console.log(`${space.name}`);
|
|
14
|
+
console.log(` path: ${space.path}`);
|
|
15
|
+
console.log(` schema: ${basename(resolveSchema(config, space))}`);
|
|
16
|
+
if (space.miroBoardId)
|
|
17
|
+
console.log(` miro: configured`);
|
|
18
|
+
const plugins = space.plugins ?? {};
|
|
19
|
+
if (Object.keys(plugins).length > 0) {
|
|
20
|
+
console.log(' plugins:');
|
|
21
|
+
for (const [name, cfg] of Object.entries(plugins)) {
|
|
22
|
+
const entries = Object.entries(cfg);
|
|
23
|
+
if (entries.length === 0) {
|
|
24
|
+
console.log(` ${name}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(` ${name}:`);
|
|
28
|
+
for (const [k, v] of entries) {
|
|
29
|
+
console.log(` ${k}: ${renderConfigValue(v)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadPlugins } from '../plugins/loader';
|
|
2
|
+
export async function templateSync(context, options) {
|
|
3
|
+
const pluginMap = context.space?.plugins ?? {};
|
|
4
|
+
const loaded = await loadPlugins(pluginMap, context.configDir);
|
|
5
|
+
for (const { plugin, pluginConfig } of loaded) {
|
|
6
|
+
if (!plugin.templateSync)
|
|
7
|
+
continue;
|
|
8
|
+
const result = await plugin.templateSync({ ...context, pluginConfig }, options);
|
|
9
|
+
if (result !== null)
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
throw new Error('No plugin supports template-sync for this space');
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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;
|
|
19
|
+
/**
|
|
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).
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateFile(filePath: string, options?: {
|
|
27
|
+
json?: boolean;
|
|
28
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
}
|
|
27
|
+
/**
|
|
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).
|
|
33
|
+
*/
|
|
34
|
+
export async function validateFile(filePath, options = {}) {
|
|
35
|
+
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
|
+
};
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify(result, null, 2));
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
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
|
+
if (options.json) {
|
|
113
|
+
console.log(JSON.stringify(result, null, 2));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
printHumanReadable(result);
|
|
117
|
+
}
|
|
118
|
+
return Object.keys(errors).length > 0 ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
function printHumanReadable(result) {
|
|
121
|
+
const reset = '\x1b[0m';
|
|
122
|
+
const green = '\x1b[32m';
|
|
123
|
+
const red = '\x1b[31m';
|
|
124
|
+
if (result.errorCount === 0) {
|
|
125
|
+
console.log(`${green}✓${reset} ${result.label} (space: ${result.space})`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.log(`\n${red}✗${reset} ${result.label} (space: ${result.space}) — ${result.errorCount} error(s)\n`);
|
|
129
|
+
for (const { kind, message } of Object.values(result.errors)) {
|
|
130
|
+
console.log(` [${kind}] ${message}`);
|
|
131
|
+
}
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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[];
|
|
13
|
+
export declare function validate(context: SpaceContext, options?: {
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}): Promise<number>;
|
|
16
|
+
export declare function watchValidate(context: SpaceContext): Promise<never>;
|