@andespindola/brainlink 0.1.0-beta.13 → 0.1.0-beta.131
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/AGENTS.md +8 -5
- package/CHANGELOG.md +26 -2
- package/CONTRIBUTING.md +2 -2
- package/COPYRIGHT.md +5 -0
- package/README.md +143 -20
- package/SECURITY.md +1 -1
- package/dist/application/analyze-vault.js +1 -15
- package/dist/application/build-context.js +64 -3
- package/dist/application/dedupe-notes.js +226 -0
- package/dist/application/frontend/client-css.js +93 -45
- package/dist/application/frontend/client-html.js +34 -25
- package/dist/application/frontend/client-js.js +2790 -162
- package/dist/application/frontend/client-worker-js.js +66 -0
- package/dist/application/get-graph-layout.js +39 -6
- package/dist/application/get-graph-node.js +3 -3
- package/dist/application/get-graph-summary.js +3 -3
- package/dist/application/get-graph-view.js +243 -0
- package/dist/application/get-graph.js +3 -3
- package/dist/application/import-legacy-sqlite.js +296 -0
- package/dist/application/index-vault.js +253 -25
- package/dist/application/list-agents.js +3 -3
- package/dist/application/list-links.js +5 -5
- package/dist/application/offline-pack-backup.js +44 -0
- package/dist/application/search-graph-node-ids.js +3 -3
- package/dist/application/search-knowledge.js +6 -6
- package/dist/application/server/routes.js +105 -1
- package/dist/application/start-server.js +75 -4
- package/dist/application/watch-vault.js +23 -2
- package/dist/benchmarks/large-vault.js +1 -1
- package/dist/cli/commands/agent-commands.js +7 -0
- package/dist/cli/commands/write-commands.js +842 -8
- package/dist/domain/context.js +54 -11
- package/dist/domain/graph-layout.js +181 -3
- package/dist/domain/markdown.js +29 -9
- package/dist/domain/middle-out.js +18 -0
- package/dist/infrastructure/config.js +38 -0
- package/dist/infrastructure/file-index.js +358 -0
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/index-state.js +58 -0
- package/dist/infrastructure/private-pack-codec.js +71 -10
- package/dist/infrastructure/search-packs.js +313 -17
- package/dist/infrastructure/volatile-memory.js +100 -0
- package/dist/mcp/server.js +21 -1
- package/dist/mcp/tools.js +96 -0
- package/docs/AGENT_USAGE.md +101 -18
- package/docs/ARCHITECTURE.md +22 -27
- package/docs/QUICKSTART.md +7 -0
- package/package.json +6 -4
- package/dist/infrastructure/sqlite/document-writer.js +0 -51
- package/dist/infrastructure/sqlite/graph-reader.js +0 -267
- package/dist/infrastructure/sqlite/recovery.js +0 -163
- package/dist/infrastructure/sqlite/schema.js +0 -114
- package/dist/infrastructure/sqlite/search-reader.js +0 -188
- package/dist/infrastructure/sqlite/types.js +0 -1
- package/dist/infrastructure/sqlite-index.js +0 -38
package/dist/domain/context.js
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
|
+
import { middleOutIndices } from './middle-out.js';
|
|
2
|
+
const maxSectionsPerDocument = 3;
|
|
3
|
+
const byScore = (left, right) => right.score - left.score || left.title.localeCompare(right.title);
|
|
4
|
+
const byOrdinal = (left, right) => (left.chunkOrdinal ?? Number.MAX_SAFE_INTEGER) - (right.chunkOrdinal ?? Number.MAX_SAFE_INTEGER);
|
|
5
|
+
const middleOutDocumentResults = (results) => {
|
|
6
|
+
if (results.length <= 1) {
|
|
7
|
+
return results;
|
|
8
|
+
}
|
|
9
|
+
const sortedByOrdinal = [...results].sort(byOrdinal);
|
|
10
|
+
const pivotChunkId = [...results].sort(byScore)[0]?.chunkId;
|
|
11
|
+
const pivotIndex = sortedByOrdinal.findIndex((result) => result.chunkId === pivotChunkId);
|
|
12
|
+
if (pivotIndex < 0) {
|
|
13
|
+
return [...results].sort(byScore);
|
|
14
|
+
}
|
|
15
|
+
return middleOutIndices(sortedByOrdinal.length, pivotIndex).map((index) => sortedByOrdinal[index]);
|
|
16
|
+
};
|
|
1
17
|
export const selectContextSections = (results, maxTokens) => {
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
18
|
+
const grouped = results.reduce((state, result) => {
|
|
19
|
+
const current = state.get(result.documentId) ?? [];
|
|
20
|
+
state.set(result.documentId, [...current, result]);
|
|
21
|
+
return state;
|
|
22
|
+
}, new Map());
|
|
23
|
+
const documentOrder = Array.from(results.reduce((state, result) => {
|
|
24
|
+
if (!state.has(result.documentId)) {
|
|
25
|
+
state.set(result.documentId, result.score);
|
|
6
26
|
}
|
|
7
|
-
return
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
return state;
|
|
28
|
+
}, new Map()).entries())
|
|
29
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
30
|
+
.map(([documentId]) => documentId);
|
|
31
|
+
const selected = documentOrder.reduce((state, documentId) => {
|
|
32
|
+
const ordered = middleOutDocumentResults(grouped.get(documentId) ?? []);
|
|
33
|
+
let usedTokens = state.usedTokens;
|
|
34
|
+
let sections = state.sections;
|
|
35
|
+
let seenChunks = state.seenChunks;
|
|
36
|
+
for (let index = 0; index < ordered.length && index < maxSectionsPerDocument; index += 1) {
|
|
37
|
+
const result = ordered[index];
|
|
38
|
+
if (seenChunks.has(result.chunkId)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const tokenCost = Math.ceil(result.content.length / 4);
|
|
42
|
+
if (usedTokens + tokenCost > maxTokens) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
usedTokens += tokenCost;
|
|
46
|
+
sections = [
|
|
47
|
+
...sections,
|
|
11
48
|
{
|
|
12
49
|
title: result.title,
|
|
13
50
|
path: result.path,
|
|
@@ -16,13 +53,18 @@ export const selectContextSections = (results, maxTokens) => {
|
|
|
16
53
|
searchMode: result.searchMode,
|
|
17
54
|
tags: result.tags
|
|
18
55
|
}
|
|
19
|
-
]
|
|
20
|
-
|
|
56
|
+
];
|
|
57
|
+
seenChunks = new Set([...seenChunks, result.chunkId]);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
usedTokens,
|
|
61
|
+
sections,
|
|
62
|
+
seenChunks
|
|
21
63
|
};
|
|
22
64
|
}, {
|
|
23
65
|
usedTokens: 0,
|
|
24
66
|
sections: [],
|
|
25
|
-
|
|
67
|
+
seenChunks: new Set()
|
|
26
68
|
});
|
|
27
69
|
return selected.sections;
|
|
28
70
|
};
|
|
@@ -34,6 +76,7 @@ export const formatContextPackage = (query, sections) => {
|
|
|
34
76
|
section.tags.length > 0 ? `Tags: ${section.tags.map((tag) => `#${tag}`).join(' ')}` : null,
|
|
35
77
|
`Score: ${section.score.toFixed(3)}`,
|
|
36
78
|
`Mode: ${section.searchMode}`,
|
|
79
|
+
section.volatile ? `Volatile: true${section.expiresAt ? `, expires ${section.expiresAt}` : ''}` : null,
|
|
37
80
|
'',
|
|
38
81
|
section.content
|
|
39
82
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const hierarchyGroupNodeLimit = 1000;
|
|
1
2
|
const groupLabels = {
|
|
2
3
|
'00-maps': 'maps',
|
|
3
4
|
'10-agent-memory': 'agent-memory',
|
|
@@ -20,6 +21,7 @@ const segmentAngles = {
|
|
|
20
21
|
Evaluation: 2.08,
|
|
21
22
|
Security: 2.82
|
|
22
23
|
};
|
|
24
|
+
const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
23
25
|
const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
|
|
24
26
|
const jitter = (value, range) => {
|
|
25
27
|
const normalized = Math.abs(hashText(value) % 1000) / 1000;
|
|
@@ -62,6 +64,44 @@ const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
|
62
64
|
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
63
65
|
return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
|
|
64
66
|
};
|
|
67
|
+
const hubScore = (node) => {
|
|
68
|
+
const title = node.title.trim().toLowerCase();
|
|
69
|
+
if (title === 'memory hub')
|
|
70
|
+
return 5;
|
|
71
|
+
if (title === 'knowledge root')
|
|
72
|
+
return 4;
|
|
73
|
+
if (/\bmoc\b/i.test(node.title))
|
|
74
|
+
return 3;
|
|
75
|
+
return hubTitlePattern.test(node.title) ? 2 : 0;
|
|
76
|
+
};
|
|
77
|
+
const selectPrimaryHubId = (nodes, degrees) => {
|
|
78
|
+
const ranked = [...nodes]
|
|
79
|
+
.filter((node) => hubScore(node) > 0)
|
|
80
|
+
.sort((left, right) => {
|
|
81
|
+
const scoreDelta = hubScore(right) - hubScore(left);
|
|
82
|
+
if (scoreDelta !== 0)
|
|
83
|
+
return scoreDelta;
|
|
84
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
85
|
+
if (degreeDelta !== 0)
|
|
86
|
+
return degreeDelta;
|
|
87
|
+
return left.title.localeCompare(right.title);
|
|
88
|
+
});
|
|
89
|
+
return ranked[0]?.id ?? null;
|
|
90
|
+
};
|
|
91
|
+
const centerLayoutByNode = (nodes, nodeId) => {
|
|
92
|
+
if (!nodeId) {
|
|
93
|
+
return nodes;
|
|
94
|
+
}
|
|
95
|
+
const anchor = nodes.find((node) => node.id === nodeId);
|
|
96
|
+
if (!anchor) {
|
|
97
|
+
return nodes;
|
|
98
|
+
}
|
|
99
|
+
return nodes.map((node) => ({
|
|
100
|
+
...node,
|
|
101
|
+
x: node.x - anchor.x,
|
|
102
|
+
y: node.y - anchor.y
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
65
105
|
const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
|
|
66
106
|
const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
|
|
67
107
|
const collectComponent = (adjacency, startId, visited) => {
|
|
@@ -128,13 +168,17 @@ const groupNodesBySegment = (nodes, segments) => {
|
|
|
128
168
|
return new Map(groups);
|
|
129
169
|
};
|
|
130
170
|
const segmentAngle = (segment, index, count) => segmentAngles[segment] ?? (Math.PI * 2 * index) / Math.max(count, 1) - Math.PI / 2;
|
|
171
|
+
const petalSpreadForSegmentSize = (size) => {
|
|
172
|
+
const safeSize = Math.max(size, 1);
|
|
173
|
+
return 180 + Math.log2(safeSize + 1) * 6;
|
|
174
|
+
};
|
|
131
175
|
const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes], segmentIndex) => {
|
|
132
176
|
const sortedNodes = [...nodes].sort(byDegreeThenTitle(degrees));
|
|
133
177
|
const angle = segmentAngle(segment, segmentIndex, segmentCount);
|
|
134
178
|
const baseRadius = segmentCount === 1 ? 0 : 340 + Math.min(sortedNodes.length, 22) * 10;
|
|
135
179
|
const centerX = Math.cos(angle) * baseRadius;
|
|
136
180
|
const centerY = Math.sin(angle) * (baseRadius * 0.78);
|
|
137
|
-
const petalSpread =
|
|
181
|
+
const petalSpread = petalSpreadForSegmentSize(sortedNodes.length);
|
|
138
182
|
return sortedNodes.map((node, index) => {
|
|
139
183
|
const localAngle = index * 2.399963 + jitter(node.title, 0.42);
|
|
140
184
|
const localRadius = Math.sqrt(index + 1) * petalSpread;
|
|
@@ -149,6 +193,136 @@ const createSegmentNodes = (segments, degrees, segmentCount) => ([segment, nodes
|
|
|
149
193
|
});
|
|
150
194
|
};
|
|
151
195
|
const distanceBetween = (left, right) => Math.hypot(right.x - left.x, right.y - left.y);
|
|
196
|
+
const layoutBounds = (nodes) => {
|
|
197
|
+
if (nodes.length === 0) {
|
|
198
|
+
return { x: 0, y: 0, radius: 1 };
|
|
199
|
+
}
|
|
200
|
+
const bounds = nodes.reduce((current, node) => ({
|
|
201
|
+
minX: Math.min(current.minX, node.x),
|
|
202
|
+
maxX: Math.max(current.maxX, node.x),
|
|
203
|
+
minY: Math.min(current.minY, node.y),
|
|
204
|
+
maxY: Math.max(current.maxY, node.y)
|
|
205
|
+
}), {
|
|
206
|
+
minX: Number.POSITIVE_INFINITY,
|
|
207
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
208
|
+
minY: Number.POSITIVE_INFINITY,
|
|
209
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
210
|
+
});
|
|
211
|
+
const x = (bounds.minX + bounds.maxX) / 2;
|
|
212
|
+
const y = (bounds.minY + bounds.maxY) / 2;
|
|
213
|
+
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y)), 1);
|
|
214
|
+
return { x, y, radius: Math.max(radius + 72, 120) };
|
|
215
|
+
};
|
|
216
|
+
const edgeTouchesGroup = (edge, nodeIds) => nodeIds.has(edge.source) || Boolean(edge.target && nodeIds.has(edge.target));
|
|
217
|
+
const edgeInsideGroup = (edge, nodeIds) => nodeIds.has(edge.source) && Boolean(edge.target && nodeIds.has(edge.target));
|
|
218
|
+
const groupTitle = (segment, level, index, nodes) => nodes.length === 1
|
|
219
|
+
? nodes[0]?.title ?? segment
|
|
220
|
+
: `${segment} ${level + 1}.${index + 1}`;
|
|
221
|
+
const chunkNodes = (nodes, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
222
|
+
const sortedNodes = [...nodes].sort((left, right) => {
|
|
223
|
+
const segmentDelta = left.segment.localeCompare(right.segment);
|
|
224
|
+
if (segmentDelta !== 0)
|
|
225
|
+
return segmentDelta;
|
|
226
|
+
const groupDelta = left.group.localeCompare(right.group);
|
|
227
|
+
if (groupDelta !== 0)
|
|
228
|
+
return groupDelta;
|
|
229
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
230
|
+
if (degreeDelta !== 0)
|
|
231
|
+
return degreeDelta;
|
|
232
|
+
return left.title.localeCompare(right.title);
|
|
233
|
+
});
|
|
234
|
+
const groupCountTarget = Math.min(groupNodeLimit, sortedNodes.length);
|
|
235
|
+
const chunkSize = sortedNodes.length <= groupNodeLimit * groupNodeLimit
|
|
236
|
+
? Math.max(1, Math.ceil(sortedNodes.length / groupCountTarget))
|
|
237
|
+
: groupNodeLimit;
|
|
238
|
+
const chunks = [];
|
|
239
|
+
for (let index = 0; index < sortedNodes.length; index += chunkSize) {
|
|
240
|
+
chunks.push(sortedNodes.slice(index, index + chunkSize));
|
|
241
|
+
}
|
|
242
|
+
return chunks;
|
|
243
|
+
};
|
|
244
|
+
const groupEdges = (edges, nodeIds) => ({
|
|
245
|
+
internalEdges: edges.filter((edge) => edgeInsideGroup(edge, nodeIds)),
|
|
246
|
+
externalEdges: edges.filter((edge) => edgeTouchesGroup(edge, nodeIds) && !edgeInsideGroup(edge, nodeIds))
|
|
247
|
+
});
|
|
248
|
+
const groupBounds = (groups) => {
|
|
249
|
+
if (groups.length === 0) {
|
|
250
|
+
return { x: 0, y: 0, radius: 1 };
|
|
251
|
+
}
|
|
252
|
+
const nodes = groups.map((group) => ({
|
|
253
|
+
x: group.x,
|
|
254
|
+
y: group.y,
|
|
255
|
+
radius: group.radius
|
|
256
|
+
}));
|
|
257
|
+
const x = nodes.reduce((sum, node) => sum + node.x, 0) / nodes.length;
|
|
258
|
+
const y = nodes.reduce((sum, node) => sum + node.y, 0) / nodes.length;
|
|
259
|
+
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - x, node.y - y) + node.radius), 1);
|
|
260
|
+
return { x, y, radius: Math.max(radius + 120, 180) };
|
|
261
|
+
};
|
|
262
|
+
const descendantNodeIds = (groups) => groups.flatMap((group) => group.nodeIds);
|
|
263
|
+
const createParentGroups = (groups, edges, level, groupNodeLimit) => {
|
|
264
|
+
if (groups.length <= groupNodeLimit) {
|
|
265
|
+
return groups;
|
|
266
|
+
}
|
|
267
|
+
const parentGroups = [];
|
|
268
|
+
for (let index = 0; index < groups.length; index += groupNodeLimit) {
|
|
269
|
+
const chunk = groups.slice(index, index + groupNodeLimit);
|
|
270
|
+
const nodeIds = new Set(descendantNodeIds(chunk));
|
|
271
|
+
const bounds = groupBounds(chunk);
|
|
272
|
+
const segment = chunk[0]?.segment ?? 'root';
|
|
273
|
+
const group = chunk[0]?.group ?? 'root';
|
|
274
|
+
const groupIndex = index / groupNodeLimit;
|
|
275
|
+
const id = ['root', level, groupIndex, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
276
|
+
const edgeGroups = groupEdges(edges, nodeIds);
|
|
277
|
+
parentGroups.push({
|
|
278
|
+
id,
|
|
279
|
+
level,
|
|
280
|
+
parentId: null,
|
|
281
|
+
title: `${segment} ${level + 1}.${Math.floor(groupIndex) + 1}`,
|
|
282
|
+
segment,
|
|
283
|
+
group,
|
|
284
|
+
x: bounds.x,
|
|
285
|
+
y: bounds.y,
|
|
286
|
+
radius: bounds.radius,
|
|
287
|
+
nodeIds: [],
|
|
288
|
+
childGroupIds: chunk.map((child) => child.id),
|
|
289
|
+
internalEdges: edgeGroups.internalEdges,
|
|
290
|
+
externalEdges: edgeGroups.externalEdges
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const relinkedChildren = groups.map((group) => {
|
|
294
|
+
const parent = parentGroups.find((candidate) => candidate.childGroupIds.includes(group.id));
|
|
295
|
+
return parent ? { ...group, parentId: parent.id } : group;
|
|
296
|
+
});
|
|
297
|
+
return [...createParentGroups(parentGroups, edges, level + 1, groupNodeLimit), ...relinkedChildren];
|
|
298
|
+
};
|
|
299
|
+
export const createGraphLayoutHierarchy = (nodes, edges, degrees, groupNodeLimit = hierarchyGroupNodeLimit) => {
|
|
300
|
+
if (nodes.length <= groupNodeLimit) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
const leafGroups = chunkNodes(nodes, degrees, groupNodeLimit).map((chunk, index) => {
|
|
304
|
+
const nodeIds = new Set(chunk.map((node) => node.id));
|
|
305
|
+
const bounds = layoutBounds(chunk);
|
|
306
|
+
const segment = chunk[0]?.segment ?? 'root';
|
|
307
|
+
const group = chunk[0]?.group ?? 'root';
|
|
308
|
+
const id = ['leaf', 0, index, chunk[0]?.id ?? 'empty', chunk.length].join(':');
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
level: 0,
|
|
312
|
+
parentId: null,
|
|
313
|
+
title: groupTitle(segment, 0, index, chunk),
|
|
314
|
+
segment,
|
|
315
|
+
group,
|
|
316
|
+
x: bounds.x,
|
|
317
|
+
y: bounds.y,
|
|
318
|
+
radius: bounds.radius,
|
|
319
|
+
nodeIds: chunk.map((node) => node.id),
|
|
320
|
+
childGroupIds: [],
|
|
321
|
+
...groupEdges(edges, nodeIds)
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
return createParentGroups(leafGroups, edges, 1, groupNodeLimit);
|
|
325
|
+
};
|
|
152
326
|
const resolveCollisionPair = (left, right, minDistance) => {
|
|
153
327
|
const dx = right.x - left.x;
|
|
154
328
|
const dy = right.y - left.y;
|
|
@@ -246,9 +420,13 @@ export const createCauliflowerGraphLayout = (graph) => {
|
|
|
246
420
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
247
421
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
248
422
|
const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
|
|
423
|
+
const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
|
|
424
|
+
const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
|
|
425
|
+
const groups = createGraphLayoutHierarchy(centeredNodes, graph.edges, degrees);
|
|
249
426
|
return {
|
|
250
|
-
nodes,
|
|
251
|
-
edges: graph.edges
|
|
427
|
+
nodes: centeredNodes,
|
|
428
|
+
edges: graph.edges,
|
|
429
|
+
...(groups.length > 0 ? { groups } : {})
|
|
252
430
|
};
|
|
253
431
|
};
|
|
254
432
|
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);
|
package/dist/domain/markdown.js
CHANGED
|
@@ -18,6 +18,9 @@ const priorityBoosts = {
|
|
|
18
18
|
high: 3,
|
|
19
19
|
critical: 6
|
|
20
20
|
};
|
|
21
|
+
const graphLinkLimit = 4;
|
|
22
|
+
export const graphLinkModelVersion = 2;
|
|
23
|
+
const hubLinkTitlePattern = /\b(?:memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
21
24
|
const priorityPatterns = [
|
|
22
25
|
['critical', /\b(?:priority|prioridade|importance|importancia|importância)\s*[:=]\s*(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
|
|
23
26
|
['critical', /#(?:critical|critica|crítica|urgent|urgente|p0)\b/i],
|
|
@@ -31,6 +34,7 @@ const priorityPatterns = [
|
|
|
31
34
|
const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '');
|
|
32
35
|
const unique = (values) => Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
33
36
|
const maxPriority = (left, right) => priorityRanks[left] >= priorityRanks[right] ? left : right;
|
|
37
|
+
const isHubLinkTitle = (title) => hubLinkTitlePattern.test(title);
|
|
34
38
|
const parseFrontmatter = (content) => {
|
|
35
39
|
const match = content.match(frontmatterPattern);
|
|
36
40
|
if (!match) {
|
|
@@ -100,6 +104,22 @@ export const extractWikiLinkWeights = (content) => {
|
|
|
100
104
|
}, new Map());
|
|
101
105
|
return Array.from(weights.values());
|
|
102
106
|
};
|
|
107
|
+
const compareGraphLinks = (left, right) => {
|
|
108
|
+
const priorityDelta = priorityRanks[right.priority] - priorityRanks[left.priority];
|
|
109
|
+
if (priorityDelta !== 0)
|
|
110
|
+
return priorityDelta;
|
|
111
|
+
const weightDelta = right.weight - left.weight;
|
|
112
|
+
if (weightDelta !== 0)
|
|
113
|
+
return weightDelta;
|
|
114
|
+
return left.title.localeCompare(right.title);
|
|
115
|
+
};
|
|
116
|
+
export const selectGraphWikiLinkWeights = (links) => {
|
|
117
|
+
const sorted = [...links].sort(compareGraphLinks);
|
|
118
|
+
const structuralLinks = sorted.filter((link) => isHubLinkTitle(link.title) && !['high', 'critical'].includes(link.priority));
|
|
119
|
+
const directLinks = sorted.filter((link) => !structuralLinks.includes(link));
|
|
120
|
+
const selected = (directLinks.length > 0 ? directLinks : sorted).slice(0, graphLinkLimit);
|
|
121
|
+
return selected.length > 0 ? selected : structuralLinks.slice(0, graphLinkLimit);
|
|
122
|
+
};
|
|
103
123
|
const extractTitle = (filePath, content, frontmatter) => {
|
|
104
124
|
if (frontmatter.title) {
|
|
105
125
|
return normalizeTitle(frontmatter.title);
|
|
@@ -191,16 +211,16 @@ export const parseMarkdownDocument = (input) => {
|
|
|
191
211
|
};
|
|
192
212
|
export const createIndexedDocument = (document, titleToDocumentId, maxChunkCharacters = 1200) => {
|
|
193
213
|
const chunks = splitIntoChunks(document.id, document.content, maxChunkCharacters);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
214
|
+
const graphLinkWeights = selectGraphWikiLinkWeights(extractWikiLinkWeights(document.content).filter((link) => link.title.toLowerCase() !== document.title.toLowerCase()));
|
|
215
|
+
const linkWeights = new Map(graphLinkWeights.map((link) => [link.title.toLowerCase(), link]));
|
|
216
|
+
const links = graphLinkWeights
|
|
217
|
+
.map((link) => ({
|
|
197
218
|
fromDocumentId: document.id,
|
|
198
|
-
toTitle,
|
|
199
|
-
toDocumentId: titleToDocumentId.get(
|
|
200
|
-
weight: linkWeights.get(
|
|
201
|
-
priority: linkWeights.get(
|
|
202
|
-
}))
|
|
203
|
-
.filter((link) => link.toDocumentId !== document.id);
|
|
219
|
+
toTitle: link.title,
|
|
220
|
+
toDocumentId: titleToDocumentId.get(link.title.toLowerCase()) ?? null,
|
|
221
|
+
weight: linkWeights.get(link.title.toLowerCase())?.weight ?? 1,
|
|
222
|
+
priority: linkWeights.get(link.title.toLowerCase())?.priority ?? 'normal'
|
|
223
|
+
}));
|
|
204
224
|
return {
|
|
205
225
|
document,
|
|
206
226
|
chunks,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const middleOutIndices = (size, pivotIndex) => {
|
|
2
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const clampedPivot = Math.max(0, Math.min(Math.floor(pivotIndex), size - 1));
|
|
6
|
+
const indices = [clampedPivot];
|
|
7
|
+
for (let offset = 1; indices.length < size; offset += 1) {
|
|
8
|
+
const left = clampedPivot - offset;
|
|
9
|
+
const right = clampedPivot + offset;
|
|
10
|
+
if (left >= 0) {
|
|
11
|
+
indices.push(left);
|
|
12
|
+
}
|
|
13
|
+
if (right < size) {
|
|
14
|
+
indices.push(right);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return indices;
|
|
18
|
+
};
|
|
@@ -15,6 +15,13 @@ export const defaultBrainlinkConfig = {
|
|
|
15
15
|
embeddingProvider: 'local',
|
|
16
16
|
defaultSearchMode: 'hybrid',
|
|
17
17
|
chunkSize: 1200,
|
|
18
|
+
searchPack: {
|
|
19
|
+
rowChunkSize: 5_000,
|
|
20
|
+
compressionLevel: 5,
|
|
21
|
+
useDictionary: true,
|
|
22
|
+
guardrailMinSavingsPercent: 8,
|
|
23
|
+
guardrailMaxLatencyRegressionPercent: 5
|
|
24
|
+
},
|
|
18
25
|
agentProfiles: {}
|
|
19
26
|
};
|
|
20
27
|
const configFilenames = ['brainlink.config.json', '.brainlink.json'];
|
|
@@ -37,6 +44,36 @@ const sanitizeEmbeddingProvider = (value) => typeof value === 'string' && embedd
|
|
|
37
44
|
export const sanitizeSearchMode = (value, fallback = defaultBrainlinkConfig.defaultSearchMode) => typeof value === 'string' && searchModes.has(value) ? value : fallback;
|
|
38
45
|
const sanitizeAllowedVaults = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
|
|
39
46
|
const sanitizePositiveNumber = (value) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
47
|
+
const sanitizeIntegerInRange = (value, fallback, minimum, maximum) => {
|
|
48
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
const rounded = Math.round(value);
|
|
52
|
+
if (rounded < minimum) {
|
|
53
|
+
return minimum;
|
|
54
|
+
}
|
|
55
|
+
if (rounded > maximum) {
|
|
56
|
+
return maximum;
|
|
57
|
+
}
|
|
58
|
+
return rounded;
|
|
59
|
+
};
|
|
60
|
+
const sanitizeSearchPackConfig = (value) => {
|
|
61
|
+
const fallback = defaultBrainlinkConfig.searchPack;
|
|
62
|
+
if (!isRecord(value)) {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
rowChunkSize: sanitizeIntegerInRange(value.rowChunkSize, fallback.rowChunkSize, 100, 100_000),
|
|
67
|
+
compressionLevel: sanitizeIntegerInRange(value.compressionLevel, fallback.compressionLevel, 0, 11),
|
|
68
|
+
useDictionary: typeof value.useDictionary === 'boolean' ? value.useDictionary : fallback.useDictionary,
|
|
69
|
+
guardrailMinSavingsPercent: typeof value.guardrailMinSavingsPercent === 'number' && Number.isFinite(value.guardrailMinSavingsPercent)
|
|
70
|
+
? Math.max(0, Math.min(95, value.guardrailMinSavingsPercent))
|
|
71
|
+
: fallback.guardrailMinSavingsPercent,
|
|
72
|
+
guardrailMaxLatencyRegressionPercent: typeof value.guardrailMaxLatencyRegressionPercent === 'number' && Number.isFinite(value.guardrailMaxLatencyRegressionPercent)
|
|
73
|
+
? Math.max(0, Math.min(300, value.guardrailMaxLatencyRegressionPercent))
|
|
74
|
+
: fallback.guardrailMaxLatencyRegressionPercent
|
|
75
|
+
};
|
|
76
|
+
};
|
|
40
77
|
const sanitizeAgentProfile = (value) => {
|
|
41
78
|
if (!isRecord(value)) {
|
|
42
79
|
return null;
|
|
@@ -130,6 +167,7 @@ const sanitizeConfig = (value) => ({
|
|
|
130
167
|
: defaultBrainlinkConfig.defaultContextTokens,
|
|
131
168
|
allowedVaults: [...sanitizeAllowedVaults(value.allowedVaults), ...readAllowedVaultsFromEnv()],
|
|
132
169
|
chunkSize: typeof value.chunkSize === 'number' && value.chunkSize > 0 ? value.chunkSize : defaultBrainlinkConfig.chunkSize,
|
|
170
|
+
searchPack: sanitizeSearchPackConfig(value.searchPack),
|
|
133
171
|
embeddingProvider: sanitizeEmbeddingProvider(value.embeddingProvider),
|
|
134
172
|
defaultSearchMode: sanitizeSearchMode(value.defaultSearchMode),
|
|
135
173
|
agentProfiles: sanitizeAgentProfiles(value.agentProfiles)
|