@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.
- package/README.md +8 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +189 -0
- package/dist/application/index-vault.js +44 -165
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +39 -3
- package/dist/domain/embeddings.js +31 -5
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/infrastructure/config.js +6 -0
- package/dist/infrastructure/file-index.js +13 -4
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +504 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +247 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -1,625 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'10-agent-memory': 'agent-memory',
|
|
6
|
-
'20-concepts': 'concepts',
|
|
7
|
-
'30-architecture': 'architecture',
|
|
8
|
-
'40-agents': 'agents',
|
|
9
|
-
'50-retrieval': 'retrieval',
|
|
10
|
-
'60-operations': 'operations',
|
|
11
|
-
'70-evaluation': 'evaluation',
|
|
12
|
-
'80-sessions': 'sessions',
|
|
13
|
-
'90-security': 'security',
|
|
14
|
-
root: 'root'
|
|
15
|
-
};
|
|
16
|
-
const segmentAngles = {
|
|
17
|
-
Brainlink: -1.58,
|
|
18
|
-
Architecture: -0.74,
|
|
19
|
-
Agents: -0.05,
|
|
20
|
-
Retrieval: 0.68,
|
|
21
|
-
Operations: 1.34,
|
|
22
|
-
Evaluation: 2.08,
|
|
23
|
-
Security: 2.82
|
|
24
|
-
};
|
|
25
|
-
const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
26
|
-
const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
|
|
27
|
-
const jitter = (value, range) => {
|
|
28
|
-
const normalized = Math.abs(hashText(value) % 1000) / 1000;
|
|
29
|
-
return (normalized - 0.5) * range;
|
|
30
|
-
};
|
|
31
|
-
const pathSegments = (path) => path.split('/').filter(Boolean);
|
|
32
|
-
const groupKey = (node) => {
|
|
33
|
-
const segments = pathSegments(node.path);
|
|
34
|
-
if (segments[0] === 'agents') {
|
|
35
|
-
return segments[2] ?? 'root';
|
|
36
|
-
}
|
|
37
|
-
return segments[0] ?? 'root';
|
|
38
|
-
};
|
|
39
|
-
const groupLabel = (key) => groupLabels[key] ?? key;
|
|
40
|
-
const incrementDegreeBy = (degrees, id, amount) => {
|
|
41
|
-
degrees.set(id, (degrees.get(id) ?? 0) + amount);
|
|
42
|
-
return degrees;
|
|
43
|
-
};
|
|
44
|
-
const edgeDegreeWeight = (edge) => Math.max(1, Math.min(edge.weight, 8));
|
|
45
|
-
const countDegrees = (edges) => edges.reduce((degrees, edge) => {
|
|
46
|
-
const weight = edgeDegreeWeight(edge);
|
|
47
|
-
return edge.target
|
|
48
|
-
? incrementDegreeBy(incrementDegreeBy(degrees, edge.source, weight), edge.target, weight)
|
|
49
|
-
: incrementDegreeBy(degrees, edge.source, weight);
|
|
50
|
-
}, new Map());
|
|
51
|
-
const createAdjacency = (nodes, edges) => {
|
|
52
|
-
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
53
|
-
const adjacency = new Map(nodes.map((node) => [node.id, new Set()]));
|
|
54
|
-
edges.forEach((edge) => {
|
|
55
|
-
if (!edge.target || !nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
adjacency.get(edge.source)?.add(edge.target);
|
|
59
|
-
adjacency.get(edge.target)?.add(edge.source);
|
|
60
|
-
});
|
|
61
|
-
return new Map(Array.from(adjacency.entries(), ([id, neighbors]) => [id, Array.from(neighbors)]));
|
|
62
|
-
};
|
|
63
|
-
const byTitle = (left, right) => left.title.localeCompare(right.title);
|
|
64
|
-
const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
65
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
66
|
-
return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
|
|
67
|
-
};
|
|
68
|
-
const hubScore = (node) => {
|
|
69
|
-
const title = node.title.trim().toLowerCase();
|
|
70
|
-
if (title === 'memory hub')
|
|
71
|
-
return 5;
|
|
72
|
-
if (title === 'knowledge root')
|
|
73
|
-
return 4;
|
|
74
|
-
if (/\bmoc\b/i.test(node.title))
|
|
75
|
-
return 3;
|
|
76
|
-
return hubTitlePattern.test(node.title) ? 2 : 0;
|
|
77
|
-
};
|
|
78
|
-
const selectPrimaryHubId = (nodes, degrees) => {
|
|
79
|
-
const ranked = [...nodes]
|
|
80
|
-
.filter((node) => hubScore(node) > 0)
|
|
81
|
-
.sort((left, right) => {
|
|
82
|
-
const scoreDelta = hubScore(right) - hubScore(left);
|
|
83
|
-
if (scoreDelta !== 0)
|
|
84
|
-
return scoreDelta;
|
|
85
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
86
|
-
if (degreeDelta !== 0)
|
|
87
|
-
return degreeDelta;
|
|
88
|
-
return left.title.localeCompare(right.title);
|
|
89
|
-
});
|
|
90
|
-
return ranked[0]?.id ?? null;
|
|
91
|
-
};
|
|
92
|
-
const selectHighestDegreeNodeId = (nodes, degrees) => [...nodes].sort((left, right) => {
|
|
93
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
94
|
-
return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
|
|
95
|
-
})[0]?.id ?? null;
|
|
96
|
-
const centerLayoutByNode = (nodes, nodeId) => {
|
|
97
|
-
if (!nodeId) {
|
|
98
|
-
return nodes;
|
|
99
|
-
}
|
|
100
|
-
const anchor = nodes.find((node) => node.id === nodeId);
|
|
101
|
-
if (!anchor) {
|
|
102
|
-
return nodes;
|
|
103
|
-
}
|
|
104
|
-
return nodes.map((node) => ({
|
|
105
|
-
...node,
|
|
106
|
-
x: node.x - anchor.x,
|
|
107
|
-
y: node.y - anchor.y
|
|
108
|
-
}));
|
|
109
|
-
};
|
|
110
|
-
const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
|
|
111
|
-
const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
|
|
112
|
-
const collectComponent = (adjacency, startId, visited) => {
|
|
113
|
-
const queue = [startId];
|
|
114
|
-
const component = [];
|
|
115
|
-
visited.add(startId);
|
|
116
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
117
|
-
const id = queue[index];
|
|
118
|
-
component.push(id);
|
|
119
|
-
(adjacency.get(id) ?? []).forEach((nextId) => {
|
|
120
|
-
if (!visited.has(nextId)) {
|
|
121
|
-
visited.add(nextId);
|
|
122
|
-
queue.push(nextId);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return component;
|
|
127
|
-
};
|
|
128
|
-
const connectedComponents = (nodes, adjacency) => {
|
|
129
|
-
const visited = new Set();
|
|
130
|
-
return [...nodes].sort(byTitle).reduce((components, node) => (visited.has(node.id) ? components : [...components, collectComponent(adjacency, node.id, visited)]), []);
|
|
131
|
-
};
|
|
132
|
-
const selectSegmentSeeds = (nodes, edges, degrees) => {
|
|
133
|
-
const adjacency = createAdjacency(nodes, edges);
|
|
134
|
-
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
135
|
-
return connectedComponents(nodes, adjacency).flatMap((component) => {
|
|
136
|
-
const componentNodes = component.map((id) => nodeById.get(id)).filter((node) => Boolean(node));
|
|
137
|
-
const naturalSeeds = componentNodes.filter(naturalSegmentSeed).sort(byDegreeThenTitle(degrees));
|
|
138
|
-
return naturalSeeds.length > 0 ? naturalSeeds : componentNodes.sort(byDegreeThenTitle(degrees)).slice(0, 1);
|
|
139
|
-
});
|
|
140
|
-
};
|
|
141
|
-
const assignSegments = (nodes, edges, degrees) => {
|
|
142
|
-
const adjacency = createAdjacency(nodes, edges);
|
|
143
|
-
const seeds = selectSegmentSeeds(nodes, edges, degrees);
|
|
144
|
-
const assignments = new Map(nodes.flatMap((node) => {
|
|
145
|
-
const visualContext = inferExplicitVisualGraphContext(node);
|
|
146
|
-
return visualContext ? [[node.id, visualContext.title]] : [];
|
|
147
|
-
}));
|
|
148
|
-
const queue = seeds.map((seed) => seed.id);
|
|
149
|
-
seeds.forEach((seed) => {
|
|
150
|
-
if (!assignments.has(seed.id)) {
|
|
151
|
-
assignments.set(seed.id, segmentName(seed));
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
155
|
-
const id = queue[index];
|
|
156
|
-
const segment = assignments.get(id);
|
|
157
|
-
if (!segment) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
;
|
|
161
|
-
(adjacency.get(id) ?? []).forEach((nextId) => {
|
|
162
|
-
if (!assignments.has(nextId)) {
|
|
163
|
-
assignments.set(nextId, segment);
|
|
164
|
-
queue.push(nextId);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return new Map(nodes.map((node) => [node.id, assignments.get(node.id) ?? groupLabel(groupKey(node))]));
|
|
169
|
-
};
|
|
170
|
-
const groupNodesBySegment = (nodes, segments) => {
|
|
171
|
-
const groups = new Map();
|
|
172
|
-
nodes.forEach((node) => {
|
|
173
|
-
const segment = segments.get(node.id) ?? groupLabel(groupKey(node));
|
|
174
|
-
const bucket = groups.get(segment);
|
|
175
|
-
if (bucket) {
|
|
176
|
-
bucket.push(node);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
groups.set(segment, [node]);
|
|
180
|
-
});
|
|
181
|
-
return new Map(groups);
|
|
182
|
-
};
|
|
183
|
-
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
184
|
-
const compareByStarOrder = (levels, degrees, segmentIndexByName, segments) => (left, right) => {
|
|
185
|
-
const levelDelta = (levels.get(left.id) ?? 1) - (levels.get(right.id) ?? 1);
|
|
186
|
-
if (levelDelta !== 0)
|
|
187
|
-
return levelDelta;
|
|
188
|
-
const leftSegment = segments.get(left.id) ?? groupLabel(groupKey(left));
|
|
189
|
-
const rightSegment = segments.get(right.id) ?? groupLabel(groupKey(right));
|
|
190
|
-
const segmentDelta = (segmentIndexByName.get(leftSegment) ?? 0) - (segmentIndexByName.get(rightSegment) ?? 0);
|
|
191
|
-
if (segmentDelta !== 0)
|
|
192
|
-
return segmentDelta;
|
|
193
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
194
|
-
return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
|
|
195
|
-
};
|
|
196
|
-
const petalRadiusForSegmentSize = (size) => {
|
|
197
|
-
const safeSize = Math.max(size, 1);
|
|
198
|
-
return Math.max(260, Math.sqrt(safeSize) * 96);
|
|
199
|
-
};
|
|
200
|
-
const selectSegmentHub = (nodes, degrees, primaryHubId) => {
|
|
201
|
-
const primary = nodes.find((node) => node.id === primaryHubId);
|
|
202
|
-
if (primary) {
|
|
203
|
-
return primary;
|
|
204
|
-
}
|
|
205
|
-
return [...nodes].sort((left, right) => {
|
|
206
|
-
const hubDelta = hubScore(right) - hubScore(left);
|
|
207
|
-
if (hubDelta !== 0)
|
|
208
|
-
return hubDelta;
|
|
209
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
210
|
-
if (degreeDelta !== 0)
|
|
211
|
-
return degreeDelta;
|
|
212
|
-
return left.title.localeCompare(right.title);
|
|
213
|
-
})[0] ?? null;
|
|
214
|
-
};
|
|
215
|
-
const segmentCenterRadius = (segments) => {
|
|
216
|
-
if (segments.length <= 1) {
|
|
217
|
-
return 0;
|
|
218
|
-
}
|
|
219
|
-
const circumference = segments.reduce((total, [, nodes]) => total + petalRadiusForSegmentSize(nodes.length) * 2 + 180, 0);
|
|
220
|
-
return Math.max(520, circumference / (Math.PI * 2));
|
|
221
|
-
};
|
|
222
|
-
const createCauliflowerSegmentNodes = (segments, degrees, rootHubId, segmentGroups) => ([segment, nodes], segmentIndex) => {
|
|
223
|
-
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
224
|
-
const segmentHub = selectSegmentHub(sortedNodes, degrees, rootHubId);
|
|
225
|
-
const angle = segmentAngle(segment, segmentIndex, segmentGroups.length);
|
|
226
|
-
const globalRadius = segmentCenterRadius(segmentGroups);
|
|
227
|
-
const petalRadius = petalRadiusForSegmentSize(sortedNodes.length);
|
|
228
|
-
const isPrimarySegment = Boolean(segmentHub && segmentHub.id === rootHubId);
|
|
229
|
-
const centerX = isPrimarySegment || globalRadius === 0 ? 0 : Math.cos(angle) * globalRadius;
|
|
230
|
-
const centerY = isPrimarySegment || globalRadius === 0 ? 0 : Math.sin(angle) * (globalRadius * 0.86);
|
|
231
|
-
const nonHubNodes = sortedNodes.filter((node) => node.id !== segmentHub?.id);
|
|
232
|
-
const hubNode = segmentHub
|
|
233
|
-
? [{
|
|
234
|
-
...segmentHub,
|
|
235
|
-
group: groupLabel(groupKey(segmentHub)),
|
|
236
|
-
segment: segments.get(segmentHub.id) ?? segment,
|
|
237
|
-
x: centerX,
|
|
238
|
-
y: centerY
|
|
239
|
-
}]
|
|
240
|
-
: [];
|
|
241
|
-
const petalNodes = nonHubNodes.map((node, index) => {
|
|
242
|
-
const localAngle = index * 2.399963 + jitter(node.title, 0.5);
|
|
243
|
-
const radialLayer = Math.sqrt(index + 1) / Math.sqrt(Math.max(nonHubNodes.length, 1));
|
|
244
|
-
const localRadius = 150 + radialLayer * petalRadius + jitter(node.id, 34);
|
|
245
|
-
const degreePull = Math.min(degrees.get(node.id) ?? 0, 16) * 8;
|
|
246
|
-
const radius = Math.max(126, localRadius - degreePull);
|
|
247
|
-
return {
|
|
248
|
-
...node,
|
|
249
|
-
group: groupLabel(groupKey(node)),
|
|
250
|
-
segment: segments.get(node.id) ?? segment,
|
|
251
|
-
x: centerX + Math.cos(localAngle) * radius + jitter(node.title, 20),
|
|
252
|
-
y: centerY + Math.sin(localAngle) * radius * 0.84 + jitter(node.path, 20)
|
|
253
|
-
};
|
|
254
|
-
});
|
|
255
|
-
return [...hubNode, ...petalNodes];
|
|
256
|
-
};
|
|
257
|
-
const createVisualEdge = (source, target, weight, priority) => ({
|
|
258
|
-
source: source.id,
|
|
259
|
-
target: target.id,
|
|
260
|
-
targetTitle: target.title,
|
|
261
|
-
weight,
|
|
262
|
-
priority
|
|
263
|
-
});
|
|
264
|
-
const createCauliflowerVisualEdges = (segmentGroups, degrees, rootHubId) => {
|
|
265
|
-
const nodeById = new Map(segmentGroups.flatMap(([, nodes]) => nodes.map((node) => [node.id, node])));
|
|
266
|
-
const rootHub = rootHubId ? nodeById.get(rootHubId) ?? null : null;
|
|
267
|
-
const edges = new Map();
|
|
268
|
-
const addEdge = (edge) => {
|
|
269
|
-
if (!edge.target || edge.source === edge.target) {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
edges.set(`${edge.source}|${edge.target}`, edge);
|
|
273
|
-
};
|
|
274
|
-
segmentGroups.forEach(([, nodes]) => {
|
|
275
|
-
const segmentHub = selectSegmentHub(nodes, degrees, rootHubId);
|
|
276
|
-
const parent = segmentHub ?? rootHub;
|
|
277
|
-
if (!parent) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (rootHub && parent.id !== rootHub.id) {
|
|
281
|
-
addEdge(createVisualEdge(rootHub, parent, 6, 'high'));
|
|
282
|
-
}
|
|
283
|
-
nodes.forEach((node) => {
|
|
284
|
-
if (node.id === parent.id || node.id === rootHub?.id) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
addEdge(createVisualEdge(parent, node, 1, 'low'));
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
return Array.from(edges.values());
|
|
291
|
-
};
|
|
292
|
-
const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
|
|
293
|
-
const layoutBounds = (nodes) => {
|
|
294
|
-
if (nodes.length === 0) {
|
|
295
|
-
return { x: 0, y: 0, radius: 1 };
|
|
296
|
-
}
|
|
297
|
-
const bounds = nodes.reduce((current, node) => ({
|
|
298
|
-
minX: Math.min(current.minX, node.x),
|
|
299
|
-
maxX: Math.max(current.maxX, node.x),
|
|
300
|
-
minY: Math.min(current.minY, node.y),
|
|
301
|
-
maxY: Math.max(current.maxY, node.y)
|
|
302
|
-
}), {
|
|
303
|
-
minX: Number.POSITIVE_INFINITY,
|
|
304
|
-
maxX: Number.NEGATIVE_INFINITY,
|
|
305
|
-
minY: Number.POSITIVE_INFINITY,
|
|
306
|
-
maxY: Number.NEGATIVE_INFINITY
|
|
307
|
-
});
|
|
308
|
-
const x = (bounds.minX + bounds.maxX) / 2;
|
|
309
|
-
const y = (bounds.minY + bounds.maxY) / 2;
|
|
310
|
-
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
|
|
311
|
-
return { x, y, radius: Math.max(radius + 72, 120) };
|
|
312
|
-
};
|
|
313
|
-
const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
|
|
314
|
-
const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
|
|
315
|
-
const groupTitle = (segment, level, index, nodes) => nodes.length === 1
|
|
316
|
-
? nodes[0]?.title ?? segment
|
|
317
|
-
: `${segment} ${level + 1}.${index + 1}`;
|
|
318
|
-
const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
319
|
-
const sortedNodes = [...nodes].sort((left, right) => {
|
|
320
|
-
const segmentDelta = left.segment.localeCompare(right.segment);
|
|
321
|
-
if (segmentDelta !== 0)
|
|
322
|
-
return segmentDelta;
|
|
323
|
-
const groupDelta = left.group.localeCompare(right.group);
|
|
324
|
-
if (groupDelta !== 0)
|
|
325
|
-
return groupDelta;
|
|
326
|
-
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
327
|
-
if (degreeDelta !== 0)
|
|
328
|
-
return degreeDelta;
|
|
329
|
-
return left.title.localeCompare(right.title);
|
|
330
|
-
});
|
|
331
|
-
const groupCountTarget = Math.min(groupNodeLimit, sortedNodes.length);
|
|
332
|
-
const chunkSize = sortedNodes.length <= groupNodeLimit * groupNodeLimit
|
|
333
|
-
? Math.max(1, Math.ceil(sortedNodes.length / groupCountTarget))
|
|
334
|
-
: groupNodeLimit;
|
|
335
|
-
const chunks = [];
|
|
336
|
-
for (let index = 0; index < sortedNodes.length; index += chunkSize) {
|
|
337
|
-
chunks.push(sortedNodes.slice(index, index + chunkSize));
|
|
338
|
-
}
|
|
339
|
-
return chunks;
|
|
340
|
-
};
|
|
341
|
-
const groupEdges = (edges, nodeIds) => ({
|
|
342
|
-
internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
|
|
343
|
-
externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
|
|
344
|
-
});
|
|
345
|
-
const groupBounds = (groups) => {
|
|
346
|
-
if (groups.length === 0) {
|
|
347
|
-
return { x: 0, y: 0, radius: 1 };
|
|
348
|
-
}
|
|
349
|
-
const nodes = groups.map((group) => ({
|
|
350
|
-
x: group.x,
|
|
351
|
-
y: group.y,
|
|
352
|
-
radius: group.radius
|
|
353
|
-
}));
|
|
354
|
-
const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
|
|
355
|
-
const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
|
|
356
|
-
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
|
|
357
|
-
return { x, y, radius: Math.max(radius + 120, 180) };
|
|
358
|
-
};
|
|
359
|
-
const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
|
|
360
|
-
const createParentGroups = (groups, edges, level, groupNodeLimit) => {
|
|
361
|
-
if (groups.length <= groupNodeLimit) {
|
|
362
|
-
return groups;
|
|
363
|
-
}
|
|
364
|
-
const parentGroups = [];
|
|
365
|
-
for (let index = 0; index < groups.length; index += groupNodeLimit) {
|
|
366
|
-
const chunk = groups.slice(index, index + groupNodeLimit);
|
|
367
|
-
const nodeIds = new Set(descendantNodeIds(chunk));
|
|
368
|
-
const bounds = groupBounds(chunk);
|
|
369
|
-
const segment = chunk[0]?.segment ?? 'root';
|
|
370
|
-
const group = chunk[0]?.group ?? 'root';
|
|
371
|
-
const groupIndex = index / groupNodeLimit;
|
|
372
|
-
const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
373
|
-
const edgeGroups = groupEdges(edges, nodeIds);
|
|
374
|
-
parentGroups.push({
|
|
375
|
-
id,
|
|
376
|
-
level,
|
|
377
|
-
parentId: null,
|
|
378
|
-
title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
|
|
379
|
-
segment,
|
|
380
|
-
group,
|
|
381
|
-
x: bounds.x,
|
|
382
|
-
y: bounds.y,
|
|
383
|
-
radius: bounds.radius,
|
|
384
|
-
nodeIds: [],
|
|
385
|
-
childGroupIds: chunk.map((child) => child.id),
|
|
386
|
-
internalEdges: edgeGroups.internalEdges,
|
|
387
|
-
externalEdges: edgeGroups.externalEdges
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
const relinkedChildren = groups.map((group) => {
|
|
391
|
-
const parent = parentGroups.find((candidate) => candidate.childGroupIds.includes(group.id));
|
|
392
|
-
return parent ? { ...group, parentId: parent.id } : group;
|
|
393
|
-
});
|
|
394
|
-
return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
|
|
395
|
-
};
|
|
396
|
-
export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
397
|
-
if (nodes.length <= groupNodeLimit) {
|
|
398
|
-
return [];
|
|
399
|
-
}
|
|
400
|
-
const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
|
|
401
|
-
const nodeIds = new Set(chunk.map((node) => node.id));
|
|
402
|
-
const bounds = layoutBounds(chunk);
|
|
403
|
-
const segment = chunk[0]?.segment ?? 'root';
|
|
404
|
-
const group = chunk[0]?.group ?? 'root';
|
|
405
|
-
const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
406
|
-
return {
|
|
407
|
-
id,
|
|
408
|
-
level: 0,
|
|
409
|
-
parentId: null,
|
|
410
|
-
title: groupTitle(segment, 0, index, chunk),
|
|
411
|
-
segment,
|
|
412
|
-
group,
|
|
413
|
-
x: bounds.x,
|
|
414
|
-
y: bounds.y,
|
|
415
|
-
radius: bounds.radius,
|
|
416
|
-
nodeIds: chunk.map((node) => node.id),
|
|
417
|
-
childGroupIds: [],
|
|
418
|
-
...groupEdges(edges, nodeIds)
|
|
419
|
-
};
|
|
420
|
-
});
|
|
421
|
-
return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
|
|
422
|
-
};
|
|
423
|
-
const resolveCollisionPair = (left, right, minDistance) => {
|
|
424
|
-
const dx = right.x - left.x;
|
|
425
|
-
const dy = right.y - left.y;
|
|
426
|
-
const distance = Math.max(Math.hypot(dx, dy), 0.001);
|
|
427
|
-
if (distance >= minDistance) {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
const push = (minDistance - distance) / 2;
|
|
431
|
-
const fallbackAngle = Math.PI * 2 * (Math.abs(hashText(`${left.id}:${right.id}`) % 1000) / 1000);
|
|
432
|
-
const ux = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.cos(fallbackAngle) : dx / distance;
|
|
433
|
-
const uy = Math.abs(dx) + Math.abs(dy) < 0.001 ? Math.sin(fallbackAngle) : dy / distance;
|
|
434
|
-
left.x -= ux * push;
|
|
435
|
-
left.y -= uy * push;
|
|
436
|
-
right.x += ux * push;
|
|
437
|
-
right.y += uy * push;
|
|
438
|
-
};
|
|
439
|
-
const buildCollisionGrid = (nodes, cellSize) => {
|
|
440
|
-
const grid = new Map();
|
|
441
|
-
nodes.forEach((node, index) => {
|
|
442
|
-
const x = Math.floor(node.x / cellSize);
|
|
443
|
-
const y = Math.floor(node.y / cellSize);
|
|
444
|
-
const key = `${x},${y}`;
|
|
445
|
-
const bucket = grid.get(key);
|
|
446
|
-
if (bucket) {
|
|
447
|
-
bucket.push(index);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
grid.set(key, [index]);
|
|
451
|
-
});
|
|
452
|
-
return grid;
|
|
453
|
-
};
|
|
454
|
-
const neighborCellKeys = (x, y) => [
|
|
455
|
-
`${x - 1},${y - 1}`,
|
|
456
|
-
`${x},${y - 1}`,
|
|
457
|
-
`${x + 1},${y - 1}`,
|
|
458
|
-
`${x - 1},${y}`,
|
|
459
|
-
`${x},${y}`,
|
|
460
|
-
`${x + 1},${y}`,
|
|
461
|
-
`${x - 1},${y + 1}`,
|
|
462
|
-
`${x},${y + 1}`,
|
|
463
|
-
`${x + 1},${y + 1}`
|
|
464
|
-
];
|
|
465
|
-
const resolveCollisionsSpatial = (nodes, minDistance) => {
|
|
466
|
-
const gridCellSize = minDistance * 1.05;
|
|
467
|
-
const grid = buildCollisionGrid(nodes, gridCellSize);
|
|
468
|
-
for (let index = 0; index < nodes.length; index += 1) {
|
|
469
|
-
const left = nodes[index];
|
|
470
|
-
const leftCellX = Math.floor(left.x / gridCellSize);
|
|
471
|
-
const leftCellY = Math.floor(left.y / gridCellSize);
|
|
472
|
-
neighborCellKeys(leftCellX, leftCellY).forEach((key) => {
|
|
473
|
-
const candidateIndices = grid.get(key) ?? [];
|
|
474
|
-
candidateIndices.forEach((candidateIndex) => {
|
|
475
|
-
if (candidateIndex <= index) {
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
resolveCollisionPair(left, nodes[candidateIndex], minDistance);
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
const resolveCollisionsBrute = (nodes, minDistance) => {
|
|
484
|
-
for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) {
|
|
485
|
-
const left = nodes[leftIndex];
|
|
486
|
-
for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) {
|
|
487
|
-
resolveCollisionPair(left, nodes[rightIndex], minDistance);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
const relaxCollisions = (nodes, minDistance = 132, rounds = 32) => {
|
|
492
|
-
if (nodes.length <= 1) {
|
|
493
|
-
return nodes;
|
|
494
|
-
}
|
|
495
|
-
const effectiveRounds = nodes.length > 1000
|
|
496
|
-
? Math.min(rounds, 12)
|
|
497
|
-
: nodes.length > 500
|
|
498
|
-
? Math.min(rounds, 20)
|
|
499
|
-
: Math.min(rounds, 26);
|
|
500
|
-
const layoutNodes = nodes.map((node) => ({
|
|
501
|
-
...node,
|
|
502
|
-
x: Number.isFinite(node.x) ? node.x : 0,
|
|
503
|
-
y: Number.isFinite(node.y) ? node.y : 0
|
|
504
|
-
}));
|
|
505
|
-
for (let round = 0; round < effectiveRounds; round += 1) {
|
|
506
|
-
if (nodes.length <= 250) {
|
|
507
|
-
resolveCollisionsBrute(layoutNodes, minDistance);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
resolveCollisionsSpatial(layoutNodes, minDistance);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return layoutNodes;
|
|
514
|
-
};
|
|
515
|
-
const assignStarLevels = (nodes, edges, hubId) => {
|
|
516
|
-
if (!hubId) {
|
|
517
|
-
return new Map(nodes.map((node) => [node.id, 1]));
|
|
518
|
-
}
|
|
519
|
-
const adjacency = createAdjacency(nodes, edges);
|
|
520
|
-
const levels = new Map([[hubId, 0]]);
|
|
521
|
-
const queue = [hubId];
|
|
522
|
-
for (let index = 0; index < queue.length; index += 1) {
|
|
523
|
-
const id = queue[index];
|
|
524
|
-
const nextLevel = (levels.get(id) ?? 0) + 1;
|
|
525
|
-
(adjacency.get(id) ?? []).forEach((nextId) => {
|
|
526
|
-
if (!levels.has(nextId)) {
|
|
527
|
-
levels.set(nextId, nextLevel);
|
|
528
|
-
queue.push(nextId);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
nodes.forEach((node) => {
|
|
533
|
-
if (!levels.has(node.id)) {
|
|
534
|
-
levels.set(node.id, 2);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
return levels;
|
|
538
|
-
};
|
|
539
|
-
const createStarNodes = (nodes, segments, degrees, hubId, levels) => {
|
|
540
|
-
const segmentNames = Array.from(new Set(nodes.map((node) => segments.get(node.id) ?? groupLabel(groupKey(node)))))
|
|
541
|
-
.sort((left, right) => segmentAngle(left, 0, 1) - segmentAngle(right, 0, 1) || left.localeCompare(right));
|
|
542
|
-
const segmentIndexByName = new Map(segmentNames.map((segment, index) => [segment, index]));
|
|
543
|
-
const sortedNodes = [...nodes].sort(compareByStarOrder(levels, degrees, segmentIndexByName, segments));
|
|
544
|
-
const nodesByLevel = sortedNodes.reduce((state, node) => {
|
|
545
|
-
const level = node.id === hubId ? 0 : Math.max(1, levels.get(node.id) ?? 1);
|
|
546
|
-
const levelNodes = state.get(level) ?? [];
|
|
547
|
-
levelNodes.push(node);
|
|
548
|
-
state.set(level, levelNodes);
|
|
549
|
-
return state;
|
|
550
|
-
}, new Map());
|
|
551
|
-
return Array.from(nodesByLevel.entries())
|
|
552
|
-
.sort(([left], [right]) => left - right)
|
|
553
|
-
.flatMap(([level, levelNodes]) => {
|
|
554
|
-
if (level === 0) {
|
|
555
|
-
return levelNodes.map((node) => ({
|
|
556
|
-
...node,
|
|
557
|
-
group: groupLabel(groupKey(node)),
|
|
558
|
-
segment: segments.get(node.id) ?? groupLabel(groupKey(node)),
|
|
559
|
-
x: 0,
|
|
560
|
-
y: 0
|
|
561
|
-
}));
|
|
562
|
-
}
|
|
563
|
-
const levelNodesBySegment = segmentNames
|
|
564
|
-
.map((segment) => ({
|
|
565
|
-
segment,
|
|
566
|
-
nodes: levelNodes.filter((node) => (segments.get(node.id) ?? groupLabel(groupKey(node))) === segment)
|
|
567
|
-
}))
|
|
568
|
-
.filter((group) => group.nodes.length > 0);
|
|
569
|
-
const totalNodes = levelNodesBySegment.reduce((total, group) => total + group.nodes.length, 0);
|
|
570
|
-
const baseRadius = Math.max(360 + (level - 1) * 460, (levelNodes.length * 156) / (Math.PI * 2));
|
|
571
|
-
let arcCursor = -Math.PI / 2;
|
|
572
|
-
return levelNodesBySegment.flatMap((group) => {
|
|
573
|
-
const arcSize = (Math.PI * 2 * group.nodes.length) / Math.max(totalNodes, 1);
|
|
574
|
-
const arcPadding = Math.min(0.22, arcSize * 0.18);
|
|
575
|
-
const arcStart = arcCursor + arcPadding;
|
|
576
|
-
const arcEnd = arcCursor + arcSize - arcPadding;
|
|
577
|
-
const usableArc = Math.max(0.001, arcEnd - arcStart);
|
|
578
|
-
const segmentRadius = Math.max(baseRadius, (group.nodes.length * 156) / usableArc);
|
|
579
|
-
arcCursor += arcSize;
|
|
580
|
-
return group.nodes.map((node, index) => {
|
|
581
|
-
const lane = index % 3 - 1;
|
|
582
|
-
const angle = arcStart + usableArc * ((index + 0.5) / Math.max(group.nodes.length, 1)) + jitter(node.title, 0.035);
|
|
583
|
-
const radialJitter = jitter(node.id, 34);
|
|
584
|
-
return {
|
|
585
|
-
...node,
|
|
586
|
-
group: groupLabel(groupKey(node)),
|
|
587
|
-
segment: group.segment,
|
|
588
|
-
x: Math.cos(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.title, 16),
|
|
589
|
-
y: Math.sin(angle) * (segmentRadius + lane * 52 + radialJitter) + jitter(node.path, 16)
|
|
590
|
-
};
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
};
|
|
595
|
-
export const createStarGraphLayout = (graph) => {
|
|
596
|
-
const degrees = countDegrees(graph.edges);
|
|
597
|
-
const hubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
598
|
-
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
599
|
-
const levels = assignStarLevels(graph.nodes, graph.edges, hubId);
|
|
600
|
-
const nodes = relaxCollisions(createStarNodes(graph.nodes, segments, degrees, hubId, levels), 156, 22);
|
|
601
|
-
const centeredNodes = centerLayoutByNode(nodes, hubId);
|
|
602
|
-
const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
|
|
603
|
-
return {
|
|
604
|
-
nodes: centeredNodes,
|
|
605
|
-
edges: graph.edges,
|
|
606
|
-
...(groups.length > 0 ? { groups } : {})
|
|
607
|
-
};
|
|
608
|
-
};
|
|
609
|
-
export const createCauliflowerGraphLayout = (graph) => {
|
|
610
|
-
const degrees = countDegrees(graph.edges);
|
|
611
|
-
const segments = assignSegments(graph.nodes, graph.edges, degrees);
|
|
612
|
-
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
613
|
-
.sort(([left], [right]) => left.localeCompare(right));
|
|
614
|
-
const rootHubId = selectPrimaryHubId(graph.nodes, degrees) ?? selectHighestDegreeNodeId(graph.nodes, degrees);
|
|
615
|
-
const nodes = relaxCollisions(segmentGroups.flatMap(createCauliflowerSegmentNodes(segments, degrees, rootHubId, segmentGroups)), 156, 28);
|
|
616
|
-
const centeredNodes = centerLayoutByNode(nodes, rootHubId);
|
|
617
|
-
const visualEdges = createCauliflowerVisualEdges(segmentGroups, degrees, rootHubId);
|
|
618
|
-
const groups = createGraphLayoutHierarchy(centeredNodes, visualEdges, degrees);
|
|
619
|
-
return {
|
|
620
|
-
nodes: centeredNodes,
|
|
621
|
-
edges: visualEdges,
|
|
622
|
-
...(groups.length > 0 ? { groups } : {})
|
|
623
|
-
};
|
|
624
|
-
};
|
|
625
|
-
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);
|
|
1
|
+
export { createGraphLayoutHierarchy } from './graph-layout/hierarchy.js';
|
|
2
|
+
export { createStarGraphLayout } from './graph-layout/star-layout.js';
|
|
3
|
+
export { createCauliflowerGraphLayout } from './graph-layout/cauliflower-layout.js';
|
|
4
|
+
export { getMinimumLayoutDistance } from './graph-layout/metrics.js';
|
|
@@ -11,6 +11,8 @@ export const defaultBrainlinkConfig = {
|
|
|
11
11
|
defaultAgent: undefined,
|
|
12
12
|
autoIndexOnWrite: true,
|
|
13
13
|
autoCanonicalContextLinks: true,
|
|
14
|
+
autoUpdateCheck: true,
|
|
15
|
+
updateCheckIntervalMs: 86_400_000,
|
|
14
16
|
defaultSearchLimit: 8,
|
|
15
17
|
defaultContextTokens: 1500,
|
|
16
18
|
defaultContextStrategy: 'auto',
|
|
@@ -177,6 +179,10 @@ const sanitizeConfig = (value) => ({
|
|
|
177
179
|
autoCanonicalContextLinks: typeof value.autoCanonicalContextLinks === 'boolean'
|
|
178
180
|
? value.autoCanonicalContextLinks
|
|
179
181
|
: defaultBrainlinkConfig.autoCanonicalContextLinks,
|
|
182
|
+
autoUpdateCheck: typeof value.autoUpdateCheck === 'boolean' ? value.autoUpdateCheck : defaultBrainlinkConfig.autoUpdateCheck,
|
|
183
|
+
updateCheckIntervalMs: typeof value.updateCheckIntervalMs === 'number' && Number.isFinite(value.updateCheckIntervalMs) && value.updateCheckIntervalMs > 0
|
|
184
|
+
? Math.floor(value.updateCheckIntervalMs)
|
|
185
|
+
: defaultBrainlinkConfig.updateCheckIntervalMs,
|
|
180
186
|
defaultSearchLimit: typeof value.defaultSearchLimit === 'number' && value.defaultSearchLimit > 0
|
|
181
187
|
? value.defaultSearchLimit
|
|
182
188
|
: defaultBrainlinkConfig.defaultSearchLimit,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { createEmbeddingBuckets, dotProduct } from '../domain/embeddings.js';
|
|
4
|
+
import { selectSemanticCandidates } from './semantic-prefilter.js';
|
|
4
5
|
const queryTokenPattern = /[\p{L}\p{N}_-]+/gu;
|
|
5
6
|
const indexCacheMaxEntries = 16;
|
|
6
7
|
const indexCache = new Map();
|
|
@@ -106,7 +107,7 @@ const textScore = (row, tokens) => {
|
|
|
106
107
|
return score + titleHits * 5 + tagHits * 4 + pathHits * 2 + Math.min(contentHits, 6);
|
|
107
108
|
}, 0);
|
|
108
109
|
};
|
|
109
|
-
const semanticScore = (row, queryEmbedding) => queryEmbedding.length > 0 && row.embedding.length > 0 ?
|
|
110
|
+
const semanticScore = (row, queryEmbedding) => queryEmbedding.length > 0 && row.embedding.length > 0 ? dotProduct(queryEmbedding, row.embedding) : 0;
|
|
110
111
|
const toResult = (row, mode, text, semantic) => {
|
|
111
112
|
const score = mode === 'fts' ? text : mode === 'semantic' ? semantic : text + semantic * 8;
|
|
112
113
|
return {
|
|
@@ -206,12 +207,20 @@ export const openFileIndex = (vaultPath) => {
|
|
|
206
207
|
chunkOrdinal: chunk.ordinal,
|
|
207
208
|
content: chunk.content,
|
|
208
209
|
tags: document.tags,
|
|
209
|
-
embedding: chunk.embedding
|
|
210
|
+
embedding: chunk.embedding,
|
|
211
|
+
buckets: chunk.buckets
|
|
210
212
|
}
|
|
211
213
|
];
|
|
212
214
|
});
|
|
213
215
|
const tokens = tokenize(query);
|
|
214
|
-
|
|
216
|
+
// Pure-semantic scoring on a large vault can skip chunks that share no
|
|
217
|
+
// embedding bucket with the query. Hybrid/fts keep every row so lexical
|
|
218
|
+
// matches are never dropped; the prefilter also falls back to a full scan
|
|
219
|
+
// on small or partially-indexed vaults.
|
|
220
|
+
const scored = mode === 'semantic' && queryEmbedding.length > 0
|
|
221
|
+
? selectSemanticCandidates(rows, createEmbeddingBuckets(queryEmbedding), limit)
|
|
222
|
+
: rows;
|
|
223
|
+
const results = scored
|
|
215
224
|
.map((row) => {
|
|
216
225
|
const text = textScore(row, tokens);
|
|
217
226
|
const semantic = semanticScore(row, queryEmbedding);
|