@andespindola/brainlink 1.0.5 → 1.0.6

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 (51) hide show
  1. package/README.md +8 -0
  2. package/dist/application/add-note.js +2 -2
  3. package/dist/application/build-context.js +16 -10
  4. package/dist/application/canonical-context-links.js +44 -5
  5. package/dist/application/check-package-update.js +105 -0
  6. package/dist/application/frontend/client/chunk-fetch.js +236 -0
  7. package/dist/application/frontend/client/controls.js +178 -0
  8. package/dist/application/frontend/client/elements.js +122 -0
  9. package/dist/application/frontend/client/input.js +202 -0
  10. package/dist/application/frontend/client/node-details.js +191 -0
  11. package/dist/application/frontend/client/rendering.js +296 -0
  12. package/dist/application/frontend/client/scope-theme.js +114 -0
  13. package/dist/application/frontend/client/spatial.js +98 -0
  14. package/dist/application/frontend/client/storage.js +215 -0
  15. package/dist/application/frontend/client/upload.js +90 -0
  16. package/dist/application/frontend/client/worker-bootstrap.js +147 -0
  17. package/dist/application/frontend/client-js.js +24 -1837
  18. package/dist/application/frontend/client-render-worker-js.js +1 -1
  19. package/dist/application/index-vault-phases.js +189 -0
  20. package/dist/application/index-vault.js +44 -165
  21. package/dist/cli/commands/write/dedupe-commands.js +59 -0
  22. package/dist/cli/commands/write/index-commands.js +205 -0
  23. package/dist/cli/commands/write/link-commands.js +68 -0
  24. package/dist/cli/commands/write/note-commands.js +146 -0
  25. package/dist/cli/commands/write/server-commands.js +553 -0
  26. package/dist/cli/commands/write/shared.js +35 -0
  27. package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
  28. package/dist/cli/commands/write-commands.js +12 -1303
  29. package/dist/cli/main.js +39 -3
  30. package/dist/domain/context.js +39 -3
  31. package/dist/domain/embeddings.js +31 -5
  32. package/dist/domain/graph-contexts.js +62 -57
  33. package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
  34. package/dist/domain/graph-layout/collisions.js +100 -0
  35. package/dist/domain/graph-layout/hierarchy.js +135 -0
  36. package/dist/domain/graph-layout/metrics.js +111 -0
  37. package/dist/domain/graph-layout/segments.js +76 -0
  38. package/dist/domain/graph-layout/star-layout.js +110 -0
  39. package/dist/domain/graph-layout.js +4 -625
  40. package/dist/infrastructure/config.js +6 -0
  41. package/dist/infrastructure/file-index.js +13 -4
  42. package/dist/infrastructure/semantic-prefilter.js +24 -0
  43. package/dist/mcp/server.js +7 -0
  44. package/dist/mcp/tool-guard.js +29 -0
  45. package/dist/mcp/tools/maintenance-tools.js +409 -0
  46. package/dist/mcp/tools/read-tools.js +504 -0
  47. package/dist/mcp/tools/shared.js +216 -0
  48. package/dist/mcp/tools/write-tools.js +247 -0
  49. package/dist/mcp/tools.js +3 -1357
  50. package/docs/QUICKSTART.md +4 -0
  51. package/package.json +2 -2
@@ -0,0 +1,135 @@
1
+ import { hierarchyGroupNodeLimit } from './metrics.js';
2
+ const layoutBounds = (nodes) => {
3
+ if (nodes.length === 0) {
4
+ return { x: 0, y: 0, radius: 1 };
5
+ }
6
+ const bounds = nodes.reduce((current, node) => ({
7
+ minX: Math.min(current.minX, node.x),
8
+ maxX: Math.max(current.maxX, node.x),
9
+ minY: Math.min(current.minY, node.y),
10
+ maxY: Math.max(current.maxY, node.y)
11
+ }), {
12
+ minX: Number.POSITIVE_INFINITY,
13
+ maxX: Number.NEGATIVE_INFINITY,
14
+ minY: Number.POSITIVE_INFINITY,
15
+ maxY: Number.NEGATIVE_INFINITY
16
+ });
17
+ const x = (bounds.minX + bounds.maxX) / 2;
18
+ const y = (bounds.minY + bounds.maxY) / 2;
19
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
20
+ return { x, y, radius: Math.max(radius + 72, 120) };
21
+ };
22
+ const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
23
+ const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
24
+ const groupTitle = (segment, level, index, nodes) => nodes.length === 1
25
+ ? nodes[0]?.title ?? segment
26
+ : `${segment} ${level + 1}.${index + 1}`;
27
+ const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
28
+ const sortedNodes = [...nodes].sort((left, right) => {
29
+ const segmentDelta = left.segment.localeCompare(right.segment);
30
+ if (segmentDelta !== 0)
31
+ return segmentDelta;
32
+ const groupDelta = left.group.localeCompare(right.group);
33
+ if (groupDelta !== 0)
34
+ return groupDelta;
35
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
36
+ if (degreeDelta !== 0)
37
+ return degreeDelta;
38
+ return left.title.localeCompare(right.title);
39
+ });
40
+ const groupCountTarget = Math.min(groupNodeLimit, sortedNodes.length);
41
+ const chunkSize = sortedNodes.length <= groupNodeLimit * groupNodeLimit
42
+ ? Math.max(1, Math.ceil(sortedNodes.length / groupCountTarget))
43
+ : groupNodeLimit;
44
+ const chunks = [];
45
+ for (let index = 0; index < sortedNodes.length; index += chunkSize) {
46
+ chunks.push(sortedNodes.slice(index, index + chunkSize));
47
+ }
48
+ return chunks;
49
+ };
50
+ const groupEdges = (edges, nodeIds) => ({
51
+ internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
52
+ externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
53
+ });
54
+ const groupBounds = (groups) => {
55
+ if (groups.length === 0) {
56
+ return { x: 0, y: 0, radius: 1 };
57
+ }
58
+ const nodes = groups.map((group) => ({
59
+ x: group.x,
60
+ y: group.y,
61
+ radius: group.radius
62
+ }));
63
+ const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
64
+ const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
65
+ const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
66
+ return { x, y, radius: Math.max(radius + 120, 180) };
67
+ };
68
+ const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
69
+ const createParentGroups = (groups, edges, level, groupNodeLimit) => {
70
+ if (groups.length <= groupNodeLimit) {
71
+ return groups;
72
+ }
73
+ const parentGroups = [];
74
+ for (let index = 0; index < groups.length; index += groupNodeLimit) {
75
+ const chunk = groups.slice(index, index + groupNodeLimit);
76
+ const nodeIds = new Set(descendantNodeIds(chunk));
77
+ const bounds = groupBounds(chunk);
78
+ const segment = chunk[0]?.segment ?? 'root';
79
+ const group = chunk[0]?.group ?? 'root';
80
+ const groupIndex = index / groupNodeLimit;
81
+ const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
82
+ const edgeGroups = groupEdges(edges, nodeIds);
83
+ parentGroups.push({
84
+ id,
85
+ level,
86
+ parentId: null,
87
+ title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
88
+ segment,
89
+ group,
90
+ x: bounds.x,
91
+ y: bounds.y,
92
+ radius: bounds.radius,
93
+ nodeIds: [],
94
+ childGroupIds: chunk.map((child) => child.id),
95
+ internalEdges: edgeGroups.internalEdges,
96
+ externalEdges: edgeGroups.externalEdges
97
+ });
98
+ }
99
+ const parentIdByChildId = new Map();
100
+ parentGroups.forEach((parent) => {
101
+ parent.childGroupIds.forEach((childId) => parentIdByChildId.set(childId, parent.id));
102
+ });
103
+ const relinkedChildren = groups.map((group) => {
104
+ const parentId = parentIdByChildId.get(group.id);
105
+ return parentId ? { ...group, parentId } : group;
106
+ });
107
+ return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
108
+ };
109
+ export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
110
+ if (nodes.length <= groupNodeLimit) {
111
+ return [];
112
+ }
113
+ const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
114
+ const nodeIds = new Set(chunk.map((node) => node.id));
115
+ const bounds = layoutBounds(chunk);
116
+ const segment = chunk[0]?.segment ?? 'root';
117
+ const group = chunk[0]?.group ?? 'root';
118
+ const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
119
+ return {
120
+ id,
121
+ level: 0,
122
+ parentId: null,
123
+ title: groupTitle(segment, 0, index, chunk),
124
+ segment,
125
+ group,
126
+ x: bounds.x,
127
+ y: bounds.y,
128
+ radius: bounds.radius,
129
+ nodeIds: chunk.map((node) => node.id),
130
+ childGroupIds: [],
131
+ ...groupEdges(edges, nodeIds)
132
+ };
133
+ });
134
+ return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
135
+ };
@@ -0,0 +1,111 @@
1
+ export const hierarchyGroupNodeLimit = 1000;
2
+ const groupLabels = {
3
+ '00-maps': 'maps',
4
+ '10-agent-memory': 'agent-memory',
5
+ '20-concepts': 'concepts',
6
+ '30-architecture': 'architecture',
7
+ '40-agents': 'agents',
8
+ '50-retrieval': 'retrieval',
9
+ '60-operations': 'operations',
10
+ '70-evaluation': 'evaluation',
11
+ '80-sessions': 'sessions',
12
+ '90-security': 'security',
13
+ root: 'root'
14
+ };
15
+ const segmentAngles = {
16
+ Brainlink: -1.58,
17
+ Architecture: -0.74,
18
+ Agents: -0.05,
19
+ Retrieval: 0.68,
20
+ Operations: 1.34,
21
+ Evaluation: 2.08,
22
+ Security: 2.82
23
+ };
24
+ const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
25
+ export const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
26
+ export const jitter = (value, range) => {
27
+ const normalized = Math.abs(hashText(value) % 1000) / 1000;
28
+ return (normalized - 0.5) * range;
29
+ };
30
+ const pathSegments = (path) => path.split('/').filter(Boolean);
31
+ export const groupKey = (node) => {
32
+ const segments = pathSegments(node.path);
33
+ if (segments[0] === 'agents') {
34
+ return segments[2] ?? 'root';
35
+ }
36
+ return segments[0] ?? 'root';
37
+ };
38
+ export const groupLabel = (key) => groupLabels[key] ?? key;
39
+ const incrementDegreeBy = (degrees, id, amount) => {
40
+ degrees.set(id, (degrees.get(id) ?? 0) + amount);
41
+ return degrees;
42
+ };
43
+ const edgeDegreeWeight = (edge) => Math.max(1, Math.min(edge.weight, 8));
44
+ export const countDegrees = (edges) => edges.reduce((degrees, edge) => {
45
+ const weight = edgeDegreeWeight(edge);
46
+ return edge.target
47
+ ? incrementDegreeBy(incrementDegreeBy(degrees, edge.source, weight), edge.target, weight)
48
+ : incrementDegreeBy(degrees, edge.source, weight);
49
+ }, new Map());
50
+ export const createAdjacency = (nodes, edges) => {
51
+ const nodeIds = new Set(nodes.map((node) => node.id));
52
+ const adjacency = new Map(nodes.map((node) => [node.id, new Set()]));
53
+ edges.forEach((edge) => {
54
+ if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
55
+ return;
56
+ }
57
+ adjacency.get(edge.source)?.add(edge.target);
58
+ adjacency.get(edge.target)?.add(edge.source);
59
+ });
60
+ return new Map(Array.from(adjacency.entries(), ([id, neighbors]) => [id, Array.from(neighbors)]));
61
+ };
62
+ export const byTitle = (left, right) => left.title.localeCompare(right.title);
63
+ export const byDegreeThenTitle = (degrees) => (left, right) => {
64
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
65
+ return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
66
+ };
67
+ export const hubScore = (node) => {
68
+ const title = node.title.trim().toLowerCase();
69
+ if (title === 'memory hub')
70
+ return 5;
71
+ if (title === 'knowledge root')
72
+ return 4;
73
+ if (/\bmoc\b/i.test(node.title))
74
+ return 3;
75
+ return hubTitlePattern.test(node.title) ? 2 : 0;
76
+ };
77
+ export const selectPrimaryHubId = (nodes, degrees) => {
78
+ const ranked = [...nodes]
79
+ .filter((node) => hubScore(node) > 0)
80
+ .sort((left, right) => {
81
+ const scoreDelta = hubScore(right) - hubScore(left);
82
+ if (scoreDelta !== 0)
83
+ return scoreDelta;
84
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
85
+ if (degreeDelta !== 0)
86
+ return degreeDelta;
87
+ return left.title.localeCompare(right.title);
88
+ });
89
+ return ranked[0]?.id ?? null;
90
+ };
91
+ export const selectHighestDegreeNodeId = (nodes, degrees) => [...nodes].sort((left, right) => {
92
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
93
+ return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
94
+ })[0]?.id ?? null;
95
+ export const centerLayoutByNode = (nodes, nodeId) => {
96
+ if (!nodeId) {
97
+ return nodes;
98
+ }
99
+ const anchor = nodes.find((node) => node.id === nodeId);
100
+ if (!anchor) {
101
+ return nodes;
102
+ }
103
+ return nodes.map((node) => ({
104
+ ...node,
105
+ x: node.x - anchor.x,
106
+ y: node.y - anchor.y
107
+ }));
108
+ };
109
+ export const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
110
+ const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
111
+ export const getMinimumLayoutDistance = (nodes) => nodes.reduce((minimumDistance, leftNode, leftIndex) => nodes.slice(leftIndex + 1).reduce((innerMinimum, rightNode) => Math.min(innerMinimum, distanceBetween(leftNode, rightNode)), minimumDistance), Number.POSITIVE_INFINITY);
@@ -0,0 +1,76 @@
1
+ import { deriveVisualContextRules, inferExplicitVisualGraphContext } from '../graph-contexts.js';
2
+ import { byDegreeThenTitle, byTitle, createAdjacency, groupKey, groupLabel } from './metrics.js';
3
+ const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
4
+ const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
5
+ const collectComponent = (adjacency, startId, visited) => {
6
+ const queue = [startId];
7
+ const component = [];
8
+ visited.add(startId);
9
+ for (let index = 0; index < queue.length; index += 1) {
10
+ const id = queue[index];
11
+ component.push(id);
12
+ (adjacency.get(id) ?? []).forEach((nextId) => {
13
+ if (!visited.has(nextId)) {
14
+ visited.add(nextId);
15
+ queue.push(nextId);
16
+ }
17
+ });
18
+ }
19
+ return component;
20
+ };
21
+ const connectedComponents = (nodes, adjacency) => {
22
+ const visited = new Set();
23
+ return [...nodes].sort(byTitle).reduce((components, node) => (visited.has(node.id) ? components : [...components, collectComponent(adjacency, node.id, visited)]), []);
24
+ };
25
+ const selectSegmentSeeds = (nodes, edges, degrees) => {
26
+ const adjacency = createAdjacency(nodes, edges);
27
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
28
+ return connectedComponents(nodes, adjacency).flatMap((component) => {
29
+ const componentNodes = component.map((id) => nodeById.get(id)).filter((node) => Boolean(node));
30
+ const naturalSeeds = componentNodes.filter(naturalSegmentSeed).sort(byDegreeThenTitle(degrees));
31
+ return naturalSeeds.length > 0 ? naturalSeeds : componentNodes.sort(byDegreeThenTitle(degrees)).slice(0, 1);
32
+ });
33
+ };
34
+ export const assignSegments = (nodes, edges, degrees) => {
35
+ const adjacency = createAdjacency(nodes, edges);
36
+ const seeds = selectSegmentSeeds(nodes, edges, degrees);
37
+ const rules = deriveVisualContextRules(nodes);
38
+ const assignments = new Map(nodes.flatMap((node) => {
39
+ const visualContext = inferExplicitVisualGraphContext(node, rules);
40
+ return visualContext ? [[node.id, visualContext.title]] : [];
41
+ }));
42
+ const queue = seeds.map((seed) => seed.id);
43
+ seeds.forEach((seed) => {
44
+ if (!assignments.has(seed.id)) {
45
+ assignments.set(seed.id, segmentName(seed));
46
+ }
47
+ });
48
+ for (let index = 0; index < queue.length; index += 1) {
49
+ const id = queue[index];
50
+ const segment = assignments.get(id);
51
+ if (!segment) {
52
+ continue;
53
+ }
54
+ ;
55
+ (adjacency.get(id) ?? []).forEach((nextId) => {
56
+ if (!assignments.has(nextId)) {
57
+ assignments.set(nextId, segment);
58
+ queue.push(nextId);
59
+ }
60
+ });
61
+ }
62
+ return new Map(nodes.map((node) => [node.id, assignments.get(node.id) ?? groupLabel(groupKey(node))]));
63
+ };
64
+ export const groupNodesBySegment = (nodes, segments) => {
65
+ const groups = new Map();
66
+ nodes.forEach((node) => {
67
+ const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
68
+ const bucket = groups.get(segment);
69
+ if (bucket) {
70
+ bucket.push(node);
71
+ return;
72
+ }
73
+ groups.set(segment, [node]);
74
+ });
75
+ return new Map(groups);
76
+ };
@@ -0,0 +1,110 @@
1
+ import { centerLayoutByNode, countDegrees, createAdjacency, groupKey, groupLabel, jitter, segmentAngle, selectHighestDegreeNodeId, selectPrimaryHubId } from './metrics.js';
2
+ import { assignSegments } from './segments.js';
3
+ import { relaxCollisions } from './collisions.js';
4
+ import { createGraphLayoutHierarchy } from './hierarchy.js';
5
+ const compareByStarOrder = (levels, degrees, segmentIndexByName, segments) => (left, right) => {
6
+ const levelDelta = (levels.get(left.id) ?? 1) - (levels.get(right.id) ?? 1);
7
+ if (levelDelta !== 0)
8
+ return levelDelta;
9
+ const leftSegment = segments.get(left.id) ?? groupLabel(groupKey(left));
10
+ const rightSegment = segments.get(right.id) ?? groupLabel(groupKey(right));
11
+ const segmentDelta = (segmentIndexByName.get(leftSegment) ?? 0) - (segmentIndexByName.get(rightSegment) ?? 0);
12
+ if (segmentDelta !== 0)
13
+ return segmentDelta;
14
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
15
+ return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
16
+ };
17
+ const assignStarLevels = (nodes, edges, hubId) => {
18
+ if (!hubId) {
19
+ return new Map(nodes.map((node) => [node.id, 1]));
20
+ }
21
+ const adjacency = createAdjacency(nodes, edges);
22
+ const levels = new Map([[hubId, 0]]);
23
+ const queue = [hubId];
24
+ for (let index = 0; index < queue.length; index += 1) {
25
+ const id = queue[index];
26
+ const nextLevel = (levels.get(id) ?? 0) + 1;
27
+ (adjacency.get(id) ?? []).forEach((nextId) => {
28
+ if (!levels.has(nextId)) {
29
+ levels.set(nextId, nextLevel);
30
+ queue.push(nextId);
31
+ }
32
+ });
33
+ }
34
+ nodes.forEach((node) => {
35
+ if (!levels.has(node.id)) {
36
+ levels.set(node.id, 2);
37
+ }
38
+ });
39
+ return levels;
40
+ };
41
+ const createStarNodes = (nodes, segments, degrees, hubId, levels) => {
42
+ const segmentNames = Array.from(new Set(nodes.map((node) => segments.get(node.id) ?? groupLabel(groupKey(node)))))
43
+ .sort((left, right) => segmentAngle(left, 0, 1) - segmentAngle(right, 0, 1) || left.localeCompare(right));
44
+ const segmentIndexByName = new Map(segmentNames.map((segment, index) => [segment, index]));
45
+ const sortedNodes = [...nodes].sort(compareByStarOrder(levels, degrees, segmentIndexByName, segments));
46
+ const nodesByLevel = sortedNodes.reduce((state, node) => {
47
+ const level = node.id === hubId ? 0 : Math.max(1, levels.get(node.id) ?? 1);
48
+ const levelNodes = state.get(level) ?? [];
49
+ levelNodes.push(node);
50
+ state.set(level, levelNodes);
51
+ return state;
52
+ }, new Map());
53
+ return Array.from(nodesByLevel.entries())
54
+ .sort(([left], [right]) => left - right)
55
+ .flatMap(([level, levelNodes]) => {
56
+ if (level === 0) {
57
+ return levelNodes.map((node) => ({
58
+ ...node,
59
+ group: groupLabel(groupKey(node)),
60
+ segment: segments.get(node.id) ?? groupLabel(groupKey(node)),
61
+ x: 0,
62
+ y: 0
63
+ }));
64
+ }
65
+ const levelNodesBySegment = segmentNames
66
+ .map((segment) => ({
67
+ segment,
68
+ nodes: levelNodes.filter((node) => (segments.get(node.id) ?? groupLabel(groupKey(node))) === segment)
69
+ }))
70
+ .filter((group) => group.nodes.length > 0);
71
+ const totalNodes = levelNodesBySegment.reduce((total, group) => total + group.nodes.length, 0);
72
+ const baseRadius = Math.max(360 + (level - 1) * 460, (levelNodes.length * 156) / (Math.PI * 2));
73
+ let arcCursor = -Math.PI / 2;
74
+ return levelNodesBySegment.flatMap((group) => {
75
+ const arcSize = (Math.PI * 2 * group.nodes.length) / Math.max(totalNodes, 1);
76
+ const arcPadding = Math.min(0.22, arcSize * 0.18);
77
+ const arcStart = arcCursor + arcPadding;
78
+ const arcEnd = arcCursor + arcSize - arcPadding;
79
+ const usableArc = Math.max(0.001, arcEnd - arcStart);
80
+ const segmentRadius = Math.max(baseRadius, (group.nodes.length * 156) / usableArc);
81
+ arcCursor += arcSize;
82
+ return group.nodes.map((node, index) => {
83
+ const lane = index % 3 - 1;
84
+ const angle = arcStart + usableArc * ((index + 0.5) / Math.max(group.nodes.length, 1)) + jitter(node.title, 0.035);
85
+ const radialJitter = jitter(node.id, 34);
86
+ return {
87
+ ...node,
88
+ group: groupLabel(groupKey(node)),
89
+ segment: group.segment,
90
+ x: Math.cos(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.title, 16),
91
+ y: Math.sin(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.path, 16)
92
+ };
93
+ });
94
+ });
95
+ });
96
+ };
97
+ export const createStarGraphLayout = (graph) => {
98
+ const degrees = countDegrees(graph.edges);
99
+ const hubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
100
+ const segments = assignSegments(graph.nodes, graph.edges, degrees);
101
+ const levels = assignStarLevels(graph.nodes, graph.edges, hubId);
102
+ const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels), 156, 22);
103
+ const centeredNodes = centerLayoutByNode(nodes, hubId);
104
+ const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
105
+ return {
106
+ nodes: centeredNodes,
107
+ edges: graph.edges,
108
+ ...(groups.length > 0 ? { groups } : {})
109
+ };
110
+ };