@atolis-hq/corum 0.1.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.
Files changed (39) hide show
  1. package/README.md +223 -0
  2. package/dist/src/adapters/index.js +12 -0
  3. package/dist/src/adapters/openapi/index.js +12 -0
  4. package/dist/src/adapters/openapi/mapper.js +218 -0
  5. package/dist/src/adapters/openapi/parser.js +16 -0
  6. package/dist/src/bin/corum.js +164 -0
  7. package/dist/src/cli.js +20 -0
  8. package/dist/src/graph/index.js +128 -0
  9. package/dist/src/graph/overlay.js +136 -0
  10. package/dist/src/import/config.js +39 -0
  11. package/dist/src/import/runner.js +56 -0
  12. package/dist/src/loader/cluster-loader.js +120 -0
  13. package/dist/src/loader/constants.js +32 -0
  14. package/dist/src/loader/edge-loader.js +59 -0
  15. package/dist/src/loader/fs-utils.js +20 -0
  16. package/dist/src/loader/index.js +108 -0
  17. package/dist/src/loader/pack-loader.js +99 -0
  18. package/dist/src/mcp/index.js +333 -0
  19. package/dist/src/mcp/serializers.js +68 -0
  20. package/dist/src/openapi-to-api-endpoints.js +240 -0
  21. package/dist/src/reconcile/index.js +46 -0
  22. package/dist/src/schema/index.js +16 -0
  23. package/dist/src/source/config-file.js +22 -0
  24. package/dist/src/source/config.js +71 -0
  25. package/dist/src/source/content-utils.js +13 -0
  26. package/dist/src/source/file-source.js +135 -0
  27. package/dist/src/source/git-cache.js +54 -0
  28. package/dist/src/source/git-source.js +333 -0
  29. package/dist/src/source/index.js +8 -0
  30. package/dist/src/web/server.js +557 -0
  31. package/dist/src/writer/graph-writer.js +153 -0
  32. package/package.json +36 -0
  33. package/web/app.jsx +668 -0
  34. package/web/favicon.svg +19 -0
  35. package/web/index.html +41 -0
  36. package/web/nav.js +141 -0
  37. package/web/primitives.jsx +583 -0
  38. package/web/router.js +49 -0
  39. package/web/style.css +827 -0
@@ -0,0 +1,108 @@
1
+ import { FileGraphSource } from '../source/file-source.js';
2
+ import { SourceError } from '../source/index.js';
3
+ import { LoadError, QueryError } from '../schema/index.js';
4
+ import { computeDiff, computeOverlay } from '../graph/overlay.js';
5
+ import { loadClusters } from './cluster-loader.js';
6
+ import { loadEdges } from './edge-loader.js';
7
+ import { loadPacks } from './pack-loader.js';
8
+ export async function loadGraph(options) {
9
+ const { strict = true } = options;
10
+ const diagnostics = [];
11
+ const source = options.source ?? new FileGraphSource({
12
+ graphDir: options.graphPath,
13
+ packsPath: options.packsPath,
14
+ });
15
+ const defaultRef = await source.defaultBranch();
16
+ const ref = options.ref ?? defaultRef;
17
+ const packContent = await source.loadPackContent(defaultRef);
18
+ const templates = loadPacks(packContent, diagnostics);
19
+ const graphContent = await source.loadGraphContent(ref);
20
+ const clusterResult = loadClusters(graphContent, templates, diagnostics);
21
+ const edgeResult = loadEdges(graphContent, clusterResult.nodes, diagnostics);
22
+ const edgesByFrom = cloneEdgeMap(clusterResult.edgesByFrom);
23
+ const edgesByTo = cloneEdgeMap(clusterResult.edgesByTo);
24
+ mergeEdgeMaps(edgesByFrom, edgeResult.edgesByFrom);
25
+ mergeEdgeMaps(edgesByTo, edgeResult.edgesByTo);
26
+ const graph = {
27
+ nodesById: clusterResult.nodes,
28
+ edgesByFrom,
29
+ edgesByTo,
30
+ templates,
31
+ diagnostics,
32
+ sourceContent: graphContent,
33
+ };
34
+ if (strict && diagnostics.some(d => d.severity === 'error')) {
35
+ throw new LoadError(diagnostics);
36
+ }
37
+ return graph;
38
+ }
39
+ export async function loadMultiGraph(options) {
40
+ const { source, strict = true } = options;
41
+ const defaultRef = await source.defaultBranch();
42
+ let defaultBranch;
43
+ try {
44
+ defaultBranch = {
45
+ ref: defaultRef,
46
+ isDefault: true,
47
+ graph: await loadGraph({ source, ref: defaultRef, strict }),
48
+ };
49
+ }
50
+ catch (err) {
51
+ throw new SourceError(`failed to load default branch '${defaultRef}'`, err);
52
+ }
53
+ const requestedBranches = options.branches ?? await source.listBranches();
54
+ const nonDefaultRefs = requestedBranches.filter(ref => ref !== defaultRef);
55
+ const settledBranches = await Promise.allSettled(nonDefaultRefs.map(async (ref) => ({
56
+ ref,
57
+ isDefault: false,
58
+ graph: await loadGraph({ source, ref, strict }),
59
+ })));
60
+ const branches = [defaultBranch];
61
+ const branchResults = [{ ref: defaultRef, status: 'loaded' }];
62
+ for (let index = 0; index < settledBranches.length; index++) {
63
+ const result = settledBranches[index];
64
+ const ref = nonDefaultRefs[index];
65
+ if (result.status === 'fulfilled') {
66
+ branches.push(result.value);
67
+ branchResults.push({ ref, status: 'loaded' });
68
+ }
69
+ else {
70
+ const diagnostics = result.reason instanceof LoadError ? result.reason.diagnostics : undefined;
71
+ branchResults.push({
72
+ ref,
73
+ status: 'failed',
74
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
75
+ diagnostics,
76
+ });
77
+ }
78
+ }
79
+ const overlayCache = new Map();
80
+ return {
81
+ default: defaultBranch,
82
+ branches,
83
+ branchResults,
84
+ overlay(viewingRef) {
85
+ let overlay = overlayCache.get(viewingRef);
86
+ if (!overlay) {
87
+ overlay = computeOverlay(viewingRef, defaultBranch, branches);
88
+ overlayCache.set(viewingRef, overlay);
89
+ }
90
+ return overlay;
91
+ },
92
+ diff(branchRef) {
93
+ const branch = branches.find(item => item.ref === branchRef);
94
+ if (!branch)
95
+ throw new QueryError(`branch '${branchRef}' not found or failed to load`);
96
+ return computeDiff(branch, defaultBranch);
97
+ },
98
+ };
99
+ }
100
+ function cloneEdgeMap(source) {
101
+ return new Map([...source.entries()].map(([key, edges]) => [key, [...edges]]));
102
+ }
103
+ function mergeEdgeMaps(target, source) {
104
+ for (const [key, edges] of source) {
105
+ const existing = target.get(key) ?? [];
106
+ target.set(key, [...existing, ...edges]);
107
+ }
108
+ }
@@ -0,0 +1,99 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { listYamlKeys, readYaml } from '../source/content-utils.js';
3
+ function topoSortTemplates(templates) {
4
+ const sorted = [];
5
+ const visited = new Set();
6
+ function visit(name) {
7
+ if (visited.has(name))
8
+ return;
9
+ visited.add(name);
10
+ const t = templates.get(name);
11
+ if (!t)
12
+ return;
13
+ if (t.extends)
14
+ visit(t.extends);
15
+ sorted.push(t);
16
+ }
17
+ for (const name of templates.keys())
18
+ visit(name);
19
+ return sorted;
20
+ }
21
+ const RESERVED_TEMPLATE_KEYS = new Set([
22
+ 'name', 'info', 'extends', 'properties', 'edge-types', 'ui',
23
+ ]);
24
+ export function getOwnedSections(template) {
25
+ const result = {};
26
+ for (const [key, value] of Object.entries(template)) {
27
+ if (!RESERVED_TEMPLATE_KEYS.has(key) &&
28
+ typeof value === 'object' &&
29
+ value !== null &&
30
+ 'item-template' in value &&
31
+ typeof value['item-template'] === 'string') {
32
+ result[key] = value['item-template'];
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+ export function loadPacks(content, diagnostics) {
38
+ const templates = new Map();
39
+ let base;
40
+ const templateKeys = listYamlKeys(content, '').filter(key => key.includes('/templates/'));
41
+ for (const key of templateKeys) {
42
+ let raw;
43
+ try {
44
+ raw = parseYaml(readYaml(content, key));
45
+ }
46
+ catch (err) {
47
+ diagnostics.push({ severity: 'error', file: key, message: `failed to parse YAML: ${err}` });
48
+ continue;
49
+ }
50
+ const templateRecord = raw;
51
+ const info = typeof templateRecord.info === 'object' && templateRecord.info !== null
52
+ ? templateRecord.info
53
+ : null;
54
+ if (typeof templateRecord.name !== 'string' || typeof info?.version !== 'string') {
55
+ diagnostics.push({ severity: 'error', file: key, message: 'template missing required name or info.version' });
56
+ continue;
57
+ }
58
+ const template = templateRecord;
59
+ if (template.name === 'base') {
60
+ base = template;
61
+ }
62
+ else {
63
+ templates.set(template.name, template);
64
+ }
65
+ }
66
+ if (base) {
67
+ for (const template of templates.values()) {
68
+ inheritNonReserved(template, base);
69
+ }
70
+ }
71
+ for (const template of topoSortTemplates(templates)) {
72
+ if (!template.extends)
73
+ continue;
74
+ const parent = templates.get(template.extends);
75
+ if (!parent) {
76
+ diagnostics.push({
77
+ severity: 'error',
78
+ file: `template:${template.name}`,
79
+ message: `extends references unknown template: ${template.extends}`,
80
+ });
81
+ continue;
82
+ }
83
+ if (parent.properties && template.properties) {
84
+ template.properties = { allOf: [parent.properties, template.properties] };
85
+ }
86
+ else if (parent.properties) {
87
+ template.properties = parent.properties;
88
+ }
89
+ inheritNonReserved(template, parent);
90
+ }
91
+ return templates;
92
+ }
93
+ function inheritNonReserved(template, parent) {
94
+ for (const [key, value] of Object.entries(parent)) {
95
+ if (!RESERVED_TEMPLATE_KEYS.has(key) && !(key in template)) {
96
+ template[key] = value;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,333 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { computeClusterOverlay, getCluster, getLinkedFields, listNodes } from '../graph/index.js';
6
+ import { loadGraph, loadMultiGraph } from '../loader/index.js';
7
+ import { QueryError } from '../schema/index.js';
8
+ import { createGraphRuntimeConfig } from '../source/config.js';
9
+ import { startGraphFileWatcher, startWebServer } from '../web/server.js';
10
+ import { compactKeys, getSerializer } from './serializers.js';
11
+ export function createMcpHandlers(graph, source, cache) {
12
+ const resolveMulti = (src) => cache ? cache.get() : loadMultiGraph({ source: src });
13
+ return {
14
+ list_nodes(args) {
15
+ const run = (targetGraph) => {
16
+ const filter = {
17
+ template: typeof args.template === 'string' ? args.template : undefined,
18
+ component: typeof args.component === 'string' ? args.component : undefined,
19
+ state: typeof args.state === 'string' ? args.state : undefined,
20
+ stability: typeof args.stability === 'string' ? args.stability : undefined,
21
+ };
22
+ const summaries = listNodes(targetGraph, filter).map(node => ({
23
+ id: node.id,
24
+ template: node.template,
25
+ component: node.component,
26
+ state: node.state,
27
+ stability: node.stability,
28
+ }));
29
+ return formatResult(summaries, args.format, getCompactKeys(args));
30
+ };
31
+ if (hasBranch(args)) {
32
+ return withBranchGraph(source, String(args.branch), branch => run(branch.graph));
33
+ }
34
+ try {
35
+ return run(graph);
36
+ }
37
+ catch (err) {
38
+ return errorResult(err);
39
+ }
40
+ },
41
+ list_templates(args) {
42
+ const run = (targetGraph) => {
43
+ const summaries = [...targetGraph.templates.values()]
44
+ .map(template => ({
45
+ name: template.name,
46
+ version: template.info?.version,
47
+ core: template.info?.core ?? false,
48
+ abstract: template.info?.abstract ?? false,
49
+ extends: template.extends,
50
+ description: template.info?.description,
51
+ }))
52
+ .sort((a, b) => a.name.localeCompare(b.name));
53
+ return formatResult(summaries, args.format, getCompactKeys(args));
54
+ };
55
+ if (hasBranch(args) && source) {
56
+ return withBranchGraph(source, String(args.branch), branch => run(branch.graph));
57
+ }
58
+ try {
59
+ return run(graph);
60
+ }
61
+ catch (err) {
62
+ return errorResult(err);
63
+ }
64
+ },
65
+ get_template(args) {
66
+ try {
67
+ const name = String(args.name);
68
+ const template = graph.templates.get(name);
69
+ if (!template) {
70
+ throw new QueryError(`Template not found: ${name}`);
71
+ }
72
+ return formatResult(template, args.format, getCompactKeys(args));
73
+ }
74
+ catch (err) {
75
+ return errorResult(err);
76
+ }
77
+ },
78
+ get_cluster(args) {
79
+ const overlayRefs = Array.isArray(args.overlay_refs)
80
+ ? args.overlay_refs.filter((ref) => typeof ref === 'string')
81
+ : [];
82
+ const run = async (targetGraph, branchRef) => {
83
+ const cluster = getCluster(targetGraph, String(args.node_id));
84
+ if (overlayRefs.length === 0 || !source || !branchRef) {
85
+ return formatResult(cluster, args.format, getCompactKeys(args));
86
+ }
87
+ const multi = await resolveMulti(source);
88
+ const overlay = computeClusterOverlay(multi, branchRef, overlayRefs, String(args.node_id));
89
+ return formatResult({ ...cluster, overlay }, args.format, getCompactKeys(args));
90
+ };
91
+ const branchRef = hasBranch(args) ? String(args.branch) : undefined;
92
+ if (branchRef) {
93
+ return withBranchGraph(source, branchRef, branch => run(branch.graph, branchRef), cache);
94
+ }
95
+ return run(graph).catch(err => errorResult(err));
96
+ },
97
+ get_linked_fields(args) {
98
+ const run = (targetGraph) => formatResult(getLinkedFields(targetGraph, String(args.node_id)), args.format, getCompactKeys(args));
99
+ if (hasBranch(args)) {
100
+ return withBranchGraph(source, String(args.branch), branch => run(branch.graph));
101
+ }
102
+ try {
103
+ return run(graph);
104
+ }
105
+ catch (err) {
106
+ return errorResult(err);
107
+ }
108
+ },
109
+ async list_branches(args) {
110
+ try {
111
+ if (!source)
112
+ throw new QueryError('GraphSource is required for list_branches');
113
+ const multi = await resolveMulti(source);
114
+ const summaries = multi.branchResults.map(result => ({
115
+ ref: result.ref,
116
+ status: result.status,
117
+ error: result.error,
118
+ isDefault: result.ref === multi.default.ref,
119
+ }));
120
+ return formatResult(summaries, args.format, getCompactKeys(args));
121
+ }
122
+ catch (err) {
123
+ return errorResult(err);
124
+ }
125
+ },
126
+ async diff_branch(args) {
127
+ try {
128
+ if (!source)
129
+ throw new QueryError('GraphSource is required for diff_branch');
130
+ if (typeof args.branch !== 'string' || args.branch.length === 0) {
131
+ throw new QueryError('branch is required');
132
+ }
133
+ const multi = await resolveMulti(source);
134
+ return formatResult(multi.diff(args.branch), args.format, getCompactKeys(args));
135
+ }
136
+ catch (err) {
137
+ return errorResult(err);
138
+ }
139
+ },
140
+ };
141
+ }
142
+ async function withBranchGraph(source, branchRef, fn, cache) {
143
+ try {
144
+ if (!source)
145
+ throw new QueryError('GraphSource is required when branch is provided');
146
+ const multi = cache ? await cache.get() : await loadMultiGraph({ source });
147
+ const branch = multi.branches.find(item => item.ref === branchRef);
148
+ if (!branch)
149
+ throw new QueryError(`branch '${branchRef}' not found or failed to load`);
150
+ return fn(branch);
151
+ }
152
+ catch (err) {
153
+ return errorResult(err);
154
+ }
155
+ }
156
+ function hasBranch(args) {
157
+ return typeof args.branch === 'string' && args.branch.length > 0;
158
+ }
159
+ function formatResult(value, format, compact = false) {
160
+ const payload = compact === true ? compactKeys(value) : value;
161
+ return { content: [{ type: 'text', text: getSerializer(format).serialize(payload) }] };
162
+ }
163
+ function getCompactKeys(args) {
164
+ return args.compact_keys === true || args.compactKeys === true;
165
+ }
166
+ function errorResult(err) {
167
+ const message = err instanceof QueryError ? err.message : String(err);
168
+ return { content: [{ type: 'text', text: message }], isError: true };
169
+ }
170
+ export async function startMcpServer(options = {}) {
171
+ const { noWeb = false, watch = false } = options;
172
+ const config = createGraphRuntimeConfig();
173
+ let graph;
174
+ let loadError;
175
+ try {
176
+ graph = await loadGraph({ source: config.source, strict: true });
177
+ }
178
+ catch (err) {
179
+ loadError = String(err);
180
+ graph = {
181
+ nodesById: new Map(),
182
+ edgesByFrom: new Map(),
183
+ edgesByTo: new Map(),
184
+ templates: new Map(),
185
+ diagnostics: [],
186
+ };
187
+ }
188
+ const handlers = createMcpHandlers(graph, config.source);
189
+ if (!noWeb) {
190
+ await startWebServer(graph, {
191
+ graphPath: config.graphPath,
192
+ fileWatcher: config.fileWatcherGraphPath && watch ? true : undefined,
193
+ source: config.source,
194
+ });
195
+ }
196
+ else if (watch && config.fileWatcherGraphPath) {
197
+ startGraphFileWatcher(graph, { graphPath: config.fileWatcherGraphPath });
198
+ }
199
+ const server = new Server({ name: 'corum', version: '0.1.0' }, { capabilities: { tools: {} } });
200
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
201
+ tools: [
202
+ {
203
+ name: 'list_nodes',
204
+ description: 'List nodes in the graph. Returns id, template, component, state, stability for each matched node.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: {
208
+ template: { type: 'string', description: 'Filter by template name' },
209
+ component: { type: 'string', description: 'Filter by component name' },
210
+ state: { type: 'string', description: 'Filter by lifecycle state' },
211
+ stability: { type: 'string', description: 'Filter by stability' },
212
+ branch: { type: 'string', description: 'Branch ref to load nodes from' },
213
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
214
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
215
+ },
216
+ },
217
+ },
218
+ {
219
+ name: 'list_templates',
220
+ description: 'List loaded graph templates. Returns name, version, core, abstract, extends, and description for each template.',
221
+ inputSchema: {
222
+ type: 'object',
223
+ properties: {
224
+ branch: { type: 'string', description: 'Branch ref to load templates from' },
225
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
226
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
227
+ },
228
+ },
229
+ },
230
+ {
231
+ name: 'get_template',
232
+ description: 'Get full details for a loaded graph template.',
233
+ inputSchema: {
234
+ type: 'object',
235
+ required: ['name'],
236
+ properties: {
237
+ name: { type: 'string', description: 'Template name' },
238
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
239
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
240
+ },
241
+ },
242
+ },
243
+ {
244
+ name: 'get_cluster',
245
+ description: 'Get the full cluster for a root node.',
246
+ inputSchema: {
247
+ type: 'object',
248
+ required: ['node_id'],
249
+ properties: {
250
+ node_id: { type: 'string', description: 'Fully qualified node ID' },
251
+ branch: { type: 'string', description: 'Branch ref to load the cluster from' },
252
+ overlay_refs: {
253
+ type: 'array',
254
+ items: { type: 'string' },
255
+ description: 'Branch refs to overlay. Returns ghost field data alongside the cluster.',
256
+ },
257
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
258
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
259
+ },
260
+ },
261
+ },
262
+ {
263
+ name: 'get_linked_fields',
264
+ description: 'Get all maps-to edges touching fields owned by the given root node.',
265
+ inputSchema: {
266
+ type: 'object',
267
+ required: ['node_id'],
268
+ properties: {
269
+ node_id: { type: 'string', description: 'Fully qualified root node ID' },
270
+ branch: { type: 'string', description: 'Branch ref to load linked fields from' },
271
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
272
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
273
+ },
274
+ },
275
+ },
276
+ {
277
+ name: 'list_branches',
278
+ description: 'List branches available from the configured graph source and their load status.',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
283
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
284
+ },
285
+ },
286
+ },
287
+ {
288
+ name: 'diff_branch',
289
+ description: 'Diff a branch against the default branch.',
290
+ inputSchema: {
291
+ type: 'object',
292
+ required: ['branch'],
293
+ properties: {
294
+ branch: { type: 'string', description: 'Branch ref to diff against the default branch' },
295
+ format: { type: 'string', enum: ['yaml', 'json', 'toon'], description: 'Output format. Defaults to yaml.' },
296
+ compact_keys: { type: 'boolean', description: 'Use compact graph keys in the selected output format.' },
297
+ },
298
+ },
299
+ },
300
+ ],
301
+ }));
302
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
303
+ if (loadError) {
304
+ return { content: [{ type: 'text', text: `Graph load error: ${loadError}` }], isError: true };
305
+ }
306
+ const args = (request.params.arguments ?? {});
307
+ switch (request.params.name) {
308
+ case 'list_nodes':
309
+ return await handlers.list_nodes(args);
310
+ case 'list_templates':
311
+ return await handlers.list_templates(args);
312
+ case 'get_template':
313
+ return await handlers.get_template(args);
314
+ case 'get_cluster':
315
+ return await handlers.get_cluster(args);
316
+ case 'get_linked_fields':
317
+ return await handlers.get_linked_fields(args);
318
+ case 'list_branches':
319
+ return await handlers.list_branches(args);
320
+ case 'diff_branch':
321
+ return await handlers.diff_branch(args);
322
+ default:
323
+ return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true };
324
+ }
325
+ });
326
+ await server.connect(new StdioServerTransport());
327
+ }
328
+ function isEntrypoint() {
329
+ return process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
330
+ }
331
+ if (isEntrypoint()) {
332
+ await startMcpServer();
333
+ }
@@ -0,0 +1,68 @@
1
+ import { encode as encodeToon } from '@toon-format/toon';
2
+ import { stringify as stringifyYaml } from 'yaml';
3
+ import { QueryError } from '../schema/index.js';
4
+ export class JsonSerializer {
5
+ format = 'json';
6
+ serialize(value) {
7
+ return JSON.stringify(value, null, 2);
8
+ }
9
+ }
10
+ export class YamlSerializer {
11
+ format = 'yaml';
12
+ serialize(value) {
13
+ return stringifyYaml(value);
14
+ }
15
+ }
16
+ export class ToonSerializer {
17
+ format = 'toon';
18
+ serialize(value) {
19
+ return encodeToon(value);
20
+ }
21
+ }
22
+ export function getSerializer(format) {
23
+ switch (format ?? 'yaml') {
24
+ case 'json':
25
+ return new JsonSerializer();
26
+ case 'yaml':
27
+ return new YamlSerializer();
28
+ case 'toon':
29
+ return new ToonSerializer();
30
+ default:
31
+ throw new QueryError(`Invalid output format: ${String(format)}. Expected yaml, json, or toon.`);
32
+ }
33
+ }
34
+ const COMPACT_KEY_MAP = {
35
+ id: 'i',
36
+ template: 't',
37
+ component: 'cp',
38
+ state: 's',
39
+ stability: 'st',
40
+ schemaVersion: 'sv',
41
+ lastModifiedAt: 'lm',
42
+ extractedFrom: 'xf',
43
+ properties: 'p',
44
+ root: 'r',
45
+ children: 'ch',
46
+ edges: 'e',
47
+ nodes: 'n',
48
+ from: 'fr',
49
+ to: 'to',
50
+ type: 'ty',
51
+ notes: 'nt',
52
+ version: 'v',
53
+ core: 'c',
54
+ abstract: 'a',
55
+ extends: 'ex',
56
+ };
57
+ export function compactKeys(value) {
58
+ if (Array.isArray(value)) {
59
+ return value.map(item => compactKeys(item));
60
+ }
61
+ if (typeof value !== 'object' || value === null) {
62
+ return value;
63
+ }
64
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
65
+ COMPACT_KEY_MAP[key] ?? key,
66
+ compactKeys(child),
67
+ ]));
68
+ }