structured-context 0.10.0 → 0.11.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 +22 -3
- package/dist/api.d.ts +21 -3
- package/dist/api.js +19 -2
- package/dist/commands/diagram.js +1 -1
- package/dist/commands/miro-sync.d.ts +3 -0
- package/dist/commands/miro-sync.js +8 -0
- package/dist/commands/schemas.js +3 -2
- package/dist/commands/spaces.js +0 -2
- package/dist/commands/validate-file.js +17 -5
- package/dist/commands/validate.js +16 -0
- package/dist/config.d.ts +8 -12
- package/dist/config.js +24 -22
- package/dist/index.js +13 -21
- package/dist/load-space-graph.d.ts +40 -0
- package/dist/load-space-graph.js +42 -0
- package/dist/plugins/index.js +3 -1
- package/dist/plugins/loader.js +2 -2
- package/dist/plugins/markdown/extract-content-links.d.ts +23 -0
- package/dist/plugins/markdown/extract-content-links.js +144 -0
- package/dist/plugins/markdown/index.js +1 -7
- package/dist/plugins/markdown/parse-embedded.js +9 -0
- package/dist/plugins/markdown/read-space.js +3 -0
- package/dist/plugins/mermaid/index.d.ts +2 -0
- package/dist/plugins/mermaid/index.js +14 -0
- package/dist/plugins/{markdown → mermaid}/render-mermaid.js +17 -16
- package/dist/{integrations → plugins}/miro/cache.d.ts +1 -1
- package/dist/plugins/miro/index.d.ts +2 -0
- package/dist/plugins/miro/index.js +27 -0
- package/dist/{integrations → plugins}/miro/layout.d.ts +2 -2
- package/dist/{integrations → plugins}/miro/layout.js +3 -16
- package/dist/{integrations → plugins}/miro/styles.d.ts +2 -2
- package/dist/plugins/miro/sync.d.ts +7 -0
- package/dist/{integrations → plugins}/miro/sync.js +23 -41
- package/dist/plugins/util.d.ts +9 -0
- package/dist/plugins/util.js +4 -0
- package/dist/read/resolve-graph-edges.js +32 -0
- package/dist/render/render.d.ts +1 -0
- package/dist/render/render.js +20 -16
- package/dist/schema/evaluate-rule.js +1 -0
- package/dist/schema/schema.d.ts +3 -1
- package/dist/schema/schema.js +6 -2
- package/dist/schemas/_knowledge_wiki.json +1 -1
- package/dist/space-context.d.ts +24 -0
- package/dist/space-context.js +41 -0
- package/dist/types.d.ts +52 -0
- package/dist/validate.d.ts +12 -3
- package/dist/validate.js +37 -12
- package/docs/concepts.md +23 -0
- package/docs/config.md +2 -0
- package/package.json +1 -1
- package/schemas/_knowledge_wiki.json +1 -1
- package/dist/integrations/miro/sync.d.ts +0 -8
- /package/dist/plugins/{markdown → mermaid}/render-mermaid.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/cache.js +0 -0
- /package/dist/{integrations → plugins}/miro/client.d.ts +0 -0
- /package/dist/{integrations → plugins}/miro/client.js +0 -0
- /package/dist/{integrations → plugins}/miro/styles.js +0 -0
package/README.md
CHANGED
|
@@ -335,13 +335,26 @@ sctx schemas show strategy_general --mermaid-erd
|
|
|
335
335
|
sctx miro-sync <space> [--new-frame <title>] [--dry-run] [--verbose]
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
Syncs space nodes to a Miro board as cards with connectors. Requires `MIRO_TOKEN` env var and `
|
|
338
|
+
Syncs space nodes to a Miro board as cards with connectors. Requires `MIRO_TOKEN` env var and `boardId` set in the `miro` plugin config for the space.
|
|
339
339
|
|
|
340
|
-
- `--new-frame <title>` — create a new frame on the board and sync into it; auto-saves the resulting `
|
|
340
|
+
- `--new-frame <title>` — create a new frame on the board and sync into it; auto-saves the resulting `frameId` back to the miro plugin config
|
|
341
341
|
- `--dry-run` — show what would change without touching Miro
|
|
342
342
|
- `--verbose` / `-v` — detailed per-card and per-connector output
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
Configure the miro plugin in the space's config entry:
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"plugins": {
|
|
349
|
+
"miro": {
|
|
350
|
+
"boardId": "your-board-id",
|
|
351
|
+
"frameId": "your-frame-id"
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
On subsequent runs, the cached `frameId` is used automatically. Cards are colour-coded by node type and linked by parent→child connectors. A local `.miro-cache/` directory tracks Miro IDs to enable incremental updates.
|
|
345
358
|
|
|
346
359
|
Sync is one-way (OST → Miro) and scoped to a single frame. Only cards and connectors created by this tool within that frame are managed — everything else on the board is left untouched. Card content and connectors are overwritten or recreated to match the markdown source; any edits made directly in Miro to managed cards will be lost on the next sync. Existing card positions are not changed.
|
|
347
360
|
|
|
@@ -357,6 +370,12 @@ Keeps Obsidian template files in sync with schema examples:
|
|
|
357
370
|
- `templatePrefix` in `plugins.markdown` config (default blank) sets a naming convention for templates (`{templatePrefix}{type}.md`). This will be used to check existing filenames, and create new templates with `--create-missing`.
|
|
358
371
|
- `--dry-run` previews changes without writing files
|
|
359
372
|
|
|
373
|
+
## Programmatic API
|
|
374
|
+
|
|
375
|
+
The package exposes a library entry point at `structured-context/api` for embedding the tooling
|
|
376
|
+
in other applications. See
|
|
377
|
+
[docs/api.md](docs/api.md) for the full reference and usage examples.
|
|
378
|
+
|
|
360
379
|
## Development
|
|
361
380
|
|
|
362
381
|
```bash
|
package/dist/api.d.ts
CHANGED
|
@@ -3,13 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Import from this module to use structured-context as a library:
|
|
5
5
|
*
|
|
6
|
-
* import { validateFile } from 'structured-context/api';
|
|
6
|
+
* import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from 'structured-context/api';
|
|
7
7
|
* import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
|
|
8
|
+
*
|
|
9
|
+
* Common patterns:
|
|
10
|
+
*
|
|
11
|
+
* const config = loadConfig();
|
|
12
|
+
* const graph = await loadSpaceGraph('my-space', config, { filter: 'my-view' });
|
|
13
|
+
*
|
|
14
|
+
* // Read once, then assemble a graph and validate from the same read and context:
|
|
15
|
+
* const ctx = createSpaceContext('my-space', config);
|
|
16
|
+
* const readResult = await readSpace(ctx);
|
|
17
|
+
* const graph = await assembleSpaceGraph(ctx, { readResult });
|
|
18
|
+
* const validation = await validateSpace(ctx, { readResult });
|
|
8
19
|
*/
|
|
9
20
|
export type { AnySchemaObject, SchemaObject, ValidateFunction } from 'ajv';
|
|
10
21
|
export type { Config, SpaceConfig } from './config';
|
|
22
|
+
export { loadConfig, setConfigPath } from './config';
|
|
23
|
+
export { PLUGIN_PREFIX } from './constants';
|
|
24
|
+
export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
|
|
11
25
|
export type { ParseHook, ParseResult, PluginContext, RenderFormat, RenderHook, RenderOptions, StructuredContextPlugin, TemplateSyncHook, TemplateSyncOptions, } from './plugins/util';
|
|
26
|
+
export { readSpace } from './read/read-space';
|
|
12
27
|
export type { SharedEmbeddingFields } from './schema/metadata-contract';
|
|
28
|
+
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
29
|
+
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
13
30
|
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 {
|
|
31
|
+
export type { BaseNode, ContentLink, EdgeDefinition, FileNotInSpaceResult, FileValidationResult, HierarchyLevel, ParseIssue, ReadSpaceResult, Relationship, ResolvedContentLink, SchemaMetadata, SchemaWithMetadata, SpaceContext, SpaceNode, UnresolvedRef, ValidateFileOutput, } from './types';
|
|
32
|
+
export type { ValidationResult } from './validate';
|
|
33
|
+
export { validateFile, validateSpace } from './validate';
|
package/dist/api.js
CHANGED
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Import from this module to use structured-context as a library:
|
|
5
5
|
*
|
|
6
|
-
* import { validateFile } from 'structured-context/api';
|
|
6
|
+
* import { loadConfig, loadSpaceGraph, validateSpace, validateFile } from 'structured-context/api';
|
|
7
7
|
* import type { StructuredContextPlugin, PluginContext, ParseResult } from 'structured-context/api';
|
|
8
|
+
*
|
|
9
|
+
* Common patterns:
|
|
10
|
+
*
|
|
11
|
+
* const config = loadConfig();
|
|
12
|
+
* const graph = await loadSpaceGraph('my-space', config, { filter: 'my-view' });
|
|
13
|
+
*
|
|
14
|
+
* // Read once, then assemble a graph and validate from the same read and context:
|
|
15
|
+
* const ctx = createSpaceContext('my-space', config);
|
|
16
|
+
* const readResult = await readSpace(ctx);
|
|
17
|
+
* const graph = await assembleSpaceGraph(ctx, { readResult });
|
|
18
|
+
* const validation = await validateSpace(ctx, { readResult });
|
|
8
19
|
*/
|
|
9
|
-
export {
|
|
20
|
+
export { loadConfig, setConfigPath } from './config';
|
|
21
|
+
export { PLUGIN_PREFIX } from './constants';
|
|
22
|
+
export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
|
|
23
|
+
export { readSpace } from './read/read-space';
|
|
24
|
+
export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
|
|
25
|
+
export { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
26
|
+
export { validateFile, validateSpace } from './validate';
|
package/dist/commands/diagram.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import { executeRender } from '../render/render';
|
|
3
3
|
export async function diagram(context, options) {
|
|
4
|
-
const result = await executeRender('
|
|
4
|
+
const result = await executeRender('mermaid.graph', context, { filter: options.filter });
|
|
5
5
|
if (options.output) {
|
|
6
6
|
writeFileSync(options.output, result);
|
|
7
7
|
console.log(`Mermaid diagram written to ${options.output}`);
|
package/dist/commands/schemas.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { loadConfig, resolveSchema } from '../config';
|
|
4
4
|
import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema } from '../schema/schema';
|
|
5
|
-
import { mergeVariantProperties } from '../schema/schema-refs';
|
|
5
|
+
import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
|
|
6
6
|
function isBundledPath(schemaPath) {
|
|
7
7
|
return dirname(schemaPath) === bundledSchemasDir;
|
|
8
8
|
}
|
|
@@ -21,6 +21,7 @@ function extractRefs(obj, refs) {
|
|
|
21
21
|
function extractEntities(oneOf, schemaRefRegistry, schema) {
|
|
22
22
|
return oneOf.map((entry) => {
|
|
23
23
|
const entryObj = entry;
|
|
24
|
+
const resolvedObj = resolveRef(entryObj, schema, schemaRefRegistry) ?? entryObj;
|
|
24
25
|
const { properties, required } = mergeVariantProperties(entryObj, schema, schemaRefRegistry);
|
|
25
26
|
const typeDef = properties.type;
|
|
26
27
|
let types = [];
|
|
@@ -30,7 +31,7 @@ function extractEntities(oneOf, schemaRefRegistry, schema) {
|
|
|
30
31
|
types = typeDef.enum.map(String);
|
|
31
32
|
return {
|
|
32
33
|
types,
|
|
33
|
-
description: typeof
|
|
34
|
+
description: typeof resolvedObj.description === 'string' ? resolvedObj.description : undefined,
|
|
34
35
|
properties: Object.keys(properties).filter((k) => k !== 'type'),
|
|
35
36
|
required: required.filter((r) => r !== 'type'),
|
|
36
37
|
};
|
package/dist/commands/spaces.js
CHANGED
|
@@ -14,8 +14,6 @@ export function listSpaces() {
|
|
|
14
14
|
console.log(` path: ${space.path}`);
|
|
15
15
|
const schema = space.schema ?? config.schema;
|
|
16
16
|
console.log(` schema: ${schema ? basename(schema) : '(none)'}`);
|
|
17
|
-
if (space.miroBoardId)
|
|
18
|
-
console.log(` miro: configured`);
|
|
19
17
|
const plugins = space.plugins ?? {};
|
|
20
18
|
if (Object.keys(plugins).length > 0) {
|
|
21
19
|
console.log(' plugins:');
|
|
@@ -21,19 +21,31 @@ export async function validateFileCommand(filePath, options = {}) {
|
|
|
21
21
|
else {
|
|
22
22
|
printHumanReadable(result);
|
|
23
23
|
}
|
|
24
|
-
return
|
|
24
|
+
return result.errorCount > 0 ? 1 : 0;
|
|
25
25
|
}
|
|
26
26
|
function printHumanReadable(result) {
|
|
27
27
|
const reset = '\x1b[0m';
|
|
28
28
|
const green = '\x1b[32m';
|
|
29
29
|
const red = '\x1b[31m';
|
|
30
|
-
|
|
30
|
+
const yellow = '\x1b[33m';
|
|
31
|
+
if (result.errorCount === 0 && result.warningCount === 0) {
|
|
31
32
|
console.log(`${green}✓${reset} ${result.label} (space: ${result.space})`);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
if (result.errorCount > 0) {
|
|
36
|
+
console.log(`\n${red}✗${reset} ${result.label} (space: ${result.space}) — ${result.errorCount} error(s)\n`);
|
|
37
|
+
for (const { kind, message } of Object.values(result.errors)) {
|
|
38
|
+
console.log(` [${kind}] ${message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(`\n${green}✓${reset} ${result.label} (space: ${result.space})`);
|
|
43
|
+
}
|
|
44
|
+
if (result.warningCount > 0) {
|
|
45
|
+
console.log(`\n ${yellow}${result.warningCount} warning(s):${reset}`);
|
|
46
|
+
for (const { kind, message } of Object.values(result.warnings)) {
|
|
47
|
+
console.log(` ${yellow}[${kind}]${reset} ${message}`);
|
|
48
|
+
}
|
|
37
49
|
}
|
|
38
50
|
console.log('');
|
|
39
51
|
}
|
|
@@ -47,6 +47,7 @@ export async function validate(context, options = {}) {
|
|
|
47
47
|
errors: errorsByFile,
|
|
48
48
|
orphanCount: result.orphans.length,
|
|
49
49
|
parseIssues: result.parseIssues,
|
|
50
|
+
unresolvedContentLinks: result.unresolvedContentLinks,
|
|
50
51
|
}, null, 2));
|
|
51
52
|
return errorCount > 0 ? 1 : 0;
|
|
52
53
|
}
|
|
@@ -81,6 +82,7 @@ export async function validate(context, options = {}) {
|
|
|
81
82
|
console.log(fmt(' Rule violations', result.ruleViolations.length, true));
|
|
82
83
|
console.log(fmt(' Hierarchy violations', result.hierarchyViolations.length, true));
|
|
83
84
|
console.log(fmt(' Orphans (hierarchy nodes - no parent)', result.orphans.length, true, true));
|
|
85
|
+
console.log(fmt(' Unresolved content links', result.unresolvedContentLinks.length, true, true));
|
|
84
86
|
const parseIssueErrorCount = result.parseIssues.filter((i) => i.severity === 'error').length;
|
|
85
87
|
console.log(fmt(' Excluded during parsing', result.parseIssues.length, true, parseIssueErrorCount === 0));
|
|
86
88
|
if (result.orphans.length > 0) {
|
|
@@ -88,6 +90,20 @@ export async function validate(context, options = {}) {
|
|
|
88
90
|
for (const node of result.orphans)
|
|
89
91
|
console.log(` ${node.label}`);
|
|
90
92
|
}
|
|
93
|
+
if (result.unresolvedContentLinks.length > 0) {
|
|
94
|
+
console.log(`\nUnresolved content links (may be outside-space or broken):`);
|
|
95
|
+
const byFile = new Map();
|
|
96
|
+
for (const { file, target } of result.unresolvedContentLinks) {
|
|
97
|
+
if (!byFile.has(file))
|
|
98
|
+
byFile.set(file, []);
|
|
99
|
+
byFile.get(file).push(target);
|
|
100
|
+
}
|
|
101
|
+
for (const [file, targets] of byFile) {
|
|
102
|
+
console.log(` ${file}:`);
|
|
103
|
+
for (const target of targets)
|
|
104
|
+
console.log(` ${target}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
91
107
|
if (result.parseIssues.length > 0) {
|
|
92
108
|
console.log(`\nExcluded during parsing:`);
|
|
93
109
|
for (const issue of result.parseIssues) {
|
package/dist/config.d.ts
CHANGED
|
@@ -2,14 +2,18 @@ export type SpaceConfig = {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
schema?: string;
|
|
5
|
-
miroBoardId?: string;
|
|
6
|
-
miroFrameId?: string;
|
|
7
5
|
/** Plugin name → plugin config map. Overrides top-level plugins when set. */
|
|
8
6
|
plugins?: Record<string, Record<string, unknown>>;
|
|
9
7
|
/** Named filter views for this space. Keys are view names; values contain the filter expression. */
|
|
10
8
|
views?: Record<string, {
|
|
11
9
|
expression: string;
|
|
12
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Directory of the config file that defined this space, used to anchor relative plugin-config
|
|
13
|
+
* paths. Set by loadConfig; absent for a hand-assembled Config, whose callers must pass an
|
|
14
|
+
* explicit configDir to createSpaceContext. Not persisted by updateSpaceField.
|
|
15
|
+
*/
|
|
16
|
+
sourceDir?: string;
|
|
13
17
|
};
|
|
14
18
|
export type Config = {
|
|
15
19
|
spaces: SpaceConfig[];
|
|
@@ -20,19 +24,11 @@ export type Config = {
|
|
|
20
24
|
export declare function setConfigPath(path: string | undefined): void;
|
|
21
25
|
/** Get all config file paths that were loaded (main config + included configs). */
|
|
22
26
|
export declare function getConfigSourceFiles(): Set<string>;
|
|
23
|
-
/**
|
|
24
|
-
* Get the directory of the config file that defines a given space.
|
|
25
|
-
*/
|
|
26
|
-
export declare function getSpaceConfigDir(spaceName: string): string;
|
|
27
27
|
export declare function configPath(): string;
|
|
28
28
|
export declare function loadConfig(): Config;
|
|
29
29
|
/** Get the full space config entry by name. Throws if not found. */
|
|
30
30
|
export declare function getSpaceConfig(name: string, config: Config): SpaceConfig;
|
|
31
31
|
/** Resolve schema path: CLI arg > space-level config > global config. Throws if none configured. */
|
|
32
32
|
export declare function resolveSchema(config: Config, space?: SpaceConfig): string;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}[keyof T];
|
|
36
|
-
/** Update a string field on a space entry and persist config. */
|
|
37
|
-
export declare function updateSpaceField(spaceName: string, field: StringFields<SpaceConfig>, value: string): void;
|
|
38
|
-
export {};
|
|
33
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
34
|
+
export declare function updateSpaceField(spaceName: string, field: string, value: string, plugin?: string): void;
|
package/dist/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import Ajv from 'ajv';
|
|
5
5
|
import JSON5 from 'json5';
|
|
6
6
|
import { ENV_CONFIG_VAR, XDG_CONFIG_DIR } from './constants';
|
|
7
|
-
import { normalizePluginName } from './plugins/util';
|
|
7
|
+
import { normalizePluginName, shortenPluginName } from './plugins/util';
|
|
8
8
|
import { bundledSchemasDir } from './schema/schema';
|
|
9
9
|
const CONFIG_SCHEMA = {
|
|
10
10
|
type: 'object',
|
|
@@ -17,8 +17,6 @@ const CONFIG_SCHEMA = {
|
|
|
17
17
|
name: { type: 'string', pattern: '^[a-z0-9_-]+$' },
|
|
18
18
|
path: { type: 'string' },
|
|
19
19
|
schema: { type: 'string' },
|
|
20
|
-
miroBoardId: { type: 'string' },
|
|
21
|
-
miroFrameId: { type: 'string' },
|
|
22
20
|
plugins: { type: 'object', additionalProperties: { type: 'object' } },
|
|
23
21
|
views: {
|
|
24
22
|
type: 'object',
|
|
@@ -37,7 +35,7 @@ const CONFIG_SCHEMA = {
|
|
|
37
35
|
schema: { type: 'string' },
|
|
38
36
|
includeSpacesFrom: { type: 'array', items: { type: 'string' } },
|
|
39
37
|
},
|
|
40
|
-
required: ['spaces'],
|
|
38
|
+
anyOf: [{ required: ['spaces'] }, { required: ['includeSpacesFrom'] }],
|
|
41
39
|
additionalProperties: false,
|
|
42
40
|
};
|
|
43
41
|
let _configPathOverride;
|
|
@@ -54,18 +52,6 @@ export function getConfigSourceFiles() {
|
|
|
54
52
|
}
|
|
55
53
|
return new Set(_spaceSourceFiles.values());
|
|
56
54
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the directory of the config file that defines a given space.
|
|
59
|
-
*/
|
|
60
|
-
export function getSpaceConfigDir(spaceName) {
|
|
61
|
-
if (_spaceSourceFiles.size === 0) {
|
|
62
|
-
loadConfig();
|
|
63
|
-
}
|
|
64
|
-
const spaceConfigPath = _spaceSourceFiles.get(spaceName);
|
|
65
|
-
if (!spaceConfigPath)
|
|
66
|
-
throw new Error('Space config path not found');
|
|
67
|
-
return dirname(spaceConfigPath);
|
|
68
|
-
}
|
|
69
55
|
export function configPath() {
|
|
70
56
|
if (_configPathOverride) {
|
|
71
57
|
return _configPathOverride;
|
|
@@ -130,6 +116,8 @@ function _loadConfig(path) {
|
|
|
130
116
|
if (!validate(config)) {
|
|
131
117
|
throw new Error(`Invalid config in ${path}: ${JSON.stringify(validate.errors)}`);
|
|
132
118
|
}
|
|
119
|
+
if (!config.spaces)
|
|
120
|
+
config.spaces = [];
|
|
133
121
|
return config;
|
|
134
122
|
}
|
|
135
123
|
export function loadConfig() {
|
|
@@ -141,8 +129,10 @@ export function loadConfig() {
|
|
|
141
129
|
const config = _loadConfig(path);
|
|
142
130
|
_spaceSourceFiles.clear();
|
|
143
131
|
// Track which spaces come from the main config file
|
|
132
|
+
const mainSourceDir = dirname(resolve(path));
|
|
144
133
|
for (const space of config.spaces) {
|
|
145
134
|
_spaceSourceFiles.set(space.name, path);
|
|
135
|
+
space.sourceDir = mainSourceDir;
|
|
146
136
|
}
|
|
147
137
|
// Load includeSpacesFrom configs and merge their spaces in, with later entries taking precedence over earlier ones
|
|
148
138
|
if (config.includeSpacesFrom) {
|
|
@@ -154,8 +144,10 @@ export function loadConfig() {
|
|
|
154
144
|
throw new Error(`Included config contains spaces with duplicate names: ${resolvedIncludePath}`);
|
|
155
145
|
}
|
|
156
146
|
// Track which spaces come from this included config file
|
|
147
|
+
const includeSourceDir = dirname(resolvedIncludePath);
|
|
157
148
|
for (const space of includedConfig.spaces) {
|
|
158
149
|
_spaceSourceFiles.set(space.name, resolvedIncludePath);
|
|
150
|
+
space.sourceDir = includeSourceDir;
|
|
159
151
|
}
|
|
160
152
|
config.spaces.push(...includedConfig.spaces);
|
|
161
153
|
}
|
|
@@ -178,17 +170,27 @@ export function resolveSchema(config, space) {
|
|
|
178
170
|
}
|
|
179
171
|
return schema;
|
|
180
172
|
}
|
|
181
|
-
/** Update a string field on a space entry and persist config. */
|
|
182
|
-
export function updateSpaceField(spaceName, field, value) {
|
|
173
|
+
/** Update a string field on a space entry and persist config. When `plugin` is given, updates that field inside `space.plugins[plugin]` instead. */
|
|
174
|
+
export function updateSpaceField(spaceName, field, value, plugin) {
|
|
183
175
|
const sourcePath = _spaceSourceFiles.get(spaceName);
|
|
184
|
-
if (!sourcePath)
|
|
176
|
+
if (!sourcePath)
|
|
185
177
|
throw new Error(`Space "${spaceName}" not found in any config file`);
|
|
186
|
-
}
|
|
187
178
|
const config = _loadConfig(sourcePath);
|
|
188
179
|
const space = config.spaces?.find((s) => s.name === spaceName);
|
|
189
|
-
if (!space)
|
|
180
|
+
if (!space)
|
|
190
181
|
throw new Error(`Unknown space config: "${spaceName}". Check config.`);
|
|
182
|
+
if (plugin !== undefined) {
|
|
183
|
+
if (!space.plugins)
|
|
184
|
+
space.plugins = {};
|
|
185
|
+
const normalized = normalizePluginName(plugin);
|
|
186
|
+
const shortName = shortenPluginName(normalized);
|
|
187
|
+
const existingKey = Object.keys(space.plugins).find((k) => k === normalized || k === shortName) ?? normalized;
|
|
188
|
+
if (!space.plugins[existingKey])
|
|
189
|
+
space.plugins[existingKey] = {};
|
|
190
|
+
space.plugins[existingKey][field] = value;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
space[field] = value;
|
|
191
194
|
}
|
|
192
|
-
space[field] = value;
|
|
193
195
|
writeFileSync(sourcePath, `${JSON5.stringify(config, null, 2)}\n`);
|
|
194
196
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { diagram } from './commands/diagram';
|
|
5
5
|
import { docs } from './commands/docs';
|
|
6
6
|
import { dump } from './commands/dump';
|
|
7
|
+
import { miroSyncCommand } from './commands/miro-sync';
|
|
7
8
|
import { listPlugins } from './commands/plugins';
|
|
8
9
|
import { render, renderList } from './commands/render';
|
|
9
10
|
import { listSchemas, showSchema } from './commands/schemas';
|
|
@@ -12,29 +13,20 @@ import { listSpaces } from './commands/spaces';
|
|
|
12
13
|
import { templateSync } from './commands/template-sync';
|
|
13
14
|
import { validate, watchValidate } from './commands/validate';
|
|
14
15
|
import { validateFileCommand } from './commands/validate-file';
|
|
15
|
-
import {
|
|
16
|
+
import { loadConfig, setConfigPath } from './config';
|
|
16
17
|
import { CLI_NAME } from './constants';
|
|
17
|
-
import {
|
|
18
|
-
import { loadSchema } from './schema/schema';
|
|
18
|
+
import { createSpaceContext, SpaceNotFoundError } from './space-context';
|
|
19
19
|
export function buildSpaceContext(spaceName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try {
|
|
21
|
+
return createSpaceContext(spaceName, loadConfig());
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof SpaceNotFoundError) {
|
|
25
|
+
console.error(`Error: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
25
29
|
}
|
|
26
|
-
const resolvedSchemaPath = resolveSchema(config, space);
|
|
27
|
-
const { schema, schemaRefRegistry, schemaValidator } = loadSchema(resolvedSchemaPath);
|
|
28
|
-
const configDir = getSpaceConfigDir(space.name);
|
|
29
|
-
return {
|
|
30
|
-
space,
|
|
31
|
-
config,
|
|
32
|
-
resolvedSchemaPath,
|
|
33
|
-
schema,
|
|
34
|
-
schemaRefRegistry,
|
|
35
|
-
schemaValidator,
|
|
36
|
-
configDir,
|
|
37
|
-
};
|
|
38
30
|
}
|
|
39
31
|
const require = createRequire(import.meta.url);
|
|
40
32
|
const packageJson = require('../package.json');
|
|
@@ -97,7 +89,7 @@ program
|
|
|
97
89
|
.option('--new-frame <title>', 'Create a new frame on the board and sync into it')
|
|
98
90
|
.option('--dry-run', 'Show what would change without touching Miro')
|
|
99
91
|
.option('-v, --verbose', 'Detailed output')
|
|
100
|
-
.action((spaceName, options) =>
|
|
92
|
+
.action((spaceName, options) => miroSyncCommand(buildSpaceContext(spaceName), options));
|
|
101
93
|
program
|
|
102
94
|
.command('template-sync')
|
|
103
95
|
.description('Sync schema examples to templates')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Config } from './config';
|
|
2
|
+
import { type SpaceGraph } from './space-graph';
|
|
3
|
+
import type { ReadSpaceResult, SpaceContext } from './types';
|
|
4
|
+
export interface AssembleSpaceGraphOptions {
|
|
5
|
+
/** Named view (resolved via space.views) or a raw filter DSL expression. */
|
|
6
|
+
filter?: string;
|
|
7
|
+
/** Pre-loaded read result to assemble from, avoiding a re-read of the space. */
|
|
8
|
+
readResult?: ReadSpaceResult;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
12
|
+
* fail schema validation, build the hierarchy graph, and apply an optional
|
|
13
|
+
* view/filter expression.
|
|
14
|
+
*
|
|
15
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
16
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
17
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
18
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
19
|
+
*
|
|
20
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
21
|
+
* graph-assembly behaviour cannot drift.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assembleSpaceGraph(context: SpaceContext, options?: AssembleSpaceGraphOptions): Promise<SpaceGraph>;
|
|
24
|
+
export interface LoadSpaceGraphOptions extends AssembleSpaceGraphOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Directory to anchor relative plugin paths against. Required when config is
|
|
27
|
+
* hand-assembled (has no source file); see {@link createSpaceContext}.
|
|
28
|
+
*/
|
|
29
|
+
configDir?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
33
|
+
*
|
|
34
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
35
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
36
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
37
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
38
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadSpaceGraph(spaceName: string, config: Config, options?: LoadSpaceGraphOptions): Promise<SpaceGraph>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { filterNodes } from './filter/filter-nodes';
|
|
2
|
+
import { readSpace } from './read/read-space';
|
|
3
|
+
import { createSpaceContext } from './space-context';
|
|
4
|
+
import { buildSpaceGraph } from './space-graph';
|
|
5
|
+
/**
|
|
6
|
+
* Assemble a SpaceGraph from a space context: read the space, drop nodes that
|
|
7
|
+
* fail schema validation, build the hierarchy graph, and apply an optional
|
|
8
|
+
* view/filter expression.
|
|
9
|
+
*
|
|
10
|
+
* Nodes that fail schema validation are silently excluded so the graph is
|
|
11
|
+
* well-formed enough to render and traverse. This is NOT a validation pass —
|
|
12
|
+
* schema-valid nodes may still have broken references, rule violations, or
|
|
13
|
+
* duplicates. Use validateSpace for a full validation report.
|
|
14
|
+
*
|
|
15
|
+
* Shared by the render pipeline and the public loadSpaceGraph API so their
|
|
16
|
+
* graph-assembly behaviour cannot drift.
|
|
17
|
+
*/
|
|
18
|
+
export async function assembleSpaceGraph(context, options = {}) {
|
|
19
|
+
const { nodes: allNodes } = options.readResult ?? (await readSpace(context));
|
|
20
|
+
const { schemaValidator } = context;
|
|
21
|
+
const validNodes = allNodes.filter((node) => schemaValidator(node.schemaData));
|
|
22
|
+
const levels = context.schema.metadata.hierarchy?.levels ?? [];
|
|
23
|
+
let graph = buildSpaceGraph(validNodes, levels);
|
|
24
|
+
if (options.filter) {
|
|
25
|
+
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
|
|
26
|
+
graph = await filterNodes(expression, graph);
|
|
27
|
+
}
|
|
28
|
+
return graph;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load a named space and return its assembled, filtered SpaceGraph.
|
|
32
|
+
*
|
|
33
|
+
* Convenience entry point over createSpaceContext + assembleSpaceGraph: pass a
|
|
34
|
+
* space name, config, and optional filter. Pass options.readResult to reuse an
|
|
35
|
+
* already-loaded read (e.g. when also calling validateSpace) and skip re-reading.
|
|
36
|
+
* When you already hold a SpaceContext, call assembleSpaceGraph directly to avoid
|
|
37
|
+
* rebuilding (and recompiling the schema for) the context.
|
|
38
|
+
*/
|
|
39
|
+
export async function loadSpaceGraph(spaceName, config, options = {}) {
|
|
40
|
+
const context = createSpaceContext(spaceName, config, { configDir: options.configDir });
|
|
41
|
+
return assembleSpaceGraph(context, options);
|
|
42
|
+
}
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { markdownPlugin } from './markdown';
|
|
2
|
+
import { mermaidPlugin } from './mermaid';
|
|
3
|
+
import { miroPlugin } from './miro';
|
|
2
4
|
/** All built-in plugins, in default load order. */
|
|
3
|
-
export const builtinPlugins = [markdownPlugin];
|
|
5
|
+
export const builtinPlugins = [markdownPlugin, mermaidPlugin, miroPlugin];
|
package/dist/plugins/loader.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
3
3
|
import Ajv, {} from 'ajv';
|
|
4
4
|
import { getConfigSourceFiles } from '../config';
|
|
5
5
|
import { builtinPlugins } from '.';
|
|
6
|
-
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX } from './util';
|
|
6
|
+
import { CONFIG_PLUGINS_DIR, normalizePluginName, PLUGIN_PREFIX, shortenPluginName, } from './util';
|
|
7
7
|
/**
|
|
8
8
|
* Walk a plugin's configSchema and resolve any string fields annotated with
|
|
9
9
|
* format:'path' relative to configDir.
|
|
@@ -92,7 +92,7 @@ export async function loadPlugins(pluginMap, configDir) {
|
|
|
92
92
|
}
|
|
93
93
|
// Built-in plugins: always loaded, config taken from map if declared (with or without prefix)
|
|
94
94
|
for (const builtin of builtinPlugins) {
|
|
95
|
-
const rawConfig = pluginMap[builtin.name] ?? pluginMap[builtin.name
|
|
95
|
+
const rawConfig = pluginMap[builtin.name] ?? pluginMap[shortenPluginName(builtin.name)] ?? {};
|
|
96
96
|
const pluginConfig = resolveConfigPaths(builtin.configSchema, rawConfig, configDir);
|
|
97
97
|
const validate = ajv.compile(builtin.configSchema);
|
|
98
98
|
if (!validate(pluginConfig)) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Node } from 'mdast';
|
|
2
|
+
import type { ContentLink, SchemaMetadata } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively walk an mdast subtree and collect all links.
|
|
5
|
+
* Handles standard markdown link/image nodes and scans text nodes for wikilinks.
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractLinksFromAstNode(node: Node): ContentLink[];
|
|
8
|
+
/**
|
|
9
|
+
* Extract all links from a raw markdown body string.
|
|
10
|
+
* Parses the full document and collects links from every node in the tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractLinksFromBody(body: string): ContentLink[];
|
|
13
|
+
/**
|
|
14
|
+
* Build the set of field names that serve as graph edges (hierarchy + relationship fields).
|
|
15
|
+
* Used to exclude edge fields from frontmatter link extraction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getEdgeFieldNames(metadata: SchemaMetadata): Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract links from frontmatter data fields, excluding known graph edge fields.
|
|
20
|
+
* Parses each string value as markdown (via mdast) to keep link extraction consistent
|
|
21
|
+
* with body content parsing — handles wikilinks, markdown links, images, and bare URLs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractLinksFromFrontmatter(data: Record<string, unknown>, edgeFields: Set<string>): ContentLink[];
|