structured-context 0.11.0 → 0.13.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 +52 -198
- package/dist/api.d.ts +17 -15
- package/dist/api.js +12 -9
- package/dist/commands/diagram.d.ts +1 -0
- package/dist/commands/diagram.js +7 -1
- package/dist/commands/docs.js +1 -0
- package/dist/commands/dump.d.ts +15 -2
- package/dist/commands/dump.js +18 -4
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/render.d.ts +4 -0
- package/dist/commands/render.js +64 -9
- package/dist/commands/schemas.d.ts +2 -2
- package/dist/commands/schemas.js +133 -22
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +7 -1
- package/dist/commands/spaces.d.ts +3 -1
- package/dist/commands/spaces.js +11 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +24 -6
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +79 -23
- package/dist/config.d.ts +25 -1
- package/dist/config.js +106 -12
- package/dist/filter/augment-nodes.d.ts +3 -1
- package/dist/filter/augment-nodes.js +12 -7
- package/dist/filter/filter-nodes.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +7 -5
- package/dist/load-space-graph.js +19 -8
- package/dist/plugins/loader.js +3 -3
- package/dist/plugins/markdown/extract-content-links.js +8 -29
- package/dist/plugins/markdown/index.js +8 -2
- package/dist/plugins/markdown/parse-embedded.d.ts +8 -0
- package/dist/plugins/markdown/parse-embedded.js +195 -24
- package/dist/plugins/markdown/read-space.js +42 -14
- package/dist/plugins/markdown/render-directory.d.ts +11 -0
- package/dist/plugins/markdown/render-directory.js +130 -0
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/plugins/mermaid/render-mermaid.js +2 -1
- package/dist/plugins/miro/sync.js +7 -6
- package/dist/plugins/util.d.ts +39 -1
- package/dist/plugins/util.js +139 -0
- package/dist/read/resolve-graph-edges.d.ts +1 -1
- package/dist/read/resolve-graph-edges.js +46 -27
- package/dist/read/wikilink-utils.d.ts +17 -0
- package/dist/read/wikilink-utils.js +40 -0
- package/dist/render/render.d.ts +3 -1
- package/dist/render/render.js +2 -2
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/meta-schemas.d.ts +13 -0
- package/dist/schema/meta-schemas.js +50 -0
- package/dist/schema/metadata-contract.d.ts +952 -183
- package/dist/schema/metadata-contract.js +177 -33
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.d.ts +14 -8
- package/dist/schema/schema.js +308 -199
- package/dist/schema/validate-graph.d.ts +2 -1
- package/dist/schema/validate-graph.js +40 -23
- package/dist/schema/validate-rules.d.ts +16 -3
- package/dist/schema/validate-rules.js +91 -29
- package/dist/schemas/_knowledge_wiki.json +24 -24
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +17 -8
- package/dist/schemas/_sctx_base.json +3 -3
- package/dist/schemas/_strategy_general.json +38 -38
- package/dist/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/knowledge_wiki.json +10 -9
- package/dist/schemas/okf.json +8 -0
- package/dist/schemas/strategy_general.json +15 -14
- package/dist/schemas/strict_ost.json +17 -16
- package/dist/space-context.d.ts +5 -2
- package/dist/space-context.js +9 -4
- package/dist/space-graph.d.ts +9 -7
- package/dist/space-graph.js +7 -5
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/types.d.ts +64 -16
- package/dist/validate.d.ts +62 -9
- package/dist/validate.js +229 -114
- package/docs/concepts.md +65 -9
- package/docs/config.md +78 -1
- package/docs/rules.md +168 -50
- package/docs/schemas.md +152 -52
- package/package.json +17 -11
- package/schemas/_knowledge_wiki.json +24 -24
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +17 -8
- package/schemas/_sctx_base.json +3 -3
- package/schemas/_strategy_general.json +38 -38
- package/schemas/generated/0.12.0/_structured_context_schema_meta.json +229 -0
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +400 -0
- package/schemas/knowledge_wiki.json +10 -9
- package/schemas/okf.json +8 -0
- package/schemas/strategy_general.json +15 -14
- package/schemas/strict_ost.json +17 -16
- package/dist/schemas/generated/_structured_context_schema_meta.json +0 -191
- package/schemas/generated/_structured_context_schema_meta.json +0 -191
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { getMarkdownConfig } from '../../index.js';
|
|
4
|
+
import { invertFieldMap, MATTER_OPTIONS } from './util';
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize a node title into a valid filesystem filename.
|
|
7
|
+
* Strips Obsidian/OS forbidden characters: \ / : * ? " < > | # ^ [ ]
|
|
8
|
+
*/
|
|
9
|
+
export function sanitizeFilename(title) {
|
|
10
|
+
const sanitized = title
|
|
11
|
+
.replace(/[\\/:*?"<>|#^[\]]/g, '-')
|
|
12
|
+
.replace(/\s+/g, ' ')
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^[-.]+/, '')
|
|
15
|
+
.replace(/[-.]+$/, '');
|
|
16
|
+
return sanitized || 'untitled';
|
|
17
|
+
}
|
|
18
|
+
const PREFERRED_FIELD_ORDER = ['type', 'title', 'status', 'summary', 'status_tweet'];
|
|
19
|
+
const EDGE_FIELDS = new Set(['parent', 'dependsOn', 'flowsTo', 'anchors', 'pipelineOf']);
|
|
20
|
+
/**
|
|
21
|
+
* Format frontmatter object with preferred key ordering for Obsidian notes.
|
|
22
|
+
*/
|
|
23
|
+
function sortFrontmatterKeys(data) {
|
|
24
|
+
const keys = Object.keys(data);
|
|
25
|
+
const preferred = [];
|
|
26
|
+
const edge = [];
|
|
27
|
+
const middle = [];
|
|
28
|
+
for (const k of PREFERRED_FIELD_ORDER) {
|
|
29
|
+
if (keys.includes(k))
|
|
30
|
+
preferred.push(k);
|
|
31
|
+
}
|
|
32
|
+
for (const k of keys) {
|
|
33
|
+
if (EDGE_FIELDS.has(k)) {
|
|
34
|
+
edge.push(k);
|
|
35
|
+
}
|
|
36
|
+
else if (!PREFERRED_FIELD_ORDER.includes(k)) {
|
|
37
|
+
middle.push(k);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const sorted = {};
|
|
41
|
+
for (const k of [...preferred, ...middle, ...edge]) {
|
|
42
|
+
sorted[k] = data[k];
|
|
43
|
+
}
|
|
44
|
+
return sorted;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render a space graph as a markdown directory with one file per node.
|
|
48
|
+
*/
|
|
49
|
+
export function renderMarkdownDirectory(context, graph, options) {
|
|
50
|
+
const markdownConfig = getMarkdownConfig(context.space?.plugins);
|
|
51
|
+
const reverseFieldMap = markdownConfig.fieldMap ? invertFieldMap(markdownConfig.fieldMap) : {};
|
|
52
|
+
const foldersOption = options.data?.folders;
|
|
53
|
+
let useFolders = false;
|
|
54
|
+
if (foldersOption === 'type') {
|
|
55
|
+
useFolders = true;
|
|
56
|
+
}
|
|
57
|
+
else if (foldersOption === 'flat') {
|
|
58
|
+
useFolders = false;
|
|
59
|
+
}
|
|
60
|
+
else if (markdownConfig.typeInference?.folderMap) {
|
|
61
|
+
useFolders = true;
|
|
62
|
+
}
|
|
63
|
+
// Build type-to-folder mapping from typeInference.folderMap if available
|
|
64
|
+
const folderMap = markdownConfig.typeInference?.folderMap;
|
|
65
|
+
const typeToFolder = new Map();
|
|
66
|
+
if (folderMap) {
|
|
67
|
+
for (const [folderPath, mappedType] of Object.entries(folderMap)) {
|
|
68
|
+
const cleanFolder = folderPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
69
|
+
typeToFolder.set(mappedType.toLowerCase(), cleanFolder);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const files = [];
|
|
73
|
+
const usedPaths = new Set();
|
|
74
|
+
for (const node of graph.nodes.values()) {
|
|
75
|
+
const baseName = sanitizeFilename(node.title);
|
|
76
|
+
let folder = '';
|
|
77
|
+
if (useFolders) {
|
|
78
|
+
const typeKey = (node.resolvedType || node.type || '').toLowerCase();
|
|
79
|
+
folder = typeToFolder.get(typeKey) ?? (node.resolvedType || node.type || 'other');
|
|
80
|
+
}
|
|
81
|
+
let candidateName = `${baseName}.md`;
|
|
82
|
+
let counter = 2;
|
|
83
|
+
let relPath = folder ? posix.join(folder, candidateName) : candidateName;
|
|
84
|
+
while (usedPaths.has(relPath)) {
|
|
85
|
+
candidateName = `${baseName}-${counter}.md`;
|
|
86
|
+
relPath = folder ? posix.join(folder, candidateName) : candidateName;
|
|
87
|
+
counter++;
|
|
88
|
+
}
|
|
89
|
+
usedPaths.add(relPath);
|
|
90
|
+
const finalBase = candidateName.slice(0, -3);
|
|
91
|
+
// Prepare frontmatter data from schemaData with reverse fieldMap
|
|
92
|
+
const rawData = node.schemaData ?? {};
|
|
93
|
+
const frontmatterData = {};
|
|
94
|
+
for (const [key, value] of Object.entries(rawData)) {
|
|
95
|
+
// Body content is not written to frontmatter
|
|
96
|
+
if (key === 'content')
|
|
97
|
+
continue;
|
|
98
|
+
// Omit title only if it matches the final filename base
|
|
99
|
+
if (key === 'title' && finalBase === node.title)
|
|
100
|
+
continue;
|
|
101
|
+
const fileKey = reverseFieldMap[key] ?? key;
|
|
102
|
+
frontmatterData[fileKey] = value;
|
|
103
|
+
}
|
|
104
|
+
// Preserve exact title if final filename differed or disambiguation occurred
|
|
105
|
+
if (finalBase !== node.title && !frontmatterData.title) {
|
|
106
|
+
const titleKey = reverseFieldMap.title ?? 'title';
|
|
107
|
+
frontmatterData[titleKey] = node.title;
|
|
108
|
+
}
|
|
109
|
+
// Ensure type is in frontmatter
|
|
110
|
+
const nodeType = node.type || node.resolvedType;
|
|
111
|
+
if (nodeType && !frontmatterData.type) {
|
|
112
|
+
const typeKey = reverseFieldMap.type ?? 'type';
|
|
113
|
+
frontmatterData[typeKey] = nodeType;
|
|
114
|
+
}
|
|
115
|
+
const sortedData = sortFrontmatterKeys(frontmatterData);
|
|
116
|
+
const body = (node.content ?? '').trim();
|
|
117
|
+
let fileContent;
|
|
118
|
+
if (Object.keys(sortedData).length > 0) {
|
|
119
|
+
fileContent = matter.stringify(body ? `\n${body}\n` : '', sortedData, MATTER_OPTIONS);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
fileContent = body ? `${body}\n` : '';
|
|
123
|
+
}
|
|
124
|
+
files.push({
|
|
125
|
+
path: relPath,
|
|
126
|
+
content: fileContent,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return { files };
|
|
130
|
+
}
|
|
@@ -2,9 +2,8 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import fg from 'fast-glob';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
6
5
|
import { mergeVariantProperties, resolveRef } from '../../schema/schema-refs';
|
|
7
|
-
import { invertFieldMap } from './util';
|
|
6
|
+
import { dumpYaml, invertFieldMap, MATTER_OPTIONS } from './util';
|
|
8
7
|
// Fields derived from the filesystem — present at validation time but not written to frontmatter
|
|
9
8
|
const DERIVED_FIELDS = new Set(['title', 'content']);
|
|
10
9
|
function enumPlaceholder(def) {
|
|
@@ -76,7 +75,7 @@ export function generateNewContent(nodeType, variant, schema, schemaRefRegistry,
|
|
|
76
75
|
const toCanonicalKey = (k) => fieldMap[k] ?? k;
|
|
77
76
|
const exampleWithPlaceholders = withEnumPlaceholders(example, properties, schema, schemaRefRegistry);
|
|
78
77
|
const remappedExample = Object.fromEntries(Object.entries(exampleWithPlaceholders).map(([k, v]) => [toFileKey(k), v]));
|
|
79
|
-
let frontmatterYaml =
|
|
78
|
+
let frontmatterYaml = dumpYaml(remappedExample, { lineWidth: -1 }).trim();
|
|
80
79
|
// Append property descriptions as comments
|
|
81
80
|
// YAML lines use file field names; look up properties using canonical key.
|
|
82
81
|
const lines = frontmatterYaml.split('\n');
|
|
@@ -206,7 +205,7 @@ export async function templateSync(context, options) {
|
|
|
206
205
|
continue;
|
|
207
206
|
}
|
|
208
207
|
const body = content.slice(fmMatch[0].length);
|
|
209
|
-
const parsed = matter(content);
|
|
208
|
+
const parsed = matter(content, MATTER_OPTIONS);
|
|
210
209
|
const nodeType = parsed.data.type;
|
|
211
210
|
if (!nodeType) {
|
|
212
211
|
continue;
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DumpOptions, type Schema } from 'js-yaml';
|
|
2
|
+
import type { TypeInferenceConfig } from '../../index.js';
|
|
2
3
|
export declare function inferTypeFromPath(filePath: string, config: TypeInferenceConfig, knownTypes: Set<string>, typeAliases: Record<string, string> | undefined): string | undefined;
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* YAML schema for frontmatter and embedded YAML blocks: YAML 1.2 Core schema extended with
|
|
6
|
+
* YAML 1.1 merge keys (`<<`), collections (binary, omap, pairs, set), and custom Date scalar
|
|
7
|
+
* handling that avoids js-yaml's timestamp type. Dates and datetimes stay as the strings
|
|
8
|
+
* written (e.g. `2026-03-31`, `2026-06-20T22:53:05+10:00`) rather than becoming Date objects,
|
|
9
|
+
* which lose precision and offset and are not valid JSON.
|
|
7
10
|
*/
|
|
8
|
-
export declare
|
|
11
|
+
export declare const YAML_SCHEMA: Schema;
|
|
12
|
+
export declare function parseYaml(text: string): unknown;
|
|
13
|
+
export type DumpYamlOptions = DumpOptions & {
|
|
14
|
+
replacer?: (key: string, value: unknown) => unknown;
|
|
15
|
+
};
|
|
16
|
+
/** Dumps with YAML_SCHEMA. Date values (e.g. from non-markdown plugins) are written as ISO 8601 strings. */
|
|
17
|
+
export declare function dumpYaml(data: unknown, options?: DumpYamlOptions): string;
|
|
18
|
+
/**
|
|
19
|
+
* gray-matter options using YAML_SCHEMA. Passing options also bypasses gray-matter's parse cache,
|
|
20
|
+
* which is corrupted by a caught parse error (https://github.com/jonschlinkert/gray-matter/issues/166).
|
|
21
|
+
*/
|
|
22
|
+
export declare const MATTER_OPTIONS: {
|
|
23
|
+
engines: {
|
|
24
|
+
yaml: {
|
|
25
|
+
parse: (text: string) => object;
|
|
26
|
+
stringify: (data: object) => string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
9
30
|
/**
|
|
10
31
|
* Apply field remapping to a data object.
|
|
11
32
|
* Renames keys according to fieldMap (file field name → canonical field name).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
|
+
import { binaryTag, CORE_SCHEMA, defineScalarTag, dump, load, mergeTag, omapTag, pairsTag, setTag, visit, } from 'js-yaml';
|
|
2
3
|
export function inferTypeFromPath(filePath, config, knownTypes, typeAliases) {
|
|
3
4
|
if (config.mode === 'off')
|
|
4
5
|
return undefined;
|
|
@@ -37,26 +38,66 @@ export function inferTypeFromPath(filePath, config, knownTypes, typeAliases) {
|
|
|
37
38
|
}
|
|
38
39
|
return undefined;
|
|
39
40
|
}
|
|
41
|
+
const dateTag = defineScalarTag('tag:yaml.org,2002:timestamp', {
|
|
42
|
+
identify: (data) => data instanceof Date,
|
|
43
|
+
represent: (data) => data.toISOString(),
|
|
44
|
+
resolve: (source) => source,
|
|
45
|
+
});
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* YAML schema for frontmatter and embedded YAML blocks: YAML 1.2 Core schema extended with
|
|
48
|
+
* YAML 1.1 merge keys (`<<`), collections (binary, omap, pairs, set), and custom Date scalar
|
|
49
|
+
* handling that avoids js-yaml's timestamp type. Dates and datetimes stay as the strings
|
|
50
|
+
* written (e.g. `2026-03-31`, `2026-06-20T22:53:05+10:00`) rather than becoming Date objects,
|
|
51
|
+
* which lose precision and offset and are not valid JSON.
|
|
44
52
|
*/
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
export const YAML_SCHEMA = CORE_SCHEMA.withTags(mergeTag, binaryTag, omapTag, pairsTag, setTag, dateTag);
|
|
54
|
+
export function parseYaml(text) {
|
|
55
|
+
return load(text, { schema: YAML_SCHEMA });
|
|
56
|
+
}
|
|
57
|
+
function applyReplacer(key, value, replacer) {
|
|
58
|
+
const replaced = replacer(key, value);
|
|
59
|
+
if (Array.isArray(replaced)) {
|
|
60
|
+
return replaced.map((item, index) => applyReplacer(String(index), item, replacer));
|
|
61
|
+
}
|
|
62
|
+
if (replaced !== null && typeof replaced === 'object' && Object.getPrototypeOf(replaced) === Object.prototype) {
|
|
63
|
+
const res = {};
|
|
64
|
+
for (const [k, v] of Object.entries(replaced)) {
|
|
65
|
+
const vReplaced = applyReplacer(k, v, replacer);
|
|
66
|
+
if (vReplaced !== undefined) {
|
|
67
|
+
res[k] = vReplaced;
|
|
68
|
+
}
|
|
56
69
|
}
|
|
70
|
+
return res;
|
|
57
71
|
}
|
|
58
|
-
return
|
|
72
|
+
return replaced;
|
|
73
|
+
}
|
|
74
|
+
/** Dumps with YAML_SCHEMA. Date values (e.g. from non-markdown plugins) are written as ISO 8601 strings. */
|
|
75
|
+
export function dumpYaml(data, options) {
|
|
76
|
+
const input = options?.replacer ? applyReplacer('', data, options.replacer) : data;
|
|
77
|
+
const { replacer: _ignored, transform, ...dumpOpts } = options ?? {};
|
|
78
|
+
return dump(input, {
|
|
79
|
+
...dumpOpts,
|
|
80
|
+
schema: YAML_SCHEMA,
|
|
81
|
+
transform: (docs) => {
|
|
82
|
+
visit(docs, (node) => {
|
|
83
|
+
if (node.kind === 'scalar' && (node.tag === '!!timestamp' || node.tag === 'tag:yaml.org,2002:timestamp')) {
|
|
84
|
+
node.tagged = false;
|
|
85
|
+
node.tag = 'tag:yaml.org,2002:str';
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
transform?.(docs);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
59
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* gray-matter options using YAML_SCHEMA. Passing options also bypasses gray-matter's parse cache,
|
|
94
|
+
* which is corrupted by a caught parse error (https://github.com/jonschlinkert/gray-matter/issues/166).
|
|
95
|
+
*/
|
|
96
|
+
export const MATTER_OPTIONS = {
|
|
97
|
+
engines: {
|
|
98
|
+
yaml: { parse: (text) => parseYaml(text), stringify: (data) => dumpYaml(data) },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
60
101
|
/**
|
|
61
102
|
* Apply field remapping to a data object.
|
|
62
103
|
* Renames keys according to fieldMap (file field name → canonical field name).
|
|
@@ -15,7 +15,8 @@ function safeNodeId(id) {
|
|
|
15
15
|
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
16
16
|
}
|
|
17
17
|
export function renderMermaid(graph) {
|
|
18
|
-
const { hierarchyRoots, orphans, hierarchyChildren: children, hierarchyTitles: hierarchyNodeSet
|
|
18
|
+
const { hierarchyRoots, orphans, hierarchyChildren: children, hierarchyTitles: hierarchyNodeSet } = graph;
|
|
19
|
+
const levels = graph.hierarchy?.levels ?? [];
|
|
19
20
|
let mmd = 'graph TD\n';
|
|
20
21
|
for (let i = 0; i < levels.length; i++) {
|
|
21
22
|
const level = levels[i];
|
|
@@ -16,6 +16,7 @@ export async function miroSync(context, graph, options) {
|
|
|
16
16
|
if (!configuredFrameId && !options.newFrame)
|
|
17
17
|
throw new Error('No frameId in miro plugin config. Pass --new-frame "Title" to create one.');
|
|
18
18
|
// 3. Filter to hierarchy nodes only (graph already built by caller)
|
|
19
|
+
const levels = graph.hierarchy?.levels ?? [];
|
|
19
20
|
const nodes = [...graph.nodes.values()].filter((n) => graph.hierarchyTitles.has(n.title));
|
|
20
21
|
if (nodes.length === 0) {
|
|
21
22
|
return 'No space nodes found.';
|
|
@@ -28,7 +29,7 @@ export async function miroSync(context, graph, options) {
|
|
|
28
29
|
let layoutOffset = null;
|
|
29
30
|
if (options.newFrame) {
|
|
30
31
|
// Calculate layout bounds to size the frame appropriately
|
|
31
|
-
const { bounds } = layoutNewCards(nodes, new Map(),
|
|
32
|
+
const { bounds } = layoutNewCards(nodes, new Map(), levels);
|
|
32
33
|
const frameWidth = Math.max(1600, bounds.maxX - bounds.minX);
|
|
33
34
|
const frameHeight = Math.max(1200, bounds.maxY - bounds.minY);
|
|
34
35
|
// Miro positions child items relative to the parent frame's top-left corner.
|
|
@@ -155,7 +156,7 @@ export async function miroSync(context, graph, options) {
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
// Compute positions for new cards
|
|
158
|
-
const { positions: newPositions } = layoutNewCards(newNodes, existingPositions,
|
|
159
|
+
const { positions: newPositions } = layoutNewCards(newNodes, existingPositions, levels);
|
|
159
160
|
// 7. Create new cards
|
|
160
161
|
let createdCount = 0;
|
|
161
162
|
for (const node of newNodes) {
|
|
@@ -185,7 +186,7 @@ export async function miroSync(context, graph, options) {
|
|
|
185
186
|
title: buildCardTitle(node),
|
|
186
187
|
description: buildCardDescription(node),
|
|
187
188
|
},
|
|
188
|
-
style: { cardTheme: getCardColor(type,
|
|
189
|
+
style: { cardTheme: getCardColor(type, levels) },
|
|
189
190
|
position: { x: pos.x, y: pos.y, origin: 'center' },
|
|
190
191
|
parent: { id: frameId },
|
|
191
192
|
geometry: { width: CARD_WIDTH },
|
|
@@ -231,7 +232,7 @@ export async function miroSync(context, graph, options) {
|
|
|
231
232
|
title: buildCardTitle(node),
|
|
232
233
|
description: buildCardDescription(node),
|
|
233
234
|
},
|
|
234
|
-
style: { cardTheme: getCardColor(type,
|
|
235
|
+
style: { cardTheme: getCardColor(type, levels) },
|
|
235
236
|
position: { x: 0, y: 0, origin: 'center' },
|
|
236
237
|
parent: { id: frameId },
|
|
237
238
|
geometry: { width: CARD_WIDTH },
|
|
@@ -256,8 +257,8 @@ export async function miroSync(context, graph, options) {
|
|
|
256
257
|
const desiredEdges = new Map();
|
|
257
258
|
for (const node of nodes) {
|
|
258
259
|
const childTitle = node.title;
|
|
259
|
-
for (const { title: parentTitle, source } of node.resolvedParents) {
|
|
260
|
-
if (source !== 'hierarchy')
|
|
260
|
+
for (const { title: parentTitle, source, hierarchy } of node.resolvedParents) {
|
|
261
|
+
if (source !== 'hierarchy' || hierarchy !== graph.hierarchy?.name)
|
|
261
262
|
continue;
|
|
262
263
|
// Both endpoints must have verified cards on the board
|
|
263
264
|
if (verifiedCardIds.has(parentTitle) && verifiedCardIds.has(childTitle)) {
|
package/dist/plugins/util.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AnySchemaObject } from 'ajv';
|
|
2
|
+
import type { Config, SpaceConfig } from '../config';
|
|
3
|
+
import type { PluginSchemaSource } from '../schema/schema';
|
|
2
4
|
import type { SpaceGraph } from '../space-graph';
|
|
3
5
|
import type { BaseNode, ParseIssue, SpaceContext } from '../types';
|
|
4
6
|
export declare const PLUGIN_PREFIX = "sctx-";
|
|
@@ -43,6 +45,21 @@ export type RenderOptions = {
|
|
|
43
45
|
/** Plugin-specific extra data (e.g. CLI flags forwarded by a convenience command). */
|
|
44
46
|
data?: Record<string, unknown>;
|
|
45
47
|
};
|
|
48
|
+
/** A single file generated as part of a multi-file render output. */
|
|
49
|
+
export type RenderFileOutput = {
|
|
50
|
+
/** Relative path of the file within the destination directory (e.g. 'goals/My Goal.md'). */
|
|
51
|
+
path: string;
|
|
52
|
+
/** File content to write. */
|
|
53
|
+
content: string;
|
|
54
|
+
};
|
|
55
|
+
/** Multi-file render output generated by a render hook. */
|
|
56
|
+
export type RenderMultiFileOutput = {
|
|
57
|
+
files: RenderFileOutput[];
|
|
58
|
+
};
|
|
59
|
+
/** Render hook output: either a single string (stdout/single file) or multiple files. */
|
|
60
|
+
export type RenderResult = string | RenderMultiFileOutput;
|
|
61
|
+
/** Type guard for multi-file render result. */
|
|
62
|
+
export declare function isRenderMultiFileOutput(result: RenderResult): result is RenderMultiFileOutput;
|
|
46
63
|
/**
|
|
47
64
|
* The render hook on a plugin: declares available formats and handles rendering.
|
|
48
65
|
* Unlike parse/templateSync (first-responder), render hooks are additive —
|
|
@@ -50,7 +67,7 @@ export type RenderOptions = {
|
|
|
50
67
|
*/
|
|
51
68
|
export type RenderHook = {
|
|
52
69
|
formats: RenderFormat[];
|
|
53
|
-
render: (context: PluginContext, graph: SpaceGraph, options: RenderOptions) => Promise<
|
|
70
|
+
render: (context: PluginContext, graph: SpaceGraph, options: RenderOptions) => Promise<RenderResult> | RenderResult;
|
|
54
71
|
};
|
|
55
72
|
/**
|
|
56
73
|
* Plugin contract:
|
|
@@ -63,7 +80,28 @@ export type StructuredContextPlugin = {
|
|
|
63
80
|
/** JSON Schema used to validate the plugin's config block. Fields with `format: 'path'`
|
|
64
81
|
* are resolved relative to the config directory by `resolveConfigPaths` in the loader. */
|
|
65
82
|
configSchema: AnySchemaObject;
|
|
83
|
+
/** Optional directory containing JSON schemas contributed by this plugin. */
|
|
84
|
+
schemasDir?: string;
|
|
66
85
|
parse?: ParseHook;
|
|
67
86
|
templateSync?: TemplateSyncHook;
|
|
68
87
|
render?: RenderHook;
|
|
69
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Synchronously locate the root directory of an external plugin.
|
|
91
|
+
* Resolution order:
|
|
92
|
+
* 1. Config-adjacent: {configDir}/plugins/{name} or {configDir}/plugins/sctx-{name}
|
|
93
|
+
* 2. Node module: resolve from configDir using standard module resolution
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolvePluginDir(pluginName: string, configDir: string): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Synchronously locate the schemas directory for a plugin.
|
|
98
|
+
* Convention:
|
|
99
|
+
* 1. package.json "sctx": { "schemas": "..." }
|
|
100
|
+
* 2. Convention: <pluginDir>/schemas
|
|
101
|
+
* 3. Fallback for test fixtures: {configDir}/plugins/schemas
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolvePluginSchemasDir(pluginName: string, configDir: string): string | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Synchronously collect all plugin schema sources available to a space.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveSpacePluginSchemas(space: SpaceConfig | undefined, config: Config, configDir: string): PluginSchemaSource[];
|
package/dist/plugins/util.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
1
4
|
import { PLUGIN_PREFIX as _PLUGIN_PREFIX } from '../constants';
|
|
2
5
|
export const PLUGIN_PREFIX = _PLUGIN_PREFIX;
|
|
3
6
|
export const CONFIG_PLUGINS_DIR = 'plugins';
|
|
@@ -9,3 +12,139 @@ export function normalizePluginName(name) {
|
|
|
9
12
|
export function shortenPluginName(name) {
|
|
10
13
|
return name.startsWith(PLUGIN_PREFIX) ? name.slice(PLUGIN_PREFIX.length) : name;
|
|
11
14
|
}
|
|
15
|
+
/** Type guard for multi-file render result. */
|
|
16
|
+
export function isRenderMultiFileOutput(result) {
|
|
17
|
+
return typeof result === 'object' && result !== null && Array.isArray(result.files);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Synchronously locate the root directory of an external plugin.
|
|
21
|
+
* Resolution order:
|
|
22
|
+
* 1. Config-adjacent: {configDir}/plugins/{name} or {configDir}/plugins/sctx-{name}
|
|
23
|
+
* 2. Node module: resolve from configDir using standard module resolution
|
|
24
|
+
*/
|
|
25
|
+
export function resolvePluginDir(pluginName, configDir) {
|
|
26
|
+
const normalized = normalizePluginName(pluginName);
|
|
27
|
+
const short = shortenPluginName(pluginName);
|
|
28
|
+
for (const candidate of [normalized, short]) {
|
|
29
|
+
const localDir = resolve(configDir, CONFIG_PLUGINS_DIR, candidate);
|
|
30
|
+
if (existsSync(localDir)) {
|
|
31
|
+
try {
|
|
32
|
+
if (statSync(localDir).isDirectory()) {
|
|
33
|
+
return localDir;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Node module resolution from configDir
|
|
40
|
+
for (const candidate of [normalized, short]) {
|
|
41
|
+
try {
|
|
42
|
+
const req = createRequire(join(configDir, 'package.json'));
|
|
43
|
+
const pkgPath = req.resolve(`${candidate}/package.json`);
|
|
44
|
+
return dirname(pkgPath);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
const nmCandidate = resolve(configDir, 'node_modules', candidate);
|
|
48
|
+
if (existsSync(nmCandidate)) {
|
|
49
|
+
try {
|
|
50
|
+
if (statSync(nmCandidate).isDirectory()) {
|
|
51
|
+
return nmCandidate;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Synchronously locate the schemas directory for a plugin.
|
|
62
|
+
* Convention:
|
|
63
|
+
* 1. package.json "sctx": { "schemas": "..." }
|
|
64
|
+
* 2. Convention: <pluginDir>/schemas
|
|
65
|
+
* 3. Fallback for test fixtures: {configDir}/plugins/schemas
|
|
66
|
+
*/
|
|
67
|
+
export function resolvePluginSchemasDir(pluginName, configDir) {
|
|
68
|
+
const dir = resolvePluginDir(pluginName, configDir);
|
|
69
|
+
if (dir) {
|
|
70
|
+
// 1. package.json sctx.schemas
|
|
71
|
+
const pkgJsonPath = join(dir, 'package.json');
|
|
72
|
+
if (existsSync(pkgJsonPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
75
|
+
if (pkg.sctx?.schemas && typeof pkg.sctx.schemas === 'string') {
|
|
76
|
+
const customSchemas = resolve(dir, pkg.sctx.schemas);
|
|
77
|
+
if (existsSync(customSchemas) && statSync(customSchemas).isDirectory()) {
|
|
78
|
+
return customSchemas;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
// 2. Convention: <pluginDir>/schemas
|
|
85
|
+
const conventionDir = join(dir, 'schemas');
|
|
86
|
+
if (existsSync(conventionDir)) {
|
|
87
|
+
try {
|
|
88
|
+
if (statSync(conventionDir).isDirectory()) {
|
|
89
|
+
return conventionDir;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 3. Check config-adjacent plugins/schemas as fallback (e.g. test fixtures)
|
|
96
|
+
const fallbackSchemas = resolve(configDir, CONFIG_PLUGINS_DIR, 'schemas');
|
|
97
|
+
if (existsSync(fallbackSchemas)) {
|
|
98
|
+
try {
|
|
99
|
+
if (statSync(fallbackSchemas).isDirectory()) {
|
|
100
|
+
return fallbackSchemas;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Synchronously collect all plugin schema sources available to a space.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveSpacePluginSchemas(space, config, configDir) {
|
|
111
|
+
const pluginSources = [];
|
|
112
|
+
const seenPlugins = new Set();
|
|
113
|
+
const maybeAddPlugin = (name) => {
|
|
114
|
+
const shortName = shortenPluginName(name);
|
|
115
|
+
if (seenPlugins.has(shortName))
|
|
116
|
+
return;
|
|
117
|
+
seenPlugins.add(shortName);
|
|
118
|
+
const schemasDir = resolvePluginSchemasDir(name, configDir);
|
|
119
|
+
if (schemasDir) {
|
|
120
|
+
pluginSources.push({
|
|
121
|
+
pluginName: shortName,
|
|
122
|
+
schemasDir,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// 1. Plugin referenced by space.schema prefix if any (e.g. sctx-wardley-mapping/wardley_map.json)
|
|
127
|
+
const rawSchema = space?.schema ?? config.schema;
|
|
128
|
+
if (rawSchema && !isAbsolute(rawSchema) && !rawSchema.startsWith('.')) {
|
|
129
|
+
const normalized = rawSchema.replace(/\\/g, '/');
|
|
130
|
+
const slashIdx = normalized.indexOf('/');
|
|
131
|
+
if (slashIdx !== -1) {
|
|
132
|
+
const prefix = normalized.slice(0, slashIdx);
|
|
133
|
+
maybeAddPlugin(prefix);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 2. Plugins configured on space
|
|
137
|
+
if (space?.plugins) {
|
|
138
|
+
for (const name of Object.keys(space.plugins)) {
|
|
139
|
+
maybeAddPlugin(name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 3. Plugins configured globally
|
|
143
|
+
const globalPlugins = config.plugins;
|
|
144
|
+
if (globalPlugins) {
|
|
145
|
+
for (const name of Object.keys(globalPlugins)) {
|
|
146
|
+
maybeAddPlugin(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return pluginSources;
|
|
150
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BaseNode, SchemaMetadata, SpaceNode, UnresolvedRef } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Enrich parsed nodes into SpaceNodes by applying type alias resolution and resolving
|
|
4
|
-
* parent links using the hierarchy
|
|
4
|
+
* parent links using the levels of every hierarchy and the relationships from schema metadata.
|
|
5
5
|
*
|
|
6
6
|
* Returns the enriched nodes and any unresolved refs (broken/invalid wikilinks).
|
|
7
7
|
*/
|