@andespindola/brainlink 0.1.0-beta.129 → 0.1.0-beta.130

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 CHANGED
@@ -562,7 +562,7 @@ Agents can raise the importance of a relationship by putting priority markers on
562
562
  Related: [[Incident Runbook]] #critical
563
563
  ```
564
564
 
565
- Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`.
565
+ Indexed edges expose `weight` and `priority` (`low`, `normal`, `high`, `critical`) through CLI JSON, HTTP graph APIs and `brainlink_graph`. Brainlink promotes only representative graph links per note: high-priority and high-weight links win, structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes are suppressed when stronger direct links exist, and old indexes are rebuilt automatically when their graph link model version is missing or stale.
566
566
 
567
567
  ## Graph UI
568
568
 
@@ -588,7 +588,7 @@ When native GUI is used, the GUI window automatically closes when the `blink ser
588
588
  The graph UI shows:
589
589
 
590
590
  - notes as nodes
591
- - `[[wiki links]]` as weighted edges
591
+ - representative `[[wiki links]]` as weighted edges
592
592
  - details opened on node click (tags, outgoing links, backlinks, full Markdown content)
593
593
  - neutral graph nodes with segment/group metadata
594
594
  - agent selector (id-only labels) for isolated views
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { createIndexedDocument, parseMarkdownDocument } from '../domain/markdown.js';
2
+ import { createIndexedDocument, graphLinkModelVersion, parseMarkdownDocument } from '../domain/markdown.js';
3
3
  import { sharedAgentId } from '../domain/agents.js';
4
4
  import { createEmbeddingProvider } from '../domain/embeddings.js';
5
5
  import { loadBrainlinkConfig } from '../infrastructure/config.js';
@@ -122,7 +122,8 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
122
122
  const previousSnapshotMap = createSnapshotMap(previousState?.files ?? []);
123
123
  const settingsChanged = previousState == null ||
124
124
  previousState.chunkSize !== config.chunkSize ||
125
- previousState.embeddingProvider !== config.embeddingProvider;
125
+ previousState.embeddingProvider !== config.embeddingProvider ||
126
+ previousState.graphLinkModelVersion !== graphLinkModelVersion;
126
127
  const packSettingsChanged = previousState == null ||
127
128
  previousState.searchPackRowChunkSize !== config.searchPack.rowChunkSize ||
128
129
  previousState.searchPackCompressionLevel !== config.searchPack.compressionLevel ||
@@ -271,6 +272,7 @@ export const indexVaultWithOptions = async (vaultPath, options) => {
271
272
  await writeIndexState(absoluteVaultPath, {
272
273
  chunkSize: config.chunkSize,
273
274
  embeddingProvider: config.embeddingProvider,
275
+ graphLinkModelVersion,
274
276
  searchPackRowChunkSize: config.searchPack.rowChunkSize,
275
277
  searchPackCompressionLevel: config.searchPack.compressionLevel,
276
278
  searchPackUseDictionary: config.searchPack.useDictionary,
@@ -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 linkWeights = new Map(extractWikiLinkWeights(document.content).map((link) => [link.title.toLowerCase(), link]));
195
- const links = document.links
196
- .map((toTitle) => ({
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(toTitle.toLowerCase()) ?? null,
200
- weight: linkWeights.get(toTitle.toLowerCase())?.weight ?? 1,
201
- priority: linkWeights.get(toTitle.toLowerCase())?.priority ?? 'normal'
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,
@@ -29,6 +29,7 @@ export const readIndexState = async (vaultPath) => {
29
29
  updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
30
30
  chunkSize: typeof parsed.chunkSize === 'number' ? parsed.chunkSize : 1200,
31
31
  embeddingProvider: typeof parsed.embeddingProvider === 'string' ? parsed.embeddingProvider : 'none',
32
+ graphLinkModelVersion: typeof parsed.graphLinkModelVersion === 'number' ? parsed.graphLinkModelVersion : 1,
32
33
  searchPackRowChunkSize: typeof parsed.searchPackRowChunkSize === 'number' ? parsed.searchPackRowChunkSize : 5_000,
33
34
  searchPackCompressionLevel: typeof parsed.searchPackCompressionLevel === 'number' ? parsed.searchPackCompressionLevel : 5,
34
35
  searchPackUseDictionary: typeof parsed.searchPackUseDictionary === 'boolean' ? parsed.searchPackUseDictionary : true,
@@ -46,6 +47,7 @@ export const writeIndexState = async (vaultPath, state) => {
46
47
  updatedAt: new Date().toISOString(),
47
48
  chunkSize: state.chunkSize,
48
49
  embeddingProvider: state.embeddingProvider,
50
+ graphLinkModelVersion: state.graphLinkModelVersion,
49
51
  searchPackRowChunkSize: state.searchPackRowChunkSize,
50
52
  searchPackCompressionLevel: state.searchPackCompressionLevel,
51
53
  searchPackUseDictionary: state.searchPackUseDictionary,
@@ -161,7 +161,7 @@ Brainlink only builds graph edges from Markdown `[[wiki links]]`.
161
161
 
162
162
  The `context` command is read-only. It retrieves indexed notes and returns a compact package for the model, but it does not write memory, create backlinks, infer relationships or modify the graph. If an agent reads context and then learns something durable, the agent must write a note with explicit links before that knowledge becomes connected memory.
163
163
 
164
- Graph edges are weighted during indexing. Repeated links increase weight. Links inside headings or task-list lines receive a small boost. Priority markers on the same line as a link raise its priority:
164
+ Graph edges are weighted during indexing. Repeated links increase weight. Links inside headings or task-list lines receive a small boost. Priority markers on the same line as a link raise its priority. The graph relationship model promotes only representative links per note: high-priority and high-weight links win, structural hub links such as `Memory Hub`, `Knowledge Root`, `MOC` and map notes are not promoted when stronger direct links exist, and each note emits a small bounded set of graph edges. Older indexes without the current graph link model version are automatically rebuilt on the next index run.
165
165
 
166
166
  ```md
167
167
  - [ ] Review [[Architecture]] priority: high
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.129",
3
+ "version": "0.1.0-beta.130",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",