@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,128 @@
1
+ import { QueryError } from '../schema/index.js';
2
+ export function listNodes(graph, filter = {}) {
3
+ return [...graph.nodesById.values()].filter(node => {
4
+ if (filter.template !== undefined && node.template !== filter.template)
5
+ return false;
6
+ if (filter.component !== undefined && node.component !== filter.component)
7
+ return false;
8
+ if (filter.state !== undefined && node.state !== filter.state)
9
+ return false;
10
+ if (filter.stability !== undefined && node.stability !== filter.stability)
11
+ return false;
12
+ return true;
13
+ });
14
+ }
15
+ export function getCluster(graph, nodeId) {
16
+ const root = graph.nodesById.get(nodeId);
17
+ if (!root)
18
+ throw new QueryError(`Node not found: ${nodeId}`);
19
+ const prefix = `${nodeId}.`;
20
+ const children = [...graph.nodesById.values()].filter(node => node.id.startsWith(prefix));
21
+ const clusterIds = new Set([nodeId, ...children.map(node => node.id)]);
22
+ const edges = [];
23
+ const seen = new Set();
24
+ for (const id of clusterIds) {
25
+ for (const edge of graph.edgesByFrom.get(id) ?? []) {
26
+ if (clusterIds.has(edge.to) && !seen.has(edge.id)) {
27
+ edges.push(edge);
28
+ seen.add(edge.id);
29
+ }
30
+ }
31
+ }
32
+ return { root, children, edges };
33
+ }
34
+ export function getClusterView(graph, nodeId, includeEdgeTypes = []) {
35
+ const cluster = getCluster(graph, nodeId);
36
+ if (includeEdgeTypes.length === 0) {
37
+ return {
38
+ root: cluster.root,
39
+ descendants: cluster.children,
40
+ includedNodes: [],
41
+ edges: cluster.edges,
42
+ };
43
+ }
44
+ const clusterIds = new Set([cluster.root.id, ...cluster.children.map(node => node.id)]);
45
+ const requestedTypes = new Set(includeEdgeTypes);
46
+ const includedNodeIds = new Set();
47
+ const edges = [...cluster.edges];
48
+ const seen = new Set(cluster.edges.map(edge => edge.id));
49
+ for (const id of clusterIds) {
50
+ for (const edge of graph.edgesByFrom.get(id) ?? []) {
51
+ collectIncludedEdge(edge, requestedTypes, clusterIds, includedNodeIds, edges, seen);
52
+ }
53
+ for (const edge of graph.edgesByTo.get(id) ?? []) {
54
+ collectIncludedEdge(edge, requestedTypes, clusterIds, includedNodeIds, edges, seen);
55
+ }
56
+ }
57
+ return {
58
+ root: cluster.root,
59
+ descendants: cluster.children,
60
+ includedNodes: [...includedNodeIds]
61
+ .map(id => graph.nodesById.get(id))
62
+ .filter((node) => node !== undefined),
63
+ edges,
64
+ };
65
+ }
66
+ export function getLinkedFields(graph, nodeId) {
67
+ const root = graph.nodesById.get(nodeId);
68
+ if (!root)
69
+ throw new QueryError(`Node not found: ${nodeId}`);
70
+ const prefix = `${nodeId}.`;
71
+ const ownedFieldIds = new Set([...graph.nodesById.entries()]
72
+ .filter(([id, node]) => id.startsWith(prefix) && node.template === 'Field')
73
+ .map(([id]) => id));
74
+ const edges = [];
75
+ const nodeIds = new Set();
76
+ const seen = new Set();
77
+ for (const fieldId of ownedFieldIds) {
78
+ for (const edge of graph.edgesByFrom.get(fieldId) ?? []) {
79
+ collectMapsTo(edge, edges, nodeIds, seen);
80
+ }
81
+ for (const edge of graph.edgesByTo.get(fieldId) ?? []) {
82
+ collectMapsTo(edge, edges, nodeIds, seen);
83
+ }
84
+ }
85
+ return {
86
+ edges,
87
+ nodes: [...nodeIds]
88
+ .map(id => graph.nodesById.get(id))
89
+ .filter((node) => node !== undefined),
90
+ };
91
+ }
92
+ function collectMapsTo(edge, edges, nodeIds, seen) {
93
+ if (edge.type !== 'maps-to' || seen.has(edge.id))
94
+ return;
95
+ edges.push(edge);
96
+ nodeIds.add(edge.from);
97
+ nodeIds.add(edge.to);
98
+ seen.add(edge.id);
99
+ }
100
+ function collectIncludedEdge(edge, requestedTypes, clusterIds, includedNodeIds, edges, seen) {
101
+ if (!requestedTypes.has(edge.type) || seen.has(edge.id))
102
+ return;
103
+ edges.push(edge);
104
+ seen.add(edge.id);
105
+ if (!clusterIds.has(edge.from))
106
+ includedNodeIds.add(edge.from);
107
+ if (!clusterIds.has(edge.to))
108
+ includedNodeIds.add(edge.to);
109
+ }
110
+ const OVERLAY_EXCLUDED = new Set(['local', 'shared']);
111
+ export function computeClusterOverlay(multi, viewingRef, overlayRefs, clusterRootId) {
112
+ const overlay = multi.overlay(viewingRef);
113
+ const prefix = `${clusterRootId}.`;
114
+ const fields = [...overlay.nodes.values()]
115
+ .filter(node => {
116
+ if (OVERLAY_EXCLUDED.has(node.ghostState))
117
+ return false;
118
+ if (!node.id.startsWith(prefix))
119
+ return false;
120
+ return overlayRefs.some(ref => node.presence.has(ref));
121
+ })
122
+ .map(node => {
123
+ const sourceRef = overlayRefs.find(ref => node.presence.has(ref)) ?? viewingRef;
124
+ const sourceNode = node.presence.get(sourceRef) ?? [...node.presence.values()][0];
125
+ return { id: node.id, ghostState: node.ghostState, sourceRef, node: sourceNode };
126
+ });
127
+ return fields.length === 0 ? null : { viewingRef, overlayRefs, fields };
128
+ }
@@ -0,0 +1,136 @@
1
+ import { QueryError } from '../schema/index.js';
2
+ export function computeOverlay(viewingRef, defaultBranch, allBranches) {
3
+ const branches = uniqueBranches([defaultBranch, ...allBranches]);
4
+ if (!branches.some(branch => branch.ref === viewingRef)) {
5
+ throw new QueryError(`branch '${viewingRef}' not found in loaded branches`);
6
+ }
7
+ const nodes = new Map();
8
+ for (const id of collectNodeIds(branches)) {
9
+ const presence = new Map();
10
+ for (const branch of branches) {
11
+ const node = branch.graph.nodesById.get(id);
12
+ if (node)
13
+ presence.set(branch.ref, node);
14
+ }
15
+ nodes.set(id, {
16
+ id,
17
+ presence,
18
+ ghostState: classifyPresence(viewingRef, defaultBranch.ref, presence, nodesEqual),
19
+ });
20
+ }
21
+ const edges = new Map();
22
+ for (const id of collectEdgeIds(branches)) {
23
+ const presence = new Map();
24
+ for (const branch of branches) {
25
+ const edge = findEdge(branch, id);
26
+ if (edge)
27
+ presence.set(branch.ref, edge);
28
+ }
29
+ edges.set(id, {
30
+ id,
31
+ presence,
32
+ ghostState: classifyPresence(viewingRef, defaultBranch.ref, presence, edgesEqual),
33
+ });
34
+ }
35
+ return { viewingRef, nodes, edges };
36
+ }
37
+ export function computeDiff(branch, defaultBranch) {
38
+ const added = [];
39
+ const modified = [];
40
+ const removed = [];
41
+ for (const [id, node] of branch.graph.nodesById) {
42
+ const defaultNode = defaultBranch.graph.nodesById.get(id);
43
+ if (!defaultNode) {
44
+ added.push(node);
45
+ }
46
+ else if (!nodesEqual(node, defaultNode)) {
47
+ modified.push(node);
48
+ }
49
+ }
50
+ for (const [id, node] of defaultBranch.graph.nodesById) {
51
+ if (!branch.graph.nodesById.has(id))
52
+ removed.push(node);
53
+ }
54
+ return { added, modified, removed };
55
+ }
56
+ function classifyPresence(viewingRef, defaultRef, presence, equal) {
57
+ const viewingItem = presence.get(viewingRef);
58
+ if (viewingItem) {
59
+ const others = [...presence.entries()].filter(([ref]) => ref !== viewingRef);
60
+ if (others.length === 0)
61
+ return 'local';
62
+ return others.every(([, item]) => equal(viewingItem, item)) ? 'shared' : 'local-modified';
63
+ }
64
+ const defaultItem = presence.get(defaultRef);
65
+ const nonDefault = [...presence.entries()].filter(([ref]) => ref !== defaultRef && ref !== viewingRef);
66
+ if (defaultItem && nonDefault.length === 0)
67
+ return 'default-only';
68
+ if (!defaultItem && nonDefault.length === 1)
69
+ return 'ghost-single';
70
+ const otherItems = [...presence.values()];
71
+ const first = otherItems[0];
72
+ return first && otherItems.every(item => equal(first, item)) ? 'ghost-consensus' : 'ghost-conflict';
73
+ }
74
+ function nodesEqual(a, b) {
75
+ return deepEqual(a.properties, b.properties)
76
+ && a.state === b.state
77
+ && a.stability === b.stability;
78
+ }
79
+ function edgesEqual(a, b) {
80
+ return a.type === b.type
81
+ && a.state === b.state
82
+ && a.stability === b.stability
83
+ && a.notes === b.notes;
84
+ }
85
+ function deepEqual(a, b) {
86
+ if (Object.is(a, b))
87
+ return true;
88
+ if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null)
89
+ return false;
90
+ if (Array.isArray(a) || Array.isArray(b)) {
91
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
92
+ return false;
93
+ return a.every((item, index) => deepEqual(item, b[index]));
94
+ }
95
+ const aRecord = a;
96
+ const bRecord = b;
97
+ const aKeys = Object.keys(aRecord);
98
+ const bKeys = Object.keys(bRecord);
99
+ if (aKeys.length !== bKeys.length)
100
+ return false;
101
+ return aKeys.every(key => Object.prototype.hasOwnProperty.call(bRecord, key) && deepEqual(aRecord[key], bRecord[key]));
102
+ }
103
+ function uniqueBranches(branches) {
104
+ const byRef = new Map();
105
+ for (const branch of branches) {
106
+ if (!byRef.has(branch.ref))
107
+ byRef.set(branch.ref, branch);
108
+ }
109
+ return [...byRef.values()];
110
+ }
111
+ function collectNodeIds(branches) {
112
+ const ids = new Set();
113
+ for (const branch of branches) {
114
+ for (const id of branch.graph.nodesById.keys())
115
+ ids.add(id);
116
+ }
117
+ return ids;
118
+ }
119
+ function collectEdgeIds(branches) {
120
+ const ids = new Set();
121
+ for (const branch of branches) {
122
+ for (const edgeList of branch.graph.edgesByFrom.values()) {
123
+ for (const edge of edgeList)
124
+ ids.add(edge.id);
125
+ }
126
+ }
127
+ return ids;
128
+ }
129
+ function findEdge(branch, edgeId) {
130
+ for (const edgeList of branch.graph.edgesByFrom.values()) {
131
+ const edge = edgeList.find(item => item.id === edgeId);
132
+ if (edge)
133
+ return edge;
134
+ }
135
+ return undefined;
136
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { parse as parseYaml } from 'yaml';
3
+ export function loadImportConfig(filePath) {
4
+ let raw;
5
+ try {
6
+ raw = parseYaml(readFileSync(filePath, 'utf-8'));
7
+ }
8
+ catch (err) {
9
+ throw new Error(`Failed to parse import config: ${err}`);
10
+ }
11
+ if (!isImportConfig(raw)) {
12
+ throw new Error(`Invalid import config: must have an "imports" array`);
13
+ }
14
+ return raw;
15
+ }
16
+ function isImportConfig(value) {
17
+ return (typeof value === 'object' &&
18
+ value !== null &&
19
+ 'imports' in value &&
20
+ Array.isArray(value.imports));
21
+ }
22
+ export function buildOpenAPIConfig(spec, strategy, segment, pattern, component) {
23
+ let componentMapping;
24
+ if (strategy === 'hardcoded') {
25
+ if (!component)
26
+ throw new Error('--component required for hardcoded strategy');
27
+ componentMapping = { strategy: 'hardcoded', component };
28
+ }
29
+ else if (strategy === 'tag') {
30
+ componentMapping = { strategy: 'tag' };
31
+ }
32
+ else if (pattern) {
33
+ componentMapping = { strategy: 'uri-segment', pattern };
34
+ }
35
+ else {
36
+ componentMapping = { strategy: 'uri-segment', segment: segment ?? 0 };
37
+ }
38
+ return { adapter: 'openapi', spec, componentMapping };
39
+ }
@@ -0,0 +1,56 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import path from 'node:path';
3
+ import { loadGraph } from '../loader/index.js';
4
+ import { serializeGraph } from '../writer/graph-writer.js';
5
+ import { getAdapter } from '../adapters/index.js';
6
+ import { diffNodes } from '../reconcile/index.js';
7
+ export async function runImport(config, runtimeConfig) {
8
+ const allDiagnostics = [];
9
+ const graph = await loadGraph({ source: runtimeConfig.source });
10
+ for (const entry of config.imports) {
11
+ const packConfig = await loadPackAdapterConfig(runtimeConfig, entry.adapter);
12
+ if (!packConfig) {
13
+ allDiagnostics.push({
14
+ severity: 'error',
15
+ file: runtimeConfig.graphPath,
16
+ message: `No ${entry.adapter} adapter config found in active packs — is the ${entry.adapter === 'openapi' ? 'rest' : entry.adapter} pack active?`,
17
+ });
18
+ continue;
19
+ }
20
+ const specPath = path.resolve(entry.spec);
21
+ const resolvedEntry = { ...entry, spec: specPath };
22
+ const adapter = getAdapter(resolvedEntry.adapter);
23
+ const result = await adapter.import(resolvedEntry, { packConfig, templates: graph.templates });
24
+ allDiagnostics.push(...result.diagnostics);
25
+ if (result.diagnostics.some(d => d.severity === 'error'))
26
+ continue;
27
+ const { toAdd, toUpdate, toRemove } = diffNodes(result.nodes, graph.nodesById, specPath);
28
+ for (const node of [...toAdd, ...toUpdate, ...toRemove]) {
29
+ graph.nodesById.set(node.id, node);
30
+ }
31
+ }
32
+ const graphPath = runtimeConfig.kind === 'filesystem' ? runtimeConfig.graphPath : undefined;
33
+ const contentMap = serializeGraph(graph, { sourceGraphPath: graphPath, outputGraphPath: graphPath });
34
+ await runtimeConfig.source.commit(await runtimeConfig.source.defaultBranch(), contentMap, 'corum import', { replaceGraphContent: true });
35
+ return { diagnostics: allDiagnostics };
36
+ }
37
+ async function loadPackAdapterConfig(runtimeConfig, adapterId) {
38
+ let packContent;
39
+ try {
40
+ packContent = await runtimeConfig.source.loadPackContent(await runtimeConfig.source.defaultBranch());
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ for (const [key, content] of packContent) {
46
+ if (key.endsWith(`/adapters/${adapterId}.yaml`)) {
47
+ try {
48
+ return parseYaml(content);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
@@ -0,0 +1,120 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { getOwnedSections } from './pack-loader.js';
3
+ import { listYamlKeys, readYaml } from '../source/content-utils.js';
4
+ import { STRUCTURAL_EDGE_BY_ITEM_TEMPLATE, VALID_STABILITY_SET, VALID_STATE_SET } from './constants.js';
5
+ export function loadClusters(content, templates, diagnostics) {
6
+ const result = { nodes: new Map(), edgesByFrom: new Map(), edgesByTo: new Map() };
7
+ for (const key of listYamlKeys(content, 'components')) {
8
+ let raw;
9
+ try {
10
+ raw = parseYaml(readYaml(content, key));
11
+ }
12
+ catch (err) {
13
+ diagnostics.push({ severity: 'error', file: key, message: `failed to parse YAML: ${err}` });
14
+ continue;
15
+ }
16
+ const record = raw;
17
+ const meta = record.metadata;
18
+ if (typeof record.id !== 'string' ||
19
+ typeof record.template !== 'string' ||
20
+ typeof record.schemaVersion !== 'string' ||
21
+ !isRecord(meta) ||
22
+ typeof meta.component !== 'string' ||
23
+ typeof meta.lastModifiedAt !== 'string') {
24
+ diagnostics.push({ severity: 'error', file: key, message: 'cluster missing required root fields' });
25
+ continue;
26
+ }
27
+ const root = {
28
+ id: record.id,
29
+ template: record.template,
30
+ component: meta.component,
31
+ state: asState(meta.state, 'proposed'),
32
+ stability: asStability(meta.stability, 'unstable'),
33
+ schemaVersion: record.schemaVersion,
34
+ lastModifiedAt: meta.lastModifiedAt,
35
+ ...(typeof meta.extractedFrom === 'string' && { extractedFrom: meta.extractedFrom }),
36
+ ...(typeof meta.derivation === 'string' && { derivation: meta.derivation }),
37
+ ...(typeof meta.derivedBy === 'string' && { derivedBy: meta.derivedBy }),
38
+ properties: isRecord(record.properties) ? record.properties : {},
39
+ };
40
+ addNode(result, root, key, diagnostics);
41
+ materialiseChildren(result, root, record, templates, key, diagnostics);
42
+ }
43
+ return result;
44
+ }
45
+ function materialiseChildren(result, parent, source, templates, filePath, diagnostics) {
46
+ const template = templates.get(parent.template);
47
+ if (!template) {
48
+ diagnostics.push({ severity: 'error', file: filePath, nodeId: parent.id, message: `unknown template: ${parent.template}` });
49
+ return;
50
+ }
51
+ for (const [sectionName, childTemplateName] of Object.entries(getOwnedSections(template))) {
52
+ const section = source[sectionName];
53
+ if (!isRecord(section))
54
+ continue;
55
+ for (const [localName, value] of Object.entries(section)) {
56
+ if (!isRecord(value)) {
57
+ diagnostics.push({
58
+ severity: 'error',
59
+ file: filePath,
60
+ nodeId: `${parent.id}.${sectionName}.${localName}`,
61
+ message: `owned item in ${sectionName} must be an object`,
62
+ });
63
+ continue;
64
+ }
65
+ const childId = `${parent.id}.${sectionName}.${localName}`;
66
+ const child = {
67
+ id: childId,
68
+ template: childTemplateName,
69
+ component: parent.component,
70
+ state: asState(value.state, parent.state),
71
+ stability: asStability(value.stability, parent.stability),
72
+ schemaVersion: parent.schemaVersion,
73
+ lastModifiedAt: parent.lastModifiedAt,
74
+ properties: stripOwnedSections(value, childTemplateName, templates),
75
+ };
76
+ addNode(result, child, filePath, diagnostics);
77
+ const edgeType = STRUCTURAL_EDGE_BY_ITEM_TEMPLATE[childTemplateName];
78
+ if (edgeType) {
79
+ addEdge(result, {
80
+ id: `${parent.id}__${edgeType}__${child.id}`,
81
+ from: parent.id,
82
+ to: child.id,
83
+ type: edgeType,
84
+ state: child.state,
85
+ stability: child.stability,
86
+ });
87
+ }
88
+ materialiseChildren(result, child, value, templates, filePath, diagnostics);
89
+ }
90
+ }
91
+ }
92
+ function stripOwnedSections(value, templateName, templates) {
93
+ const ownedSections = new Set(Object.keys(getOwnedSections(templates.get(templateName) ?? ({ name: templateName, info: { version: 'unknown' } }))));
94
+ const nodeMetadata = new Set(['state', 'stability']);
95
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !ownedSections.has(key) && !nodeMetadata.has(key)));
96
+ }
97
+ function addNode(result, node, file, diagnostics) {
98
+ if (result.nodes.has(node.id)) {
99
+ diagnostics.push({ severity: 'error', file, nodeId: node.id, message: `duplicate node id: ${node.id}` });
100
+ return;
101
+ }
102
+ result.nodes.set(node.id, node);
103
+ }
104
+ function addEdge(result, edge) {
105
+ const from = result.edgesByFrom.get(edge.from) ?? [];
106
+ from.push(edge);
107
+ result.edgesByFrom.set(edge.from, from);
108
+ const to = result.edgesByTo.get(edge.to) ?? [];
109
+ to.push(edge);
110
+ result.edgesByTo.set(edge.to, to);
111
+ }
112
+ function isRecord(value) {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
114
+ }
115
+ function asState(value, fallback) {
116
+ return typeof value === 'string' && VALID_STATE_SET.has(value) ? value : fallback;
117
+ }
118
+ function asStability(value, fallback) {
119
+ return typeof value === 'string' && VALID_STABILITY_SET.has(value) ? value : fallback;
120
+ }
@@ -0,0 +1,32 @@
1
+ export const VALID_STATES = [
2
+ 'draft',
3
+ 'proposed',
4
+ 'agreed',
5
+ 'future',
6
+ 'removed',
7
+ 'implemented',
8
+ ];
9
+ export const VALID_STABILITIES = [
10
+ 'unstable',
11
+ 'stable',
12
+ 'deprecated',
13
+ ];
14
+ export const VALID_EDGE_TYPES = [
15
+ 'triggers',
16
+ 'produces',
17
+ 'reads',
18
+ 'calls',
19
+ 'implements',
20
+ 'maps-to',
21
+ 'derived-from',
22
+ 'renamed-from',
23
+ 'has-field',
24
+ 'has-value',
25
+ ];
26
+ export const VALID_STATE_SET = new Set(VALID_STATES);
27
+ export const VALID_STABILITY_SET = new Set(VALID_STABILITIES);
28
+ export const VALID_EDGE_TYPE_SET = new Set(VALID_EDGE_TYPES);
29
+ export const STRUCTURAL_EDGE_BY_ITEM_TEMPLATE = {
30
+ Field: 'has-field',
31
+ EnumValue: 'has-value',
32
+ };
@@ -0,0 +1,59 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { VALID_EDGE_TYPE_SET } from './constants.js';
3
+ import { listYamlKeys, readYaml } from '../source/content-utils.js';
4
+ export function loadEdges(content, nodes, diagnostics) {
5
+ const result = { edgesByFrom: new Map(), edgesByTo: new Map() };
6
+ for (const key of listYamlKeys(content, 'edges')) {
7
+ let raw;
8
+ try {
9
+ raw = parseYaml(readYaml(content, key));
10
+ }
11
+ catch (err) {
12
+ diagnostics.push({ severity: 'error', file: key, message: `failed to parse YAML: ${err}` });
13
+ continue;
14
+ }
15
+ const edgeList = raw.edges ?? [];
16
+ for (const edgeRaw of edgeList) {
17
+ const edgeRecord = edgeRaw;
18
+ if (typeof edgeRecord.from !== 'string' ||
19
+ typeof edgeRecord.to !== 'string' ||
20
+ typeof edgeRecord.type !== 'string') {
21
+ diagnostics.push({ severity: 'error', file: key, message: 'edge missing required from, to, or type' });
22
+ continue;
23
+ }
24
+ if (!VALID_EDGE_TYPE_SET.has(edgeRecord.type)) {
25
+ diagnostics.push({ severity: 'error', file: key, message: `invalid edge type: ${edgeRecord.type}` });
26
+ continue;
27
+ }
28
+ let unresolvedEndpoint = false;
29
+ if (!nodes.has(edgeRecord.from)) {
30
+ diagnostics.push({ severity: 'error', file: key, message: `edge from unresolved node: ${edgeRecord.from}` });
31
+ unresolvedEndpoint = true;
32
+ }
33
+ if (!nodes.has(edgeRecord.to)) {
34
+ diagnostics.push({ severity: 'error', file: key, message: `edge to unresolved node: ${edgeRecord.to}` });
35
+ unresolvedEndpoint = true;
36
+ }
37
+ if (unresolvedEndpoint)
38
+ continue;
39
+ addEdge(result, {
40
+ id: `${edgeRecord.from}__${edgeRecord.type}__${edgeRecord.to}`,
41
+ from: edgeRecord.from,
42
+ to: edgeRecord.to,
43
+ type: edgeRecord.type,
44
+ state: typeof edgeRecord.state === 'string' ? edgeRecord.state : 'proposed',
45
+ stability: typeof edgeRecord.stability === 'string' ? edgeRecord.stability : 'unstable',
46
+ notes: typeof edgeRecord.notes === 'string' ? edgeRecord.notes : undefined,
47
+ });
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ function addEdge(result, edge) {
53
+ const from = result.edgesByFrom.get(edge.from) ?? [];
54
+ from.push(edge);
55
+ result.edgesByFrom.set(edge.from, from);
56
+ const to = result.edgesByTo.get(edge.to) ?? [];
57
+ to.push(edge);
58
+ result.edgesByTo.set(edge.to, to);
59
+ }
@@ -0,0 +1,20 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ export function walkYamlFiles(dir) {
4
+ const result = [];
5
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
6
+ const fullPath = path.join(dir, entry.name);
7
+ if (entry.isDirectory()) {
8
+ result.push(...walkYamlFiles(fullPath));
9
+ }
10
+ else if (entry.isFile() && entry.name.endsWith('.yaml')) {
11
+ result.push(fullPath);
12
+ }
13
+ }
14
+ return result;
15
+ }
16
+ export function isPackRef(value) {
17
+ return typeof value === 'object' &&
18
+ value !== null &&
19
+ typeof value.path === 'string';
20
+ }