structured-context 0.15.0 → 0.16.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 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +2 -0
- package/dist/commands/schemas.d.ts +3 -1
- package/dist/commands/schemas.js +22 -51
- package/dist/index.js +3 -2
- package/dist/schema/list-schemas.d.ts +27 -0
- package/dist/schema/list-schemas.js +108 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,6 +188,14 @@ Generates a Mermaid `graph TD` diagram from validated space nodes:
|
|
|
188
188
|
- Handles orphan nodes (no parent) as a separate cluster
|
|
189
189
|
- Outputs to file or stdout
|
|
190
190
|
|
|
191
|
+
### List schemas
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
sctx schemas list [--json]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Lists the bundled schemas, schemas contributed by plugins, and any other schema file your config uses, with the spaces that use each. `--json` gives the data that `listSchemas` returns; see [docs/api.md](docs/api.md).
|
|
198
|
+
|
|
191
199
|
### Show schema ERD
|
|
192
200
|
|
|
193
201
|
```bash
|
package/dist/api.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type { ParseHook, ParseResult, PluginContext, RenderFileOutput, RenderFor
|
|
|
27
27
|
export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
|
|
28
28
|
export { readSpace } from './read/read-space';
|
|
29
29
|
export { buildTargetIndex, formatWikilink, type ParsedWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
|
|
30
|
+
export { listSchemas } from './schema/list-schemas';
|
|
30
31
|
export type { SharedEmbeddingFields } from './schema/metadata-contract';
|
|
31
32
|
export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema';
|
|
32
33
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Public entry point: do not add exports without the maintainer's agreement (AGENTS.md, "Public API surface").
|
|
1
2
|
/**
|
|
2
3
|
* Public API for structured-context.
|
|
3
4
|
*
|
|
@@ -24,6 +25,7 @@ export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
|
|
|
24
25
|
export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
|
|
25
26
|
export { readSpace } from './read/read-space';
|
|
26
27
|
export { buildTargetIndex, formatWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
|
|
28
|
+
export { listSchemas } from './schema/list-schemas';
|
|
27
29
|
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
28
30
|
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
29
31
|
export { buildSpaceGraph } from './space-graph';
|
package/dist/commands/schemas.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { loadConfig } from '../config';
|
|
4
4
|
import { discoverPlugins } from '../plugins/loader';
|
|
5
5
|
import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
|
+
import { listSchemas } from '../schema/list-schemas';
|
|
6
7
|
import { ruleSemantic } from '../schema/rule-severity';
|
|
7
8
|
import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema, } from '../schema/schema';
|
|
8
9
|
import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
|
|
9
10
|
import { DEFAULT_SEVERITY_BY_CATEGORY } from '../schema/validate-rules';
|
|
10
11
|
import { createSpaceContext } from '../space-context';
|
|
11
|
-
function isBundledPath(schemaPath) {
|
|
12
|
-
return dirname(schemaPath) === bundledSchemasDir;
|
|
13
|
-
}
|
|
14
12
|
function extractRefs(obj, refs) {
|
|
15
13
|
if (typeof obj !== 'object' || obj === null)
|
|
16
14
|
return;
|
|
@@ -207,57 +205,30 @@ function ruleAnnotations(rule) {
|
|
|
207
205
|
notes.push(`when: ${rule.when}`);
|
|
208
206
|
return notes.length > 0 ? ` [${notes.join('] [')}]` : '';
|
|
209
207
|
}
|
|
210
|
-
export async function
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
.filter((f) => f.endsWith('.json'))
|
|
216
|
-
.sort();
|
|
217
|
-
if (files.length > 0) {
|
|
218
|
-
console.log('Bundled schemas:');
|
|
219
|
-
for (const file of files) {
|
|
220
|
-
const schema = readRawSchema(join(bundledSchemasDir, file));
|
|
221
|
-
const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
|
|
222
|
-
console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
208
|
+
export async function printSchemas(options = {}) {
|
|
209
|
+
const listings = await listSchemas();
|
|
210
|
+
if (options.json) {
|
|
211
|
+
console.log(JSON.stringify(listings, null, 2));
|
|
212
|
+
return;
|
|
225
213
|
}
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
214
|
+
const sources = [...new Set(listings.map((l) => l.source))].sort((a, b) => Number(b === 'core') - Number(a === 'core') || Number(a === 'configured') - Number(b === 'configured'));
|
|
215
|
+
for (const source of sources) {
|
|
216
|
+
const group = listings.filter((l) => l.source === source);
|
|
217
|
+
if (source === 'core')
|
|
218
|
+
console.log('Bundled schemas:');
|
|
219
|
+
else if (source === 'configured')
|
|
220
|
+
console.log('\nConfigured schemas:');
|
|
221
|
+
else
|
|
222
|
+
console.log(`\nPlugin schemas (${source}):`);
|
|
223
|
+
for (const l of group) {
|
|
224
|
+
if (source === 'configured')
|
|
225
|
+
console.log(` ${l.usedBy.join(', ')}: ${l.path ?? l.schema}${l.error ? ` [${l.error}]` : ''}`);
|
|
226
|
+
else {
|
|
227
|
+
const notes = [l.partial && '[partial]', l.id && `(${l.id})`, l.error && `[${l.error}]`].filter(Boolean);
|
|
228
|
+
console.log(` ${l.schema}${notes.length > 0 ? ` ${notes.join(' ')}` : ''}`);
|
|
240
229
|
}
|
|
241
230
|
}
|
|
242
231
|
}
|
|
243
|
-
const configured = [];
|
|
244
|
-
const seen = new Set();
|
|
245
|
-
if (config.schema && !isBundledPath(config.schema)) {
|
|
246
|
-
configured.push({ source: 'global', path: config.schema });
|
|
247
|
-
seen.add(config.schema);
|
|
248
|
-
}
|
|
249
|
-
for (const space of config.spaces) {
|
|
250
|
-
if (space.schema && !seen.has(space.schema) && !isBundledPath(space.schema)) {
|
|
251
|
-
configured.push({ source: space.name, path: space.schema });
|
|
252
|
-
seen.add(space.schema);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (configured.length > 0) {
|
|
256
|
-
console.log('\nConfigured schemas:');
|
|
257
|
-
for (const { source, path } of configured) {
|
|
258
|
-
console.log(` ${source}: ${path}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
232
|
}
|
|
262
233
|
export async function showSchema(file, options) {
|
|
263
234
|
const config = loadConfig();
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dump } from './commands/dump';
|
|
|
8
8
|
import { miroSyncCommand } from './commands/miro-sync';
|
|
9
9
|
import { listPlugins } from './commands/plugins';
|
|
10
10
|
import { render, renderList } from './commands/render';
|
|
11
|
-
import {
|
|
11
|
+
import { printSchemas, showSchema } from './commands/schemas';
|
|
12
12
|
import { show } from './commands/show';
|
|
13
13
|
import { addSpaceCommand, listSpaces } from './commands/spaces';
|
|
14
14
|
import { templateSync } from './commands/template-sync';
|
|
@@ -176,7 +176,8 @@ const schemasCmd = new Command('schemas').alias('schema').description('List and
|
|
|
176
176
|
schemasCmd
|
|
177
177
|
.command('list', { isDefault: true })
|
|
178
178
|
.description('List available schemas')
|
|
179
|
-
.
|
|
179
|
+
.option('--json', 'Output as JSON')
|
|
180
|
+
.action(async (options) => await printSchemas({ json: options.json ?? false }));
|
|
180
181
|
schemasCmd
|
|
181
182
|
.command('show')
|
|
182
183
|
.description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Config } from '../config';
|
|
2
|
+
type SchemaListing = {
|
|
3
|
+
/** The value for a config's `schema` field. */
|
|
4
|
+
schema: string;
|
|
5
|
+
/** Absolute location or URL. Omitted for bundled schemas. */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** `core`, a plugin's short name, or `configured`. */
|
|
8
|
+
source: string;
|
|
9
|
+
/** Names of the spaces that use it, and `global` when the top-level `schema` does. */
|
|
10
|
+
usedBy: string[];
|
|
11
|
+
partial: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Why the schema could not be read. */
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
|
|
20
|
+
* schema file the config points at. Each schema appears once, under the place its file lives.
|
|
21
|
+
*/
|
|
22
|
+
export declare function listSchemas(options?: {
|
|
23
|
+
/** Loaded by discovery from `startDir` when omitted. */
|
|
24
|
+
config?: Config;
|
|
25
|
+
startDir?: string;
|
|
26
|
+
}): Promise<SchemaListing[]>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { loadConfig, resolveConfigPath, resolveSchemaPath } from '../config';
|
|
4
|
+
import { PLUGIN_PREFIX } from '../constants';
|
|
5
|
+
import { CONFIG_PLUGINS_DIR, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
|
|
6
|
+
import { bundledSchemasDir, readRawSchema } from './schema';
|
|
7
|
+
function isRemote(ref) {
|
|
8
|
+
return /^https?:\/\//.test(ref);
|
|
9
|
+
}
|
|
10
|
+
function describeSchema(path) {
|
|
11
|
+
try {
|
|
12
|
+
const schema = readRawSchema(path);
|
|
13
|
+
return {
|
|
14
|
+
...(typeof schema.$id === 'string' && { id: schema.$id }),
|
|
15
|
+
...(typeof schema.title === 'string' && { title: schema.title }),
|
|
16
|
+
...(typeof schema.description === 'string' && { description: schema.description }),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function jsonFiles(dir) {
|
|
24
|
+
if (!dir || !existsSync(dir))
|
|
25
|
+
return [];
|
|
26
|
+
return readdirSync(dir)
|
|
27
|
+
.filter((f) => f.endsWith('.json'))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
|
|
32
|
+
* schema file the config points at. Each schema appears once, under the place its file lives.
|
|
33
|
+
*/
|
|
34
|
+
export async function listSchemas(options = {}) {
|
|
35
|
+
const config = options.config ?? loadConfig({ startDir: options.startDir });
|
|
36
|
+
const listings = [];
|
|
37
|
+
const byPath = new Map();
|
|
38
|
+
const add = (listing, location) => {
|
|
39
|
+
listings.push(listing);
|
|
40
|
+
byPath.set(resolve(location), listing);
|
|
41
|
+
};
|
|
42
|
+
for (const file of jsonFiles(bundledSchemasDir)) {
|
|
43
|
+
const location = join(bundledSchemasDir, file);
|
|
44
|
+
add({ schema: file, source: 'core', usedBy: [], partial: file.startsWith('_'), ...describeSchema(location) }, location);
|
|
45
|
+
}
|
|
46
|
+
// Relative schema references are anchored at the folder of the config that holds them
|
|
47
|
+
const fallbackDir = dirname(resolveConfigPath(options.startDir).path);
|
|
48
|
+
const configDirs = [...new Set([fallbackDir, ...config.spaces.map((s) => s.sourceDir ?? fallbackDir)])];
|
|
49
|
+
// Plugins are found the way schema references resolve them, without loading their code: those declared by a
|
|
50
|
+
// space or the config, and any installed beside a config file
|
|
51
|
+
const pluginSchemaDirs = new Map();
|
|
52
|
+
for (const space of [undefined, ...config.spaces]) {
|
|
53
|
+
for (const { pluginName, schemasDir } of resolveSpacePluginSchemas(space, config, space?.sourceDir ?? fallbackDir)) {
|
|
54
|
+
pluginSchemaDirs.set(pluginName, schemasDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const dir of configDirs) {
|
|
58
|
+
const pluginsDir = join(dir, CONFIG_PLUGINS_DIR);
|
|
59
|
+
if (!existsSync(pluginsDir))
|
|
60
|
+
continue;
|
|
61
|
+
for (const entry of readdirSync(pluginsDir).filter((e) => e.startsWith(PLUGIN_PREFIX))) {
|
|
62
|
+
const schemasDir = resolvePluginSchemasDir(entry, dir);
|
|
63
|
+
if (schemasDir && !pluginSchemaDirs.has(shortenPluginName(entry))) {
|
|
64
|
+
pluginSchemaDirs.set(shortenPluginName(entry), schemasDir);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const [source, schemasDir] of pluginSchemaDirs) {
|
|
69
|
+
for (const file of jsonFiles(schemasDir)) {
|
|
70
|
+
const location = join(schemasDir, file);
|
|
71
|
+
add({
|
|
72
|
+
schema: `${source}/${file}`,
|
|
73
|
+
path: location,
|
|
74
|
+
source,
|
|
75
|
+
usedBy: [],
|
|
76
|
+
partial: file.startsWith('_'),
|
|
77
|
+
...describeSchema(location),
|
|
78
|
+
}, location);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const references = [
|
|
82
|
+
...(config.schema ? [{ user: 'global', raw: config.schema, dir: fallbackDir }] : []),
|
|
83
|
+
...config.spaces.flatMap((s) => s.schema ? [{ user: s.name, raw: s.schema, dir: s.sourceDir ?? fallbackDir }] : []),
|
|
84
|
+
];
|
|
85
|
+
for (const { user, raw, dir } of references) {
|
|
86
|
+
const location = isRemote(raw) ? raw : resolveSchemaPath(raw, dir);
|
|
87
|
+
const key = isRemote(raw) ? raw : resolve(location);
|
|
88
|
+
let listing = byPath.get(key);
|
|
89
|
+
if (!listing) {
|
|
90
|
+
listing = {
|
|
91
|
+
schema: raw,
|
|
92
|
+
path: isRemote(raw) || isAbsolute(location) ? location : resolve(location),
|
|
93
|
+
source: 'configured',
|
|
94
|
+
usedBy: [],
|
|
95
|
+
partial: basename(raw).startsWith('_'),
|
|
96
|
+
...(isRemote(raw)
|
|
97
|
+
? { error: 'Remote schemas are not read' }
|
|
98
|
+
: existsSync(location)
|
|
99
|
+
? describeSchema(location)
|
|
100
|
+
: { error: `Schema file not found: ${location}` }),
|
|
101
|
+
};
|
|
102
|
+
add(listing, key);
|
|
103
|
+
}
|
|
104
|
+
if (!listing.usedBy.includes(user))
|
|
105
|
+
listing.usedBy.push(user);
|
|
106
|
+
}
|
|
107
|
+
return listings;
|
|
108
|
+
}
|
package/package.json
CHANGED