structured-context 0.9.3 → 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 +33 -0
- package/dist/api.js +26 -0
- package/dist/commands/diagram.js +1 -1
- package/dist/commands/dump.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.d.ts +5 -24
- package/dist/commands/validate-file.js +26 -108
- package/dist/commands/validate.d.ts +1 -12
- package/dist/commands/validate.js +22 -157
- package/dist/config.d.ts +8 -12
- package/dist/config.js +25 -23
- package/dist/index.js +15 -23
- 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.d.ts +42 -32
- package/dist/plugins/markdown/index.js +10 -15
- package/dist/plugins/markdown/parse-embedded.d.ts +1 -1
- package/dist/plugins/markdown/parse-embedded.js +9 -0
- package/dist/plugins/markdown/read-space.js +6 -3
- package/dist/plugins/markdown/template-sync.d.ts +1 -1
- package/dist/plugins/markdown/template-sync.js +2 -2
- 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 +7 -3
- 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 +72 -2
- package/dist/validate.d.ts +59 -0
- package/dist/validate.js +316 -0
- package/docs/concepts.md +23 -0
- package/docs/config.md +2 -0
- package/package.json +11 -5
- package/schemas/_knowledge_wiki.json +1 -1
- package/dist/integrations/miro/sync.d.ts +0 -8
- package/dist/plugin-api.d.ts +0 -12
- package/dist/plugin-api.js +0 -7
- /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
|
@@ -3,6 +3,7 @@ import { toString as mdastToString } from 'mdast-util-to-string';
|
|
|
3
3
|
import remarkGfm from 'remark-gfm';
|
|
4
4
|
import remarkParse from 'remark-parse';
|
|
5
5
|
import { unified } from 'unified';
|
|
6
|
+
import { extractLinksFromAstNode } from './extract-content-links';
|
|
6
7
|
import { applyFieldMap, coerceDates } from './util';
|
|
7
8
|
/** Type values that identify a space_on_a_page container (not themselves space nodes). */
|
|
8
9
|
export const ON_A_PAGE_TYPES = ['ost_on_a_page', 'space_on_a_page'];
|
|
@@ -191,6 +192,7 @@ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, build
|
|
|
191
192
|
schemaData,
|
|
192
193
|
linkTargets,
|
|
193
194
|
type,
|
|
195
|
+
contentLinks: [],
|
|
194
196
|
};
|
|
195
197
|
nodes.push(newNode);
|
|
196
198
|
if (parentFieldAppend) {
|
|
@@ -230,6 +232,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
230
232
|
schemaData: { type: 'space_on_a_page' },
|
|
231
233
|
linkTargets: [],
|
|
232
234
|
type: 'space_on_a_page',
|
|
235
|
+
contentLinks: [],
|
|
233
236
|
};
|
|
234
237
|
const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
|
|
235
238
|
// In typed-page mode: stack starts with the page's own virtual entry (depth 0).
|
|
@@ -471,6 +474,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
471
474
|
schemaData,
|
|
472
475
|
linkTargets,
|
|
473
476
|
type,
|
|
477
|
+
contentLinks: [],
|
|
474
478
|
};
|
|
475
479
|
// Push to stack BEFORE resolving semantic parent — stack[-2] is the correct parent.
|
|
476
480
|
const refTarget = linkTargets[0] ?? title;
|
|
@@ -608,6 +612,7 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
608
612
|
schemaData,
|
|
609
613
|
linkTargets,
|
|
610
614
|
type: rowTypeStr,
|
|
615
|
+
contentLinks: [],
|
|
611
616
|
};
|
|
612
617
|
nodes.push(rowNode);
|
|
613
618
|
if (tableParentFieldAppend) {
|
|
@@ -640,6 +645,8 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
640
645
|
Object.assign(activeNode.schemaData, allFields);
|
|
641
646
|
if (remainingText)
|
|
642
647
|
appendContent(activeNode, remainingText);
|
|
648
|
+
activeNode.contentLinks ??= [];
|
|
649
|
+
activeNode.contentLinks.push(...extractLinksFromAstNode(child));
|
|
643
650
|
}
|
|
644
651
|
else if (child.type === 'code' && child.lang?.trim() === 'yaml') {
|
|
645
652
|
const code = child;
|
|
@@ -656,6 +663,8 @@ export function extractEmbeddedNodes(body, options) {
|
|
|
656
663
|
}
|
|
657
664
|
else {
|
|
658
665
|
appendContent(activeNode, mdastToString(child));
|
|
666
|
+
activeNode.contentLinks ??= [];
|
|
667
|
+
activeNode.contentLinks.push(...extractLinksFromAstNode(child));
|
|
659
668
|
}
|
|
660
669
|
}
|
|
661
670
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, join, resolve } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { extractSchemaTypeNames } from '../../schema/schema';
|
|
6
|
+
import { extractLinksFromBody, extractLinksFromFrontmatter, getEdgeFieldNames } from './extract-content-links';
|
|
6
7
|
import { extractEmbeddedNodes, ON_A_PAGE_TYPES } from './parse-embedded';
|
|
7
8
|
import { applyFieldMap, coerceDates, inferTypeFromPath } from './util';
|
|
8
9
|
export function readSpaceOnAPage(context) {
|
|
@@ -42,7 +43,8 @@ export async function readSpaceDirectory(context, options) {
|
|
|
42
43
|
const absoluteTemplateDir = templateDir ? resolve(templateDir) : undefined;
|
|
43
44
|
const typeInferenceCfg = mdCfg.typeInference;
|
|
44
45
|
const knownTypes = typeInferenceCfg?.mode !== 'off' ? extractSchemaTypeNames(context.schema, context.schemaRefRegistry) : undefined;
|
|
45
|
-
const
|
|
46
|
+
const edgeFields = getEdgeFieldNames(metadata);
|
|
47
|
+
const files = await fg('**/*.md', { cwd: directory, followSymbolicLinks: true });
|
|
46
48
|
const nodes = [];
|
|
47
49
|
const parseIssues = [];
|
|
48
50
|
for (const file of files) {
|
|
@@ -70,7 +72,7 @@ export async function readSpaceDirectory(context, options) {
|
|
|
70
72
|
continue;
|
|
71
73
|
}
|
|
72
74
|
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
73
|
-
parseIssues.push({ file, severity: 'warning', type: '
|
|
75
|
+
parseIssues.push({ file, severity: 'warning', type: 'no-type', message: 'No front-matter or type specified' });
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
78
|
const data = coerceDates(applyFieldMap(parsed.data, fieldMap));
|
|
@@ -93,6 +95,7 @@ export async function readSpaceDirectory(context, options) {
|
|
|
93
95
|
schemaData: { title, ...data },
|
|
94
96
|
linkTargets: [title, fileBase],
|
|
95
97
|
type: pageType,
|
|
98
|
+
contentLinks: [...extractLinksFromFrontmatter(data, edgeFields), ...extractLinksFromBody(parsed.content)],
|
|
96
99
|
});
|
|
97
100
|
if (!ON_A_PAGE_TYPES.includes(pageType)) {
|
|
98
101
|
const { nodes: embedded, terminatedHeadings } = extractEmbeddedNodes(parsed.content, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnySchemaObject } from 'ajv';
|
|
2
|
-
import type { HierarchyLevel, Relationship, SchemaWithMetadata } from '../../
|
|
2
|
+
import type { HierarchyLevel, Relationship, SchemaWithMetadata } from '../../api';
|
|
3
3
|
import type { PluginContext, TemplateSyncOptions } from '../util';
|
|
4
4
|
export interface TypeVariant {
|
|
5
5
|
required: string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
6
|
import { mergeVariantProperties, resolveRef } from '../../schema/schema-refs';
|
|
@@ -190,7 +190,7 @@ export async function templateSync(context, options) {
|
|
|
190
190
|
const { schema, schemaRefRegistry } = context;
|
|
191
191
|
const typeVariants = getTypeVariants(schema, schemaRefRegistry);
|
|
192
192
|
const matchedTypes = new Set();
|
|
193
|
-
const files = await
|
|
193
|
+
const files = await fg('*.md', { cwd: templateDir, absolute: true });
|
|
194
194
|
const dryRun = options.dryRun ?? false;
|
|
195
195
|
let filesModified = 0;
|
|
196
196
|
let filesCreated = 0;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PLUGIN_PREFIX } from '../util';
|
|
2
|
+
import { renderMermaid } from './render-mermaid';
|
|
3
|
+
export const mermaidPlugin = {
|
|
4
|
+
name: `${PLUGIN_PREFIX}mermaid`,
|
|
5
|
+
configSchema: { type: 'object', additionalProperties: false },
|
|
6
|
+
render: {
|
|
7
|
+
formats: [{ name: 'graph', description: 'Mermaid graph TD diagram' }],
|
|
8
|
+
render(_context, graph, { format }) {
|
|
9
|
+
if (format === 'graph')
|
|
10
|
+
return renderMermaid(graph);
|
|
11
|
+
throw new Error(`Unknown mermaid render format: "${format}"`);
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
const LEVEL_PALETTE = [
|
|
2
|
+
{ fill: '#ff9999', stroke: '#ff0000' },
|
|
3
|
+
{ fill: '#99ccff', stroke: '#0066cc' },
|
|
4
|
+
{ fill: '#99ff99', stroke: '#00cc00' },
|
|
5
|
+
{ fill: '#ffcc99', stroke: '#cc9900' },
|
|
6
|
+
{ fill: '#cc99ff', stroke: '#6600cc' },
|
|
7
|
+
{ fill: '#ff99cc', stroke: '#cc0066' },
|
|
8
|
+
{ fill: '#99ffff', stroke: '#009999' },
|
|
9
|
+
{ fill: '#ffff99', stroke: '#cccc00' },
|
|
10
|
+
];
|
|
1
11
|
function escapeMermaidString(str) {
|
|
2
12
|
return str.replace(/"/g, '"');
|
|
3
13
|
}
|
|
@@ -5,20 +15,13 @@ function safeNodeId(id) {
|
|
|
5
15
|
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6
16
|
}
|
|
7
17
|
export function renderMermaid(graph) {
|
|
8
|
-
const { hierarchyRoots, orphans, hierarchyChildren: children, hierarchyTitles: hierarchyNodeSet } = graph;
|
|
18
|
+
const { hierarchyRoots, orphans, hierarchyChildren: children, hierarchyTitles: hierarchyNodeSet, levels } = graph;
|
|
9
19
|
let mmd = 'graph TD\n';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mmd += ' classDef identified fill:#f0f0f0,stroke:#999999,stroke-dasharray: 5 5\n';
|
|
16
|
-
mmd += ' classDef wondering fill:#fff0cc,stroke:#cccc00,stroke-dasharray: 5 5\n';
|
|
17
|
-
mmd += ' classDef exploring fill:#ffcc99,stroke:#cc9900,stroke-dasharray: 5 5\n';
|
|
18
|
-
mmd += ' classDef active fill:#99ff99,stroke:#00cc00,stroke-width:2px\n';
|
|
19
|
-
mmd += ' classDef paused fill:#ffcc99,stroke:#cc9900,stroke-width:2px\n';
|
|
20
|
-
mmd += ' classDef completed fill:#ccccff,stroke:#6666cc,stroke-width:2px\n';
|
|
21
|
-
mmd += ' classDef archived fill:#e0e0e0,stroke:#999999,stroke-width:2px\n';
|
|
20
|
+
for (let i = 0; i < levels.length; i++) {
|
|
21
|
+
const level = levels[i];
|
|
22
|
+
const color = LEVEL_PALETTE[i % LEVEL_PALETTE.length];
|
|
23
|
+
mmd += ` classDef ${level.type} fill:${color.fill},stroke:${color.stroke},stroke-width:2px\n`;
|
|
24
|
+
}
|
|
22
25
|
const addedNodes = new Set();
|
|
23
26
|
function addNodeAndChildren(node) {
|
|
24
27
|
const nodeId = node.title;
|
|
@@ -26,13 +29,11 @@ export function renderMermaid(graph) {
|
|
|
26
29
|
return;
|
|
27
30
|
addedNodes.add(nodeId);
|
|
28
31
|
const type = node.resolvedType;
|
|
29
|
-
const status = node.schemaData.status;
|
|
30
32
|
const priority = node.schemaData.priority;
|
|
31
33
|
const label = priority ? `${nodeId} (${priority})` : nodeId;
|
|
32
|
-
const className = `${type}_${status}`;
|
|
33
34
|
const safeId = safeNodeId(nodeId);
|
|
34
35
|
const escapedLabel = escapeMermaidString(label);
|
|
35
|
-
mmd += ` ${safeId}["${escapedLabel}"]:::${
|
|
36
|
+
mmd += ` ${safeId}["${escapedLabel}"]:::${type}\n`;
|
|
36
37
|
const nodeChildren = children.get(nodeId) ?? [];
|
|
37
38
|
for (const child of nodeChildren) {
|
|
38
39
|
const childTitle = child.title;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PLUGIN_PREFIX } from '../../api';
|
|
2
|
+
import { miroSync } from './sync';
|
|
3
|
+
export const miroPlugin = {
|
|
4
|
+
name: `${PLUGIN_PREFIX}miro`,
|
|
5
|
+
configSchema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
boardId: { type: 'string' },
|
|
9
|
+
frameId: { type: 'string' },
|
|
10
|
+
},
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
},
|
|
13
|
+
render: {
|
|
14
|
+
formats: [{ name: 'board', description: 'Sync space to Miro board' }],
|
|
15
|
+
async render(context, graph, { format, data }) {
|
|
16
|
+
if (format === 'board') {
|
|
17
|
+
const options = {
|
|
18
|
+
newFrame: data?.newFrame,
|
|
19
|
+
dryRun: data?.dryRun,
|
|
20
|
+
verbose: data?.verbose,
|
|
21
|
+
};
|
|
22
|
+
return await miroSync(context, graph, options);
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Unknown miro render format: "${format}"`);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HierarchyLevel, SpaceNode } from '../../
|
|
1
|
+
import type { HierarchyLevel, SpaceNode } from '../../api';
|
|
2
2
|
export declare const CARD_WIDTH = 320;
|
|
3
3
|
export interface LayoutResult {
|
|
4
4
|
positions: Map<string, {
|
|
@@ -25,4 +25,4 @@ export interface LayoutResult {
|
|
|
25
25
|
export declare function layoutNewCards(newNodes: SpaceNode[], existingPositions: Map<string, {
|
|
26
26
|
x: number;
|
|
27
27
|
y: number;
|
|
28
|
-
}>, hierarchyLevels: HierarchyLevel[]): LayoutResult;
|
|
28
|
+
}>, hierarchyLevels: readonly HierarchyLevel[]): LayoutResult;
|
|
@@ -3,18 +3,6 @@ const CARD_HEIGHT = 160;
|
|
|
3
3
|
const H_GAP = 40;
|
|
4
4
|
const V_GAP = 60;
|
|
5
5
|
const FRAME_PADDING = 60;
|
|
6
|
-
/**
|
|
7
|
-
* DEPRECATED - likely not needed after migration to render plugin and SpaceGraph
|
|
8
|
-
* Build a depth map from hierarchy levels.
|
|
9
|
-
* The position in the hierarchy array determines the depth.
|
|
10
|
-
*/
|
|
11
|
-
function buildDepthMap(hierarchyLevels) {
|
|
12
|
-
const depthMap = new Map();
|
|
13
|
-
for (const [i, level] of hierarchyLevels.entries()) {
|
|
14
|
-
depthMap.set(level.type, i);
|
|
15
|
-
}
|
|
16
|
-
return depthMap;
|
|
17
|
-
}
|
|
18
6
|
/**
|
|
19
7
|
* Compute positions for new cards only. Existing cards keep their Miro positions.
|
|
20
8
|
* New cards are laid out in rows grouped by OST type depth, starting below
|
|
@@ -25,8 +13,6 @@ function buildDepthMap(hierarchyLevels) {
|
|
|
25
13
|
* Returns positions and a bounding box covering all cards (for frame sizing).
|
|
26
14
|
*/
|
|
27
15
|
export function layoutNewCards(newNodes, existingPositions, hierarchyLevels) {
|
|
28
|
-
// Build depth map from hierarchy levels (position in hierarchy = depth)
|
|
29
|
-
const depthMap = buildDepthMap(hierarchyLevels);
|
|
30
16
|
// Find the lowest y among existing cards
|
|
31
17
|
let lowestY = 0;
|
|
32
18
|
for (const pos of existingPositions.values()) {
|
|
@@ -35,10 +21,11 @@ export function layoutNewCards(newNodes, existingPositions, hierarchyLevels) {
|
|
|
35
21
|
}
|
|
36
22
|
}
|
|
37
23
|
const startY = existingPositions.size > 0 ? lowestY + V_GAP * 2 : 0;
|
|
38
|
-
// Group new nodes by depth
|
|
24
|
+
// Group new nodes by depth (position in hierarchy levels array)
|
|
39
25
|
const byDepth = new Map();
|
|
40
26
|
for (const node of newNodes) {
|
|
41
|
-
const
|
|
27
|
+
const idx = hierarchyLevels.findIndex((l) => l.type === node.schemaData.type);
|
|
28
|
+
const depth = idx === -1 ? hierarchyLevels.length : idx;
|
|
42
29
|
if (!byDepth.has(depth))
|
|
43
30
|
byDepth.set(depth, []);
|
|
44
31
|
byDepth.get(depth)?.push(node);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HierarchyLevel, SpaceNode } from '../../
|
|
1
|
+
import type { HierarchyLevel, SpaceNode } from '../../api';
|
|
2
2
|
/**
|
|
3
3
|
* Get card color based on type and hierarchy levels.
|
|
4
4
|
* Colors are assigned from palette based on level position.
|
|
@@ -6,6 +6,6 @@ import type { HierarchyLevel, SpaceNode } from '../../types';
|
|
|
6
6
|
* @param type - The node's type string
|
|
7
7
|
* @param hierarchyLevels - Hierarchy levels from metadata
|
|
8
8
|
*/
|
|
9
|
-
export declare function getCardColor(type: string, hierarchyLevels: HierarchyLevel[]): string;
|
|
9
|
+
export declare function getCardColor(type: string, hierarchyLevels: readonly HierarchyLevel[]): string;
|
|
10
10
|
export declare function buildCardTitle(node: SpaceNode): string;
|
|
11
11
|
export declare function buildCardDescription(node: SpaceNode): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginContext, SpaceGraph } from '../../api';
|
|
2
|
+
export interface SyncOptions {
|
|
3
|
+
newFrame?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function miroSync(context: PluginContext, graph: SpaceGraph, options: SyncOptions): Promise<string>;
|
|
@@ -1,50 +1,34 @@
|
|
|
1
|
-
import { updateSpaceField } from '../../config';
|
|
2
|
-
import { readSpace } from '../../read/read-space';
|
|
3
|
-
import { buildSpaceGraph } from '../../space-graph';
|
|
4
1
|
import { computeMiroCardHash, computeNodeHash, loadCache, saveCache } from './cache';
|
|
5
2
|
import { MiroClient, MiroNotFoundError } from './client';
|
|
6
3
|
import { CARD_WIDTH, layoutNewCards } from './layout';
|
|
7
4
|
import { buildCardDescription, buildCardTitle, getCardColor } from './styles';
|
|
8
|
-
export async function miroSync(context, options) {
|
|
5
|
+
export async function miroSync(context, graph, options) {
|
|
9
6
|
const token = process.env.MIRO_TOKEN;
|
|
10
|
-
if (!token)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
7
|
+
if (!token)
|
|
8
|
+
throw new Error('MIRO_TOKEN environment variable is required');
|
|
9
|
+
const { space } = context;
|
|
10
|
+
const boardId = context.pluginConfig.boardId;
|
|
11
|
+
const configuredFrameId = context.pluginConfig.frameId;
|
|
15
12
|
// 1. Resolve board
|
|
16
|
-
if (!
|
|
17
|
-
|
|
18
|
-
console.error('Add miroBoardId to the space entry in config.');
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
const boardId = space.miroBoardId;
|
|
13
|
+
if (!boardId)
|
|
14
|
+
throw new Error(`No boardId configured for space "${space.name}". Add boardId to the miro plugin config in the space entry.`);
|
|
22
15
|
// 2. Resolve frame
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// 3. Load space nodes (load before creating frame so we can calculate size)
|
|
28
|
-
let nodes;
|
|
29
|
-
({ nodes } = await readSpace(context));
|
|
16
|
+
if (!configuredFrameId && !options.newFrame)
|
|
17
|
+
throw new Error('No frameId in miro plugin config. Pass --new-frame "Title" to create one.');
|
|
18
|
+
// 3. Filter to hierarchy nodes only (graph already built by caller)
|
|
19
|
+
const nodes = [...graph.nodes.values()].filter((n) => graph.hierarchyTitles.has(n.title));
|
|
30
20
|
if (nodes.length === 0) {
|
|
31
|
-
|
|
32
|
-
return;
|
|
21
|
+
return 'No space nodes found.';
|
|
33
22
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const { nonHierarchy, hierarchyTitles: hierarchyNodeTitles } = buildSpaceGraph(nodes, levels);
|
|
37
|
-
// Filter nodes to only hierarchy nodes
|
|
38
|
-
nodes = nodes.filter((n) => hierarchyNodeTitles.has(n.title));
|
|
39
|
-
if (options.verbose && nonHierarchy.length > 0) {
|
|
40
|
-
console.log(`Excluded ${nonHierarchy.length} non-hierarchy nodes from sync`);
|
|
23
|
+
if (options.verbose && graph.nonHierarchy.length > 0) {
|
|
24
|
+
console.log(`Excluded ${graph.nonHierarchy.length} non-hierarchy nodes from sync`);
|
|
41
25
|
}
|
|
42
26
|
const client = new MiroClient(boardId, token);
|
|
43
27
|
let frameId;
|
|
44
28
|
let layoutOffset = null;
|
|
45
29
|
if (options.newFrame) {
|
|
46
30
|
// Calculate layout bounds to size the frame appropriately
|
|
47
|
-
const { bounds } = layoutNewCards(nodes, new Map(), levels);
|
|
31
|
+
const { bounds } = layoutNewCards(nodes, new Map(), graph.levels);
|
|
48
32
|
const frameWidth = Math.max(1600, bounds.maxX - bounds.minX);
|
|
49
33
|
const frameHeight = Math.max(1200, bounds.maxY - bounds.minY);
|
|
50
34
|
// Miro positions child items relative to the parent frame's top-left corner.
|
|
@@ -69,12 +53,12 @@ export async function miroSync(context, options) {
|
|
|
69
53
|
});
|
|
70
54
|
frameId = frame.id;
|
|
71
55
|
console.log(`Created frame "${options.newFrame}" (${frameId}) - size: ${finalFrameWidth}x${finalFrameHeight}`);
|
|
72
|
-
|
|
73
|
-
console.log(`Saved
|
|
56
|
+
context.callbacks?.persistConfig?.({ frameId });
|
|
57
|
+
console.log(`Saved frameId to miro plugin config`);
|
|
74
58
|
}
|
|
75
59
|
}
|
|
76
60
|
else {
|
|
77
|
-
frameId =
|
|
61
|
+
frameId = configuredFrameId;
|
|
78
62
|
}
|
|
79
63
|
// 4. Load cache
|
|
80
64
|
const cache = loadCache(boardId, frameId);
|
|
@@ -171,7 +155,7 @@ export async function miroSync(context, options) {
|
|
|
171
155
|
}
|
|
172
156
|
}
|
|
173
157
|
// Compute positions for new cards
|
|
174
|
-
const { positions: newPositions } = layoutNewCards(newNodes, existingPositions, levels);
|
|
158
|
+
const { positions: newPositions } = layoutNewCards(newNodes, existingPositions, graph.levels);
|
|
175
159
|
// 7. Create new cards
|
|
176
160
|
let createdCount = 0;
|
|
177
161
|
for (const node of newNodes) {
|
|
@@ -201,7 +185,7 @@ export async function miroSync(context, options) {
|
|
|
201
185
|
title: buildCardTitle(node),
|
|
202
186
|
description: buildCardDescription(node),
|
|
203
187
|
},
|
|
204
|
-
style: { cardTheme: getCardColor(type, levels) },
|
|
188
|
+
style: { cardTheme: getCardColor(type, graph.levels) },
|
|
205
189
|
position: { x: pos.x, y: pos.y, origin: 'center' },
|
|
206
190
|
parent: { id: frameId },
|
|
207
191
|
geometry: { width: CARD_WIDTH },
|
|
@@ -247,7 +231,7 @@ export async function miroSync(context, options) {
|
|
|
247
231
|
title: buildCardTitle(node),
|
|
248
232
|
description: buildCardDescription(node),
|
|
249
233
|
},
|
|
250
|
-
style: { cardTheme: getCardColor(type, levels) },
|
|
234
|
+
style: { cardTheme: getCardColor(type, graph.levels) },
|
|
251
235
|
position: { x: 0, y: 0, origin: 'center' },
|
|
252
236
|
parent: { id: frameId },
|
|
253
237
|
geometry: { width: CARD_WIDTH },
|
|
@@ -341,7 +325,5 @@ export async function miroSync(context, options) {
|
|
|
341
325
|
if (!options.dryRun)
|
|
342
326
|
saveCache(cache);
|
|
343
327
|
// Summary
|
|
344
|
-
|
|
345
|
-
console.log(` Cards: ${createdCount} created, ${updatedCount} updated, ${skippedCount} unchanged`);
|
|
346
|
-
console.log(` Connectors: ${connectorsCreated} created, ${connectorsDeleted} deleted`);
|
|
328
|
+
return `\n${prefix}Sync complete:\n Cards: ${createdCount} created, ${updatedCount} updated, ${skippedCount} unchanged\n Connectors: ${connectorsCreated} created, ${connectorsDeleted} deleted`;
|
|
347
329
|
}
|
package/dist/plugins/util.d.ts
CHANGED
|
@@ -5,9 +5,16 @@ export declare const PLUGIN_PREFIX = "sctx-";
|
|
|
5
5
|
export declare const CONFIG_PLUGINS_DIR = "plugins";
|
|
6
6
|
/** Normalize a plugin name to its canonical prefixed form. */
|
|
7
7
|
export declare function normalizePluginName(name: string): string;
|
|
8
|
+
/** Shorten a plugin name to its non-prefixed form. */
|
|
9
|
+
export declare function shortenPluginName(name: string): string;
|
|
8
10
|
export type PluginContext = SpaceContext & {
|
|
9
11
|
/** Validated config for this plugin invocation. */
|
|
10
12
|
pluginConfig: Record<string, unknown>;
|
|
13
|
+
/** Optional callback functions for plugins to interact with the system. */
|
|
14
|
+
callbacks?: {
|
|
15
|
+
/** Persist plugin config updates to the config file. */
|
|
16
|
+
persistConfig?: (updates: Record<string, string>) => void;
|
|
17
|
+
};
|
|
11
18
|
};
|
|
12
19
|
export type ParseResult = {
|
|
13
20
|
nodes: BaseNode[];
|
|
@@ -33,6 +40,8 @@ export type RenderFormat = {
|
|
|
33
40
|
export type RenderOptions = {
|
|
34
41
|
/** The format name being rendered (e.g. 'bullets', 'mermaid'). */
|
|
35
42
|
format: string;
|
|
43
|
+
/** Plugin-specific extra data (e.g. CLI flags forwarded by a convenience command). */
|
|
44
|
+
data?: Record<string, unknown>;
|
|
36
45
|
};
|
|
37
46
|
/**
|
|
38
47
|
* The render hook on a plugin: declares available formats and handles rendering.
|
package/dist/plugins/util.js
CHANGED
|
@@ -5,3 +5,7 @@ export const CONFIG_PLUGINS_DIR = 'plugins';
|
|
|
5
5
|
export function normalizePluginName(name) {
|
|
6
6
|
return name.startsWith(PLUGIN_PREFIX) ? name : `${PLUGIN_PREFIX}${name}`;
|
|
7
7
|
}
|
|
8
|
+
/** Shorten a plugin name to its non-prefixed form. */
|
|
9
|
+
export function shortenPluginName(name) {
|
|
10
|
+
return name.startsWith(PLUGIN_PREFIX) ? name.slice(PLUGIN_PREFIX.length) : name;
|
|
11
|
+
}
|
|
@@ -126,6 +126,33 @@ function resolveEdge(nodesByType, targetIndex, edge, source, selfRef, unresolved
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Classify a ContentLink into a ResolvedContentLink by determining its location.
|
|
131
|
+
*
|
|
132
|
+
* Wikilinks are resolved against the target index: if found, location is 'node',
|
|
133
|
+
* otherwise 'internal' (in the source system but not a space node).
|
|
134
|
+
* Markdown links are classified by URL scheme: external (http/https), system (file://),
|
|
135
|
+
* protocol (other scheme), or internal (relative path / no scheme).
|
|
136
|
+
*/
|
|
137
|
+
function classifyContentLink(link, targetIndex) {
|
|
138
|
+
const { linkSyntax, target, text, action, anchor } = link;
|
|
139
|
+
const base = { text, target, action, ...(anchor !== undefined ? { anchor } : {}) };
|
|
140
|
+
if (linkSyntax === 'wikilink') {
|
|
141
|
+
// Try the full target#anchor key first, then the bare target — either means it's a space node.
|
|
142
|
+
const lookupKey = anchor !== undefined ? `${target}#${anchor}` : target;
|
|
143
|
+
const inIndex = targetIndex.has(lookupKey) || (anchor !== undefined && targetIndex.has(target));
|
|
144
|
+
return { ...base, location: inIndex ? 'node' : 'internal' };
|
|
145
|
+
}
|
|
146
|
+
// Markdown link: classify by URL scheme.
|
|
147
|
+
const colonIdx = target.indexOf('://');
|
|
148
|
+
if (colonIdx > 0) {
|
|
149
|
+
const scheme = target.slice(0, colonIdx).toLowerCase();
|
|
150
|
+
const location = scheme === 'https' || scheme === 'http' ? 'external' : scheme === 'file' ? 'system' : 'protocol';
|
|
151
|
+
return { ...base, location };
|
|
152
|
+
}
|
|
153
|
+
// Relative path or fragment — in the source system but not a resolvable space node.
|
|
154
|
+
return { ...base, location: 'internal' };
|
|
155
|
+
}
|
|
129
156
|
/**
|
|
130
157
|
* Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
|
|
131
158
|
* parent links using the hierarchy levels and relationships from schema metadata.
|
|
@@ -141,6 +168,7 @@ export function resolveGraphEdges(nodes, metadata) {
|
|
|
141
168
|
...n,
|
|
142
169
|
resolvedType: resolveNodeType(n.type, typeAliases),
|
|
143
170
|
resolvedParents: [],
|
|
171
|
+
resolvedLinks: [],
|
|
144
172
|
}));
|
|
145
173
|
const targetIndex = buildTargetIndex(spaceNodes);
|
|
146
174
|
// Build nodesByType map
|
|
@@ -186,6 +214,10 @@ export function resolveGraphEdges(nodes, metadata) {
|
|
|
186
214
|
};
|
|
187
215
|
resolveEdge(nodesByType, targetIndex, edge, 'relationship', rel.type === rel.parent, unresolvedRefs, typeAliases);
|
|
188
216
|
}
|
|
217
|
+
// 3. Resolve content links — classify each raw ContentLink into a ResolvedContentLink.
|
|
218
|
+
for (const node of spaceNodes) {
|
|
219
|
+
node.resolvedLinks = (node.contentLinks ?? []).map((link) => classifyContentLink(link, targetIndex));
|
|
220
|
+
}
|
|
189
221
|
// Deduplicate by (label, field, ref) — the same broken link may be encountered across
|
|
190
222
|
// multiple resolveEdge calls (e.g. selfRef + regular hierarchy share the same field).
|
|
191
223
|
const seen = new Set();
|
package/dist/render/render.d.ts
CHANGED
package/dist/render/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { shortenPluginName } from '@/plugins/util';
|
|
2
|
+
import { updateSpaceField } from '../config';
|
|
3
|
+
import { assembleSpaceGraph } from '../load-space-graph';
|
|
2
4
|
import { loadPlugins } from '../plugins/loader';
|
|
3
|
-
import { readSpace } from '../read/read-space';
|
|
4
|
-
import { buildSpaceGraph } from '../space-graph';
|
|
5
5
|
import { buildFormatRegistry } from './registry';
|
|
6
6
|
export async function executeRender(formatName, context, options) {
|
|
7
7
|
const pluginMap = context.space?.plugins ?? {};
|
|
@@ -12,17 +12,21 @@ export async function executeRender(formatName, context, options) {
|
|
|
12
12
|
const available = registry.map((r) => r.qualifiedName).join(', ');
|
|
13
13
|
throw new Error(`Unknown render format: "${formatName}".${available ? ` Available: ${available}` : ' No formats registered.'}`);
|
|
14
14
|
}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
const graph = await assembleSpaceGraph(context, { filter: options.filter });
|
|
16
|
+
const shortName = shortenPluginName(entry.plugin.plugin.name);
|
|
17
|
+
const pluginContext = {
|
|
18
|
+
...context,
|
|
19
|
+
pluginConfig: entry.plugin.pluginConfig,
|
|
20
|
+
callbacks: {
|
|
21
|
+
persistConfig: (updates) => {
|
|
22
|
+
for (const [field, value] of Object.entries(updates)) {
|
|
23
|
+
updateSpaceField(context.space.name, field, value, shortName);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return entry.plugin.plugin.render.render(pluginContext, graph, {
|
|
29
|
+
format: entry.format.name,
|
|
30
|
+
data: options.data,
|
|
31
|
+
});
|
|
28
32
|
}
|
|
@@ -52,6 +52,7 @@ function flattenNode(node) {
|
|
|
52
52
|
resolvedType: node.resolvedType,
|
|
53
53
|
resolvedParentTitle: node.resolvedParents[0]?.title, // first parent or undefined, provided for convenience
|
|
54
54
|
resolvedParentTitles: node.resolvedParents.map((r) => r.title), // full array of parent titles
|
|
55
|
+
resolvedLinks: node.resolvedLinks,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
/**
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AnySchemaObject, type ValidateFunction } from 'ajv';
|
|
2
2
|
import type { SchemaMetadata, SchemaWithMetadata } from '../types';
|
|
3
|
-
export declare
|
|
3
|
+
export declare let bundledSchemasDir: string;
|
|
4
|
+
/** Override the bundled schemas directory (e.g. when running in a bundled context). */
|
|
5
|
+
export declare function setBundledSchemasDir(dir: string): void;
|
|
4
6
|
export declare function readRawSchema(schemaPath: string): AnySchemaObject;
|
|
5
7
|
/**
|
|
6
8
|
* Build the full two-layer registry for a schema path:
|
package/dist/schema/schema.js
CHANGED
|
@@ -3,11 +3,15 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
5
|
import Ajv, {} from 'ajv';
|
|
6
|
-
import
|
|
6
|
+
import JSON5 from 'json5';
|
|
7
7
|
import { DIALECT_META_SCHEMA, METADATA_SCHEMA, SCHEMA_META_ID, } from './metadata-contract';
|
|
8
8
|
import { isObject, mergeVariantProperties, resolveJsonPointer } from './schema-refs';
|
|
9
|
-
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
export
|
|
9
|
+
const packageDir = import.meta.url ? dirname(fileURLToPath(import.meta.url)) : '';
|
|
10
|
+
export let bundledSchemasDir = packageDir ? join(packageDir, '..', '..', 'schemas') : '';
|
|
11
|
+
/** Override the bundled schemas directory (e.g. when running in a bundled context). */
|
|
12
|
+
export function setBundledSchemasDir(dir) {
|
|
13
|
+
bundledSchemasDir = dir;
|
|
14
|
+
}
|
|
11
15
|
const validateMetadataContract = new Ajv().compile(METADATA_SCHEMA);
|
|
12
16
|
export function readRawSchema(schemaPath) {
|
|
13
17
|
return JSON5.parse(readFileSync(resolve(schemaPath), 'utf-8'));
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
},
|
|
208
208
|
"note": {
|
|
209
209
|
"type": "object",
|
|
210
|
-
"description": "Personal notes, journal entries,
|
|
210
|
+
"description": "Personal notes, journal entries, talk notes, or rough thoughts not drawn from an external URL.",
|
|
211
211
|
"allOf": [{ "$ref": "sctx://_sctx_base#/$defs/baseNodeProps" }],
|
|
212
212
|
"properties": {
|
|
213
213
|
"type": { "const": "note" },
|