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',
|
|
233
|
-
['Edges',
|
|
234
|
-
['Files',
|
|
235
|
-
['
|
|
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(
|
|
133
|
-
.force('charge', d3.forceManyBody().strength(d => d
|
|
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) +
|
|
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
|
|
142
|
-
.attr('stroke-opacity', d => d
|
|
143
|
-
.attr('stroke-dasharray', d =>
|
|
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
|
-
|
|
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(-
|
|
188
|
-
rect.setAttribute('y', String(-
|
|
189
|
-
rect.setAttribute('width', String(
|
|
190
|
-
rect.setAttribute('height', String(
|
|
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')
|
|
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')
|
|
208
|
-
if (type === 'configures')
|
|
209
|
-
if (type === 'tests')
|
|
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')
|
|
215
|
-
if (type === 'tests')
|
|
216
|
-
if (type === 'configures')
|
|
217
|
-
if (type === 'memory-overlay')
|
|
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')
|
|
225
|
-
if (tier === 'hippocampus' || tier === 'episodic')
|
|
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
|
|
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
|
-
'.
|
|
13
|
-
|
|
14
|
-
|
|
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((
|
|
151
|
-
const fileRelSet = new Set(fileNodes.map((
|
|
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((
|
|
169
|
-
.
|
|
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
|
|
173
|
-
pushEdge(structuralEdges, structuralSeen, node.id,
|
|
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((
|
|
179
|
-
.map((
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
187
|
-
pushEdge(structuralEdges, structuralSeen, node.id,
|
|
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((
|
|
193
|
-
id: `memory:${
|
|
257
|
+
const memoryNodes = snapshots.map((s) => ({
|
|
258
|
+
id: `memory:${s.id}`,
|
|
194
259
|
type: 'memory',
|
|
195
|
-
label: (
|
|
196
|
-
tier:
|
|
197
|
-
priority:
|
|
198
|
-
tags: Array.isArray(
|
|
199
|
-
promoted:
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
const
|
|
214
|
-
for (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
structuralEdges.some((
|
|
242
|
-
structuralEdges.some((
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
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 ordering — stable 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
|
|
252
|
-
|
|
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:
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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.');
|
package/dist/phantom/runtime.js
CHANGED
|
@@ -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
|
-
|
|
2786
|
-
|
|
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
|
+
"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",
|