nexus-prime 7.3.1 → 7.4.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.
@@ -228,11 +228,15 @@ function renderTopologyStats() {
228
228
  $('topo-build-btn')?.addEventListener('click', _buildTopology);
229
229
  return;
230
230
  }
231
+ const m = t.meta || {};
231
232
  const rows=[
232
- ['Nodes', fmtNum(t.nodes?.length)],
233
- ['Edges', fmtNum(t.edges?.length)],
234
- ['Files', fmtNum(t.nodes?.filter(n=>n.type==='file').length)],
235
- ['Memories', fmtNum(t.nodes?.filter(n=>n.type==='memory').length)],
233
+ ['Nodes', fmtNum(t.nodes?.length)],
234
+ ['Edges', fmtNum(t.edges?.length)],
235
+ ['Files', fmtNum(m.fileCount ?? t.nodes?.filter(n=>n.type==='file').length)],
236
+ ['Modules', fmtNum(m.moduleCount ?? t.nodes?.filter(n=>n.type==='module').length)],
237
+ ['Memories', fmtNum(m.memoryCount ?? t.nodes?.filter(n=>n.type==='memory').length)],
238
+ ['Concepts', fmtNum(m.conceptCount ?? t.nodes?.filter(n=>n.type==='concept').length)],
239
+ ['Source', t.provenance?.graphSource || m.graphSource || '—'],
236
240
  ];
237
241
  el.innerHTML=rows.map(([k,v])=>`<div class="row"><span class="row-k">${esc(k)}</span><span class="row-v">${esc(v)}</span></div>`).join('');
238
242
  }
@@ -81,12 +81,14 @@ function _renderMeta() {
81
81
  el.innerHTML = [
82
82
  `<span>${esc(_topo.repoName || 'workspace')}</span>`,
83
83
  `<span>${fmtNum(meta.fileCount ?? 0)} files</span>`,
84
+ meta.moduleCount ? `<span>${fmtNum(meta.moduleCount)} modules</span>` : null,
85
+ meta.memoryCount ? `<span>${fmtNum(meta.memoryCount)} memories</span>` : null,
86
+ meta.conceptCount ? `<span>${fmtNum(meta.conceptCount)} concepts</span>` : null,
84
87
  `<span>${fmtNum(meta.edgeCount ?? 0)} edges</span>`,
85
88
  `<span>${esc(provenance.graphSource || meta.graphSource || 'unknown-source')}</span>`,
86
- `<span>${esc(provenance.freshness || meta.freshness || 'n/a')}</span>`,
87
89
  `<span>${esc(buildLabel)}</span>`,
88
90
  `<span>${esc(generatedAt)}</span>`,
89
- ].join(' · ');
91
+ ].filter(Boolean).join(' · ');
90
92
  _syncBuildControls();
91
93
  }
92
94
 
@@ -129,18 +131,18 @@ function _renderGraph() {
129
131
  if (_sim) _sim.stop();
130
132
 
131
133
  _sim = d3.forceSimulation(nodes)
132
- .force('link', d3.forceLink(links).id(d => d.id).distance(d => _edgeDistance(d.type)).strength(0.22))
133
- .force('charge', d3.forceManyBody().strength(d => d.type === 'memory' ? -40 : -70))
134
+ .force('link', d3.forceLink(links).id(d => d.id).distance(d => _edgeDistance(d.type)).strength(d => _edgeLinkStrength(d)))
135
+ .force('charge', d3.forceManyBody().strength(d => _nodeCharge(d)))
134
136
  .force('center', d3.forceCenter(W / 2, H / 2))
135
- .force('collide', d3.forceCollide(d => _nodeR(d) + 3));
137
+ .force('collide', d3.forceCollide(d => _nodeR(d) + 4));
136
138
 
137
139
  const linkLayer = d3s.append('g').attr('class', 'links');
138
140
  const linkEl = linkLayer.selectAll('line')
139
141
  .data(links).enter().append('line')
140
142
  .attr('stroke', d => _edgeColor(d.type))
141
- .attr('stroke-width', d => d.type === 'imports' ? 1.3 : 1)
142
- .attr('stroke-opacity', d => d.type === 'memory-overlay' ? 0.35 : 0.5)
143
- .attr('stroke-dasharray', d => ['memory-overlay', 'memory-association'].includes(d.type) ? '3,3' : null);
143
+ .attr('stroke-width', d => _edgeWidth(d))
144
+ .attr('stroke-opacity', d => _edgeOpacity(d))
145
+ .attr('stroke-dasharray', d => _edgeDash(d.type));
144
146
 
145
147
  const nodeLayer = d3s.append('g').attr('class', 'nodes');
146
148
  const nodeEl = nodeLayer.selectAll('.repo-node')
@@ -170,6 +172,7 @@ function _renderGraph() {
170
172
  function _makeNodeEl(d) {
171
173
  const doc = document.createElementNS('http://www.w3.org/2000/svg', 'g');
172
174
  const r = _nodeR(d);
175
+
173
176
  if (d.type === 'memory') {
174
177
  const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
175
178
  c.setAttribute('r', String(r));
@@ -182,12 +185,58 @@ function _makeNodeEl(d) {
182
185
  return doc;
183
186
  }
184
187
 
185
- const half = r;
188
+ if (d.type === 'module') {
189
+ // Diamond shape — rotated square
190
+ const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
191
+ diamond.setAttribute('points', `0,${-r} ${r},0 0,${r} ${-r},0`);
192
+ diamond.setAttribute('fill', '#f59e0b');
193
+ diamond.setAttribute('fill-opacity', '0.9');
194
+ diamond.setAttribute('stroke', '#000');
195
+ diamond.setAttribute('stroke-width', '1.2');
196
+ doc.appendChild(diamond);
197
+ // Short label below diamond
198
+ const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
199
+ txt.setAttribute('text-anchor', 'middle');
200
+ txt.setAttribute('y', String(r + 9));
201
+ txt.setAttribute('font-size', '7');
202
+ txt.setAttribute('fill', '#a3a3a3');
203
+ txt.setAttribute('pointer-events', 'none');
204
+ txt.textContent = (d.label || '').slice(0, 14);
205
+ doc.appendChild(txt);
206
+ return doc;
207
+ }
208
+
209
+ if (d.type === 'concept') {
210
+ // Hexagon shape
211
+ const pts = [];
212
+ for (let i = 0; i < 6; i++) {
213
+ const angle = (i * Math.PI) / 3 - Math.PI / 6;
214
+ pts.push(`${(r * Math.cos(angle)).toFixed(1)},${(r * Math.sin(angle)).toFixed(1)}`);
215
+ }
216
+ const hex = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
217
+ hex.setAttribute('points', pts.join(' '));
218
+ hex.setAttribute('fill', '#a855f7');
219
+ hex.setAttribute('fill-opacity', '0.85');
220
+ hex.setAttribute('stroke', '#000');
221
+ hex.setAttribute('stroke-width', '1');
222
+ doc.appendChild(hex);
223
+ const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
224
+ txt.setAttribute('text-anchor', 'middle');
225
+ txt.setAttribute('y', String(r + 9));
226
+ txt.setAttribute('font-size', '7');
227
+ txt.setAttribute('fill', '#c084fc');
228
+ txt.setAttribute('pointer-events', 'none');
229
+ txt.textContent = (d.label || '').slice(0, 12);
230
+ doc.appendChild(txt);
231
+ return doc;
232
+ }
233
+
234
+ // File node — rectangle
186
235
  const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
187
- rect.setAttribute('x', String(-half));
188
- rect.setAttribute('y', String(-half));
189
- rect.setAttribute('width', String(half * 2));
190
- rect.setAttribute('height', String(half * 2));
236
+ rect.setAttribute('x', String(-r));
237
+ rect.setAttribute('y', String(-r));
238
+ rect.setAttribute('width', String(r * 2));
239
+ rect.setAttribute('height', String(r * 2));
191
240
  rect.setAttribute('rx', '2');
192
241
  rect.setAttribute('fill', _fileColor(d));
193
242
  rect.setAttribute('fill-opacity', '0.8');
@@ -198,31 +247,66 @@ function _makeNodeEl(d) {
198
247
  }
199
248
 
200
249
  function _nodeR(d) {
201
- if (d.type === 'memory') return 4 + (d.priority || 0.3) * 8;
250
+ if (d.type === 'memory') return 4 + (d.priority || 0.3) * 8;
251
+ if (d.type === 'module') return 9;
252
+ if (d.type === 'concept') return 7;
202
253
  return 5;
203
254
  }
204
255
 
256
+ function _nodeCharge(d) {
257
+ if (d.type === 'module') return -120;
258
+ if (d.type === 'concept') return -90;
259
+ if (d.type === 'memory') return -40;
260
+ return -70;
261
+ }
262
+
205
263
  function _edgeDistance(type) {
264
+ if (type === 'belongs-to') return 42;
265
+ if (type === 'concept-of') return 95;
206
266
  if (type === 'memory-association') return 85;
207
- if (type === 'memory-overlay') return 70;
208
- if (type === 'configures') return 80;
209
- if (type === 'tests') return 60;
267
+ if (type === 'memory-overlay') return 70;
268
+ if (type === 'configures') return 80;
269
+ if (type === 'tests') return 60;
210
270
  return 55;
211
271
  }
212
272
 
273
+ function _edgeLinkStrength(d) {
274
+ if (d.type === 'belongs-to') return 0.5;
275
+ if (d.type === 'imports') return 0.3;
276
+ return 0.2;
277
+ }
278
+
213
279
  function _edgeColor(type) {
214
- if (type === 'imports') return '#27272a';
215
- if (type === 'tests') return '#22c55e';
216
- if (type === 'configures') return '#78716c';
217
- if (type === 'memory-overlay') return '#00d4ff';
280
+ if (type === 'imports') return '#27272a';
281
+ if (type === 'tests') return '#22c55e';
282
+ if (type === 'configures') return '#78716c';
283
+ if (type === 'memory-overlay') return '#00d4ff';
218
284
  if (type === 'memory-association') return '#3f3f46';
285
+ if (type === 'belongs-to') return '#52525b';
286
+ if (type === 'concept-of') return '#a855f7';
219
287
  return '#27272a';
220
288
  }
221
289
 
290
+ function _edgeWidth(d) {
291
+ const base = d.type === 'imports' ? 1.3 : d.type === 'belongs-to' ? 0.8 : 1;
292
+ return d.strength != null ? base * (0.5 + d.strength * 0.6) : base;
293
+ }
294
+
295
+ function _edgeOpacity(d) {
296
+ const base = ['memory-overlay', 'concept-of', 'belongs-to'].includes(d.type) ? 0.35 : 0.5;
297
+ return d.strength != null ? Math.min(0.85, base * (0.5 + d.strength * 0.7)) : base;
298
+ }
299
+
300
+ function _edgeDash(type) {
301
+ if (['memory-overlay', 'memory-association', 'concept-of'].includes(type)) return '3,3';
302
+ if (type === 'belongs-to') return '2,4';
303
+ return null;
304
+ }
305
+
222
306
  function _memColor(d) {
223
307
  const tier = d.tier || 'cortex';
224
- if (tier === 'prefrontal' || tier === 'working') return '#00d4ff';
225
- if (tier === 'hippocampus' || tier === 'episodic') return '#00ff88';
308
+ if (tier === 'prefrontal' || tier === 'working') return '#00d4ff';
309
+ if (tier === 'hippocampus' || tier === 'episodic') return '#00ff88';
226
310
  return '#ffd14d';
227
311
  }
228
312
 
@@ -254,7 +338,7 @@ function _onHover(d, nodeEl, linkEl) {
254
338
 
255
339
  function _onHoverOut(nodeEl, linkEl) {
256
340
  nodeEl.attr('opacity', 1);
257
- linkEl.attr('stroke-opacity', l => l.type === 'memory-overlay' ? 0.35 : 0.5);
341
+ linkEl.attr('stroke-opacity', l => _edgeOpacity(l));
258
342
  _hideTooltip();
259
343
  }
260
344
 
@@ -264,6 +348,11 @@ function _onClick(d) {
264
348
  if (d.type === 'file') {
265
349
  rows.push(['Path', d.path]);
266
350
  rows.push(['Kind', d.kind || 'other']);
351
+ if (d.module) rows.push(['Module', d.module]);
352
+ } else if (d.type === 'module') {
353
+ rows.push(['Module path', d.path]);
354
+ } else if (d.type === 'concept') {
355
+ rows.push(['Terms', d.terms?.join(', ') || d.label]);
267
356
  } else {
268
357
  rows.push(['Tier', d.tier || 'cortex']);
269
358
  rows.push(['Priority', (d.priority || 0).toFixed(2)]);
@@ -5,24 +5,34 @@ const FILE_LIMIT = 120;
5
5
  const MEMORY_LIMIT = 60;
6
6
  const FILE_READ_LIMIT = 64 * 1024;
7
7
  const MAX_STRUCTURAL_EDGES = 360;
8
+ const MAX_MODULE_EDGES = 300;
9
+ const MAX_MEMORY_OVERLAY_EDGES = FILE_LIMIT * 2;
8
10
  const MAX_MEMORY_ASSOC_EDGES = 120;
11
+ const MAX_CONCEPT_EDGES = 240;
12
+ const MEMORY_FILE_SIM_THRESHOLD = 0.08;
13
+ const MEMORY_ASSOC_SIM_THRESHOLD = 0.12;
14
+ const CONCEPT_MIN_OCCURRENCES = 2;
15
+ const CONCEPT_MAX = 20;
9
16
  const IMPORT_CANDIDATE_SUFFIXES = [
10
17
  '',
11
- '.ts',
12
- '.tsx',
13
- '.js',
14
- '.jsx',
15
- '.mts',
16
- '.cts',
17
- '.json',
18
- '.yaml',
19
- '.yml',
20
- '.toml',
21
- `${path.sep}index.ts`,
22
- `${path.sep}index.tsx`,
23
- `${path.sep}index.js`,
24
- `${path.sep}index.jsx`,
18
+ '.ts', '.tsx', '.js', '.jsx', '.mts', '.cts',
19
+ '.json', '.yaml', '.yml', '.toml',
20
+ `${path.sep}index.ts`, `${path.sep}index.tsx`,
21
+ `${path.sep}index.js`, `${path.sep}index.jsx`,
25
22
  ];
23
+ const GENERIC_TERMS = new Set([
24
+ 'the', 'and', 'for', 'with', 'this', 'that', 'are', 'was',
25
+ 'has', 'from', 'not', 'but', 'all', 'use', 'its', 'via',
26
+ 'can', 'will', 'when', 'into', 'also', 'only', 'each',
27
+ 'been', 'have', 'more', 'than', 'some', 'any', 'how',
28
+ ]);
29
+ const GENERIC_TAGS = new Set([
30
+ 'task', 'finding', 'note', 'important', 'todo', 'fix', 'done',
31
+ 'bug', 'feature', 'info', 'update', 'change', 'work', 'new',
32
+ 'code', 'file', 'data', 'type', 'value', 'item', 'list',
33
+ 'run', 'log', 'error', 'result', 'output', 'input',
34
+ ]);
35
+ // ── File helpers ─────────────────────────────────────────────────────────────
26
36
  function classifyFileKind(relPath) {
27
37
  const normalized = relPath.replace(/\\/g, '/');
28
38
  const ext = path.extname(normalized).toLowerCase();
@@ -37,6 +47,17 @@ function classifyFileKind(relPath) {
37
47
  }
38
48
  return 'other';
39
49
  }
50
+ function extractModulePath(relPath) {
51
+ const normalized = relPath.replace(/\\/g, '/');
52
+ const parts = normalized.split('/');
53
+ if (parts.length < 2)
54
+ return null;
55
+ if (parts[0] === 'src' && parts.length >= 3)
56
+ return `${parts[0]}/${parts[1]}`;
57
+ if (['test', 'tests', 'scripts'].includes(parts[0]))
58
+ return parts[0];
59
+ return parts[0];
60
+ }
40
61
  async function readFileSnippet(absPath) {
41
62
  try {
42
63
  const stat = await fs.stat(absPath);
@@ -54,9 +75,8 @@ function extractRelativeImports(source) {
54
75
  let match = null;
55
76
  while ((match = pattern.exec(source))) {
56
77
  const spec = String(match[1] || match[2] || match[3] || '').trim();
57
- if (spec.startsWith('.')) {
78
+ if (spec.startsWith('.'))
58
79
  specs.add(spec);
59
- }
60
80
  }
61
81
  return [...specs];
62
82
  }
@@ -73,15 +93,6 @@ function resolveRelativeTarget(importerAbsPath, spec, absToRel) {
73
93
  function normalizedBaseName(relPath) {
74
94
  return path.basename(relPath).replace(/\.(test|spec)\.[^.]+$/i, '').replace(/\.[^.]+$/i, '').toLowerCase();
75
95
  }
76
- function pushEdge(edges, seen, source, target, type, limit) {
77
- if (!source || !target || source === target || edges.length >= limit)
78
- return;
79
- const key = `${source}:${target}:${type}`;
80
- if (seen.has(key))
81
- return;
82
- seen.add(key);
83
- edges.push({ source, target, type });
84
- }
85
96
  function scoreConfigReference(configText, relPath) {
86
97
  if (!configText)
87
98
  return 0;
@@ -94,6 +105,42 @@ function scoreConfigReference(configText, relPath) {
94
105
  return 1;
95
106
  return 0;
96
107
  }
108
+ // ── Semantic similarity ───────────────────────────────────────────────────────
109
+ function tokenizeText(text) {
110
+ return text
111
+ .toLowerCase()
112
+ .replace(/[/\\._\-:,;()[\]{}'"`<>@#!?=+*&%$^~]/g, ' ')
113
+ .split(/\s+/)
114
+ .filter(w => w.length > 2 && !GENERIC_TERMS.has(w));
115
+ }
116
+ function tokenOverlapScore(aTokens, bTokens) {
117
+ if (!aTokens.length || !bTokens.length)
118
+ return 0;
119
+ const bSet = new Set(bTokens);
120
+ let overlap = 0;
121
+ for (const t of aTokens) {
122
+ if (bSet.has(t))
123
+ overlap++;
124
+ }
125
+ const union = aTokens.length + bTokens.length - overlap;
126
+ return union === 0 ? 0 : overlap / union;
127
+ }
128
+ function extractConceptTerms(tags) {
129
+ return tags
130
+ .map(t => t.replace(/^[#@]/, '').toLowerCase().trim())
131
+ .filter(t => t.length > 3 && !GENERIC_TAGS.has(t) && !GENERIC_TERMS.has(t));
132
+ }
133
+ // ── Edge helpers ──────────────────────────────────────────────────────────────
134
+ function pushEdge(edges, seen, source, target, type, limit, strength) {
135
+ if (!source || !target || source === target || edges.length >= limit)
136
+ return;
137
+ const key = `${source}:${target}:${type}`;
138
+ if (seen.has(key))
139
+ return;
140
+ seen.add(key);
141
+ edges.push({ source, target, type, ...(strength !== undefined ? { strength } : {}) });
142
+ }
143
+ // ── Route handler ─────────────────────────────────────────────────────────────
97
144
  export const handleGraphRoutes = async (ctx, req, res, url) => {
98
145
  if (req.method === 'POST' && url.pathname === '/api/knowledge-topology/build') {
99
146
  const repoIdentity = ctx.getSelectedRepoIdentity(url);
@@ -104,13 +151,7 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
104
151
  ? repoIdentity.repoName.trim()
105
152
  : path.basename(repoRoot) || 'workspace';
106
153
  void ensureCrGraphBuilt(repoRoot);
107
- ctx.respondJson(res, {
108
- ok: true,
109
- status: 'queued',
110
- repoRoot,
111
- repoName,
112
- queuedAt: Date.now(),
113
- }, 202);
154
+ ctx.respondJson(res, { ok: true, status: 'queued', repoRoot, repoName, queuedAt: Date.now() }, 202);
114
155
  return true;
115
156
  }
116
157
  if (req.method === 'GET' && url.pathname === '/api/knowledge-topology') {
@@ -127,6 +168,7 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
127
168
  const repoName = typeof repoIdentity?.repoName === 'string' && repoIdentity.repoName.trim()
128
169
  ? repoIdentity.repoName.trim()
129
170
  : path.basename(repoRoot) || 'workspace';
171
+ // ── File nodes ─────────────────────────────────────────────────────────
130
172
  let filePaths = [];
131
173
  try {
132
174
  filePaths = await orchestrator?.listTopologyFiles?.(FILE_LIMIT) ?? [];
@@ -145,10 +187,36 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
145
187
  label: path.basename(relPath),
146
188
  path: relPath,
147
189
  absPath: path.normalize(absPath),
190
+ module: extractModulePath(relPath),
148
191
  };
149
192
  });
150
- const absToRel = new Map(fileNodes.map((node) => [node.absPath, node.path]));
151
- const fileRelSet = new Set(fileNodes.map((node) => node.path));
193
+ const absToRel = new Map(fileNodes.map((n) => [n.absPath, n.path]));
194
+ const fileRelSet = new Set(fileNodes.map((n) => n.path));
195
+ // Pre-tokenize file paths+labels for similarity scoring
196
+ const fileTokens = new Map();
197
+ for (const node of fileNodes) {
198
+ fileTokens.set(node.id, [...tokenizeText(node.path), ...tokenizeText(node.label)]);
199
+ }
200
+ // ── Module nodes + belongs-to edges ────────────────────────────────────
201
+ const modulePathSet = new Set();
202
+ for (const node of fileNodes) {
203
+ if (node.module)
204
+ modulePathSet.add(node.module);
205
+ }
206
+ const moduleNodes = [...modulePathSet].sort().map((mod) => ({
207
+ id: `module:${mod}`,
208
+ type: 'module',
209
+ label: path.basename(mod),
210
+ path: mod,
211
+ }));
212
+ const belongsToEdges = [];
213
+ const belongsToSeen = new Set();
214
+ for (const node of fileNodes) {
215
+ if (node.module) {
216
+ pushEdge(belongsToEdges, belongsToSeen, node.id, `module:${node.module}`, 'belongs-to', MAX_MODULE_EDGES, 1.0);
217
+ }
218
+ }
219
+ // ── Structural edges (imports / tests / configures) ────────────────────
152
220
  const structuralEdges = [];
153
221
  const structuralSeen = new Set();
154
222
  for (const node of fileNodes) {
@@ -160,66 +228,130 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
160
228
  for (const spec of extractRelativeImports(fileText)) {
161
229
  const targetRel = resolveRelativeTarget(node.absPath, spec, absToRel);
162
230
  if (targetRel) {
163
- pushEdge(structuralEdges, structuralSeen, node.id, `file:${targetRel}`, 'imports', MAX_STRUCTURAL_EDGES);
231
+ pushEdge(structuralEdges, structuralSeen, node.id, `file:${targetRel}`, 'imports', MAX_STRUCTURAL_EDGES, 1.0);
164
232
  }
165
233
  }
166
234
  if (node.kind === 'test') {
167
235
  const affinityMatches = fileNodes
168
- .filter((candidate) => candidate.kind === 'source')
169
- .filter((candidate) => normalizedBaseName(candidate.path) === normalizedBaseName(node.path))
170
- .sort((left, right) => left.path.length - right.path.length)
236
+ .filter((c) => c.kind === 'source' && normalizedBaseName(c.path) === normalizedBaseName(node.path))
237
+ .sort((a, b) => a.path.length - b.path.length)
171
238
  .slice(0, 2);
172
- for (const match of affinityMatches) {
173
- pushEdge(structuralEdges, structuralSeen, node.id, match.id, 'tests', MAX_STRUCTURAL_EDGES);
239
+ for (const m of affinityMatches) {
240
+ pushEdge(structuralEdges, structuralSeen, node.id, m.id, 'tests', MAX_STRUCTURAL_EDGES, 0.9);
174
241
  }
175
242
  }
176
243
  if (node.kind === 'config') {
177
244
  const configMatches = fileNodes
178
- .filter((candidate) => candidate.kind === 'source')
179
- .map((candidate) => ({
180
- candidate,
181
- score: scoreConfigReference(fileText, candidate.path),
182
- }))
183
- .filter((entry) => entry.score > 0)
184
- .sort((left, right) => right.score - left.score || left.candidate.path.length - right.candidate.path.length)
245
+ .filter((c) => c.kind === 'source')
246
+ .map((c) => ({ c, score: scoreConfigReference(fileText, c.path) }))
247
+ .filter((e) => e.score > 0)
248
+ .sort((a, b) => b.score - a.score || a.c.path.length - b.c.path.length)
185
249
  .slice(0, 4);
186
- for (const match of configMatches) {
187
- pushEdge(structuralEdges, structuralSeen, node.id, match.candidate.id, 'configures', MAX_STRUCTURAL_EDGES);
250
+ for (const { c, score } of configMatches) {
251
+ pushEdge(structuralEdges, structuralSeen, node.id, c.id, 'configures', MAX_STRUCTURAL_EDGES, score / 3);
188
252
  }
189
253
  }
190
254
  }
255
+ // ── Memory nodes ───────────────────────────────────────────────────────
191
256
  const snapshots = memory?.listSnapshots?.(MEMORY_LIMIT, { state: 'active' }) ?? [];
192
- const memoryNodes = snapshots.map((snapshot) => ({
193
- id: `memory:${snapshot.id}`,
257
+ const memoryNodes = snapshots.map((s) => ({
258
+ id: `memory:${s.id}`,
194
259
  type: 'memory',
195
- label: (snapshot.excerpt ?? '').slice(0, 60) || snapshot.id.slice(0, 16),
196
- tier: snapshot.tier ?? 'cortex',
197
- priority: snapshot.priority ?? 0.3,
198
- tags: Array.isArray(snapshot.tags) ? snapshot.tags : [],
199
- promoted: snapshot.tier === 'hippocampus' || snapshot.tier === 'cortex',
260
+ label: (s.excerpt ?? '').slice(0, 60) || s.id.slice(0, 16),
261
+ tier: s.tier ?? 'cortex',
262
+ priority: s.priority ?? 0.3,
263
+ tags: Array.isArray(s.tags) ? s.tags : [],
264
+ promoted: s.tier === 'hippocampus' || s.tier === 'cortex',
200
265
  }));
266
+ // ── Memory → file overlay (token similarity, not fragile string match) ─
201
267
  const memoryOverlayEdges = [];
202
268
  const memoryOverlaySeen = new Set();
203
269
  for (const snapshot of snapshots) {
204
270
  const excerpt = String(snapshot.excerpt ?? '');
205
- for (const relPath of fileRelSet) {
206
- if (excerpt.includes(relPath) || excerpt.includes(path.basename(relPath))) {
207
- pushEdge(memoryOverlayEdges, memoryOverlaySeen, `memory:${snapshot.id}`, `file:${relPath}`, 'memory-overlay', FILE_LIMIT * 2);
208
- break;
271
+ const excerptTokens = tokenizeText(excerpt);
272
+ const scored = [];
273
+ for (const node of fileNodes) {
274
+ // Exact path match first (high confidence)
275
+ if (excerpt.includes(node.path) || excerpt.includes(path.basename(node.path))) {
276
+ scored.push({ nodeId: node.id, score: 0.95 });
277
+ continue;
278
+ }
279
+ // Token overlap (semantic match)
280
+ const fTokens = fileTokens.get(node.id) ?? [];
281
+ const sim = tokenOverlapScore(excerptTokens, fTokens);
282
+ if (sim >= MEMORY_FILE_SIM_THRESHOLD) {
283
+ scored.push({ nodeId: node.id, score: sim });
284
+ }
285
+ }
286
+ // Cap at top 3 links per memory — keeps graph readable
287
+ scored.sort((a, b) => b.score - a.score);
288
+ for (const { nodeId, score } of scored.slice(0, 3)) {
289
+ pushEdge(memoryOverlayEdges, memoryOverlaySeen, `memory:${snapshot.id}`, nodeId, 'memory-overlay', MAX_MEMORY_OVERLAY_EDGES, score);
290
+ }
291
+ }
292
+ // ── Memory ↔ memory association (specific tags + excerpt similarity) ───
293
+ const memoryAssocEdges = [];
294
+ const memoryAssocSeen = new Set();
295
+ const memCache = snapshots.map((s, i) => ({
296
+ id: memoryNodes[i]?.id ?? `memory:${s.id}`,
297
+ tokens: tokenizeText(String(s.excerpt ?? '')),
298
+ specificTags: (Array.isArray(s.tags) ? s.tags : [])
299
+ .map((t) => t.replace(/^[#@]/, '').toLowerCase())
300
+ .filter((t) => t.length > 3 && !GENERIC_TAGS.has(t)),
301
+ }));
302
+ for (let i = 0; i < memCache.length && memoryAssocEdges.length < MAX_MEMORY_ASSOC_EDGES; i++) {
303
+ for (let j = i + 1; j < memCache.length && memoryAssocEdges.length < MAX_MEMORY_ASSOC_EDGES; j++) {
304
+ const a = memCache[i];
305
+ const b = memCache[j];
306
+ const sharedTags = a.specificTags.filter((t) => b.specificTags.includes(t)).length;
307
+ const textSim = tokenOverlapScore(a.tokens, b.tokens);
308
+ if (sharedTags >= 1 || textSim >= MEMORY_ASSOC_SIM_THRESHOLD) {
309
+ const strength = Math.min(1, sharedTags * 0.35 + textSim);
310
+ pushEdge(memoryAssocEdges, memoryAssocSeen, a.id, b.id, 'memory-association', MAX_MEMORY_ASSOC_EDGES, strength);
209
311
  }
210
312
  }
211
313
  }
212
- const memoryAssociationEdges = [];
213
- const memoryAssociationSeen = new Set();
214
- for (let i = 0; i < memoryNodes.length && memoryAssociationEdges.length < MAX_MEMORY_ASSOC_EDGES; i += 1) {
215
- for (let j = i + 1; j < memoryNodes.length && memoryAssociationEdges.length < MAX_MEMORY_ASSOC_EDGES; j += 1) {
216
- const sharedTagCount = memoryNodes[i].tags.filter((tag) => memoryNodes[j].tags.includes(tag)).length;
217
- if (sharedTagCount >= 2) {
218
- pushEdge(memoryAssociationEdges, memoryAssociationSeen, memoryNodes[i].id, memoryNodes[j].id, 'memory-association', MAX_MEMORY_ASSOC_EDGES);
314
+ // ── Concept nodes (prominent memory tags, ≥2 occurrences) ──────────────
315
+ const conceptCounts = new Map();
316
+ for (const snapshot of snapshots) {
317
+ const tags = Array.isArray(snapshot.tags) ? snapshot.tags : [];
318
+ for (const term of extractConceptTerms(tags)) {
319
+ conceptCounts.set(term, (conceptCounts.get(term) ?? 0) + 1);
320
+ }
321
+ }
322
+ const conceptTerms = [...conceptCounts.entries()]
323
+ .filter(([, count]) => count >= CONCEPT_MIN_OCCURRENCES)
324
+ .sort(([, a], [, b]) => b - a)
325
+ .slice(0, CONCEPT_MAX)
326
+ .map(([term]) => term);
327
+ const conceptNodes = conceptTerms.map((term) => ({
328
+ id: `concept:${term}`,
329
+ type: 'concept',
330
+ label: term,
331
+ terms: [term],
332
+ }));
333
+ // concept-of edges: memory → concept (via tags)
334
+ const conceptEdges = [];
335
+ const conceptEdgesSeen = new Set();
336
+ for (const snapshot of snapshots) {
337
+ const tags = Array.isArray(snapshot.tags) ? snapshot.tags : [];
338
+ for (const term of extractConceptTerms(tags)) {
339
+ if ((conceptCounts.get(term) ?? 0) >= CONCEPT_MIN_OCCURRENCES) {
340
+ pushEdge(conceptEdges, conceptEdgesSeen, `memory:${snapshot.id}`, `concept:${term}`, 'concept-of', MAX_CONCEPT_EDGES, 0.8);
219
341
  }
220
342
  }
221
343
  }
222
- const edges = [...structuralEdges, ...memoryOverlayEdges, ...memoryAssociationEdges];
344
+ // file concept (via file name token match)
345
+ for (const node of fileNodes) {
346
+ const fTokens = fileTokens.get(node.id) ?? [];
347
+ for (const term of conceptTerms) {
348
+ if (fTokens.includes(term)) {
349
+ pushEdge(conceptEdges, conceptEdgesSeen, node.id, `concept:${term}`, 'concept-of', MAX_CONCEPT_EDGES, 0.7);
350
+ }
351
+ }
352
+ }
353
+ // ── Assemble ───────────────────────────────────────────────────────────
354
+ const edges = [...belongsToEdges, ...structuralEdges, ...memoryOverlayEdges, ...memoryAssocEdges, ...conceptEdges];
223
355
  const graphSource = structuralEdges.length > 0
224
356
  ? 'heuristic-local-relationships'
225
357
  : fileNodes.length > 0
@@ -227,37 +359,31 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
227
359
  : memoryOverlayEdges.length > 0 || memoryNodes.length > 0
228
360
  ? 'memory-overlay-fallback'
229
361
  : 'empty';
230
- // fallbackMode tells the dashboard whether this payload represents a real
231
- // topology, a degraded approximation, or no graph at all. The memory view
232
- // uses this to pick between rendering the graph, a synthetic-data banner,
233
- // or a "no graph yet" empty state — so we stop drawing edges that lie.
234
362
  const fallbackMode = graphSource === 'heuristic-local-relationships' ? 'none'
235
363
  : graphSource === 'runtime-topology-shell' ? 'synthetic'
236
364
  : graphSource === 'memory-overlay-fallback' ? 'topology-missing'
237
365
  : 'empty';
238
366
  const sources = [
239
367
  fileNodes.length > 0 ? 'runtime-topology-files' : null,
240
- structuralEdges.some((edge) => edge.type === 'imports') ? 'heuristic-imports' : null,
241
- structuralEdges.some((edge) => edge.type === 'tests') ? 'heuristic-test-affinity' : null,
242
- structuralEdges.some((edge) => edge.type === 'configures') ? 'heuristic-config-affinity' : null,
243
- memoryOverlayEdges.length > 0 ? 'memory-overlay' : null,
244
- memoryAssociationEdges.length > 0 ? 'memory-association' : null,
245
- ].filter((value) => Boolean(value));
246
- // Deterministic ordering so two consecutive loads produce the same node
247
- // list (id-sorted). The D3 simulation may still animate, but the input
248
- // is stableno more "graph jumps between refreshes" surprise.
368
+ moduleNodes.length > 0 ? 'module-grouping' : null,
369
+ structuralEdges.some((e) => e.type === 'imports') ? 'heuristic-imports' : null,
370
+ structuralEdges.some((e) => e.type === 'tests') ? 'heuristic-test-affinity' : null,
371
+ structuralEdges.some((e) => e.type === 'configures') ? 'heuristic-config-affinity' : null,
372
+ memoryOverlayEdges.length > 0 ? 'memory-overlay-similarity' : null,
373
+ memoryAssocEdges.length > 0 ? 'memory-association' : null,
374
+ conceptNodes.length > 0 ? 'concept-extraction' : null,
375
+ ].filter((v) => Boolean(v));
376
+ // Deterministic orderingstable across refreshes
249
377
  const sortedFileNodes = [...fileNodes].sort((a, b) => a.id.localeCompare(b.id));
378
+ const sortedModuleNodes = [...moduleNodes].sort((a, b) => a.id.localeCompare(b.id));
250
379
  const sortedMemoryNodes = [...memoryNodes].sort((a, b) => String(a.id).localeCompare(String(b.id)));
251
- const sortedEdges = [...edges].sort((a, b) => {
252
- const leftKey = `${a.source}|${a.target}|${a.type}`;
253
- const rightKey = `${b.source}|${b.target}|${b.type}`;
254
- return leftKey.localeCompare(rightKey);
255
- });
380
+ const sortedConceptNodes = [...conceptNodes].sort((a, b) => a.id.localeCompare(b.id));
381
+ const sortedEdges = [...edges].sort((a, b) => `${a.source}|${a.target}|${a.type}`.localeCompare(`${b.source}|${b.target}|${b.type}`));
256
382
  return {
257
383
  generatedAt: Date.now(),
258
384
  repoName,
259
385
  repoRoot,
260
- nodes: [...sortedFileNodes, ...sortedMemoryNodes],
386
+ nodes: [...sortedFileNodes, ...sortedModuleNodes, ...sortedMemoryNodes, ...sortedConceptNodes],
261
387
  edges: sortedEdges,
262
388
  fallbackMode,
263
389
  provenance: {
@@ -276,10 +402,14 @@ export const handleGraphRoutes = async (ctx, req, res, url) => {
276
402
  },
277
403
  meta: {
278
404
  fileCount: fileNodes.length,
405
+ moduleCount: moduleNodes.length,
279
406
  memoryCount: memoryNodes.length,
407
+ conceptCount: conceptNodes.length,
280
408
  structuralEdgeCount: structuralEdges.length,
409
+ belongsToEdgeCount: belongsToEdges.length,
281
410
  memoryOverlayEdgeCount: memoryOverlayEdges.length,
282
- memoryAssociationEdgeCount: memoryAssociationEdges.length,
411
+ memoryAssociationEdgeCount: memoryAssocEdges.length,
412
+ conceptEdgeCount: conceptEdges.length,
283
413
  edgeCount: edges.length,
284
414
  graphSource,
285
415
  fallbackMode,
@@ -329,12 +329,16 @@ export class InstructionGateway {
329
329
  const workspaceRuntimeDir = path.join(repoRoot, '.agent', 'runtime');
330
330
  workspaceJsonPath = path.join(workspaceRuntimeDir, 'packet.json');
331
331
  workspaceMarkdownPath = path.join(workspaceRuntimeDir, 'packet.md');
332
- void fs.promises.mkdir(workspaceRuntimeDir, { recursive: true }).then(() => Promise.all([
333
- fs.promises.writeFile(workspaceJsonPath, JSON.stringify(packet, null, 2), 'utf8'),
334
- fs.promises.writeFile(workspaceMarkdownPath, renderInstructionPacketMarkdown(packet), 'utf8'),
335
- ])).catch((err) => {
332
+ // Sync write: callers read this file immediately after execute() returns.
333
+ // Fire-and-forget async races the reader and causes truncated JSON.parse failures.
334
+ try {
335
+ fs.mkdirSync(workspaceRuntimeDir, { recursive: true });
336
+ fs.writeFileSync(workspaceJsonPath, JSON.stringify(packet, null, 2), 'utf8');
337
+ fs.writeFileSync(workspaceMarkdownPath, renderInstructionPacketMarkdown(packet), 'utf8');
338
+ }
339
+ catch (err) {
336
340
  console.error(`[nexus-prime] Failed to persist instruction packet to workspace: ${err}`);
337
- });
341
+ }
338
342
  }
339
343
  else if (isRoot) {
340
344
  console.error('[nexus-prime] Persistence to system root is disabled for stability.');
@@ -2782,8 +2782,8 @@ export class SubAgentRuntime {
2782
2782
  return;
2783
2783
  const runtimeDir = path.join(worktreeDir, '.agent', 'runtime');
2784
2784
  fs.mkdirSync(runtimeDir, { recursive: true });
2785
- void fs.promises.writeFile(path.join(runtimeDir, 'packet.json'), JSON.stringify(packet, null, 2), 'utf8');
2786
- void fs.promises.writeFile(path.join(runtimeDir, 'packet.md'), renderInstructionPacketMarkdown(packet), 'utf8');
2785
+ fs.writeFileSync(path.join(runtimeDir, 'packet.json'), JSON.stringify(packet, null, 2), 'utf8');
2786
+ fs.writeFileSync(path.join(runtimeDir, 'packet.md'), renderInstructionPacketMarkdown(packet), 'utf8');
2787
2787
  }
2788
2788
  gatherSkillBindings(skills, allowMutateSkills) {
2789
2789
  return skills.flatMap((skill) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.3.1",
3
+ "version": "7.4.0",
4
4
  "description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",