@unson/brainbase-mcp 0.10.0 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -336,5 +336,5 @@
336
336
  "engines": {
337
337
  "node": ">=20"
338
338
  },
339
- "gitHead": "56a13a2c780fb0390c804bddfb55f00c2d5a06ed"
339
+ "gitHead": "3fab56acebc9266f2bd256cc1795a3d7f550c311"
340
340
  }
@@ -10,7 +10,9 @@ import { MultiDirectedGraph, Sigma } from './project-graph-vendor.js';
10
10
  import {
11
11
  PROJECT_GRAPH_TYPE_COLORS,
12
12
  createProjectGraphModel,
13
+ projectGraphEdgePresentation,
13
14
  projectGraphNeighborIds,
15
+ projectGraphNodePresentation,
14
16
  } from './project-graph.js';
15
17
 
16
18
  const DEFAULT_EDGE_COLOR = '#cbd5e1';
@@ -85,12 +87,6 @@ function makeGraph(model) {
85
87
  return graph;
86
88
  }
87
89
 
88
- function edgeTouches(edge, nodeId, neighborIds) {
89
- if (!nodeId) return false;
90
- return edge.source === nodeId || edge.target === nodeId ||
91
- (neighborIds.has(edge.source) && neighborIds.has(edge.target));
92
- }
93
-
94
90
  /**
95
91
  * Mounts one project graph and returns an idempotent resource handle.
96
92
  *
@@ -153,46 +149,53 @@ export function mountProjectGraph(container, options = {}) {
153
149
  const base = { ...data };
154
150
  const neighbors = getNeighbors();
155
151
  if (!selectedId) return base;
156
- if (nodeId === selectedId) {
157
- return {
158
- ...base,
159
- color: SELECTED_COLOR,
160
- size: Math.max(11, (base.size || 9) + 4),
161
- highlighted: true,
162
- forceLabel: true,
163
- };
164
- }
165
- if (neighbors.has(nodeId)) {
166
- return {
167
- ...base,
168
- color: base.baseColor || PROJECT_GRAPH_TYPE_COLORS.unknown,
169
- size: Math.max(9, (base.size || 9) + 1),
170
- highlighted: true,
171
- forceLabel: true,
172
- };
173
- }
152
+ const presentation = projectGraphNodePresentation(
153
+ { id: nodeId, size: base.size },
154
+ { selectedId, neighborIds: neighbors },
155
+ );
156
+ const { state, ...visuals } = presentation;
157
+ if (state === 'selected') return { ...base, ...visuals, color: SELECTED_COLOR };
158
+ if (state === 'neighbor') return {
159
+ ...base,
160
+ ...visuals,
161
+ color: base.baseColor || PROJECT_GRAPH_TYPE_COLORS.unknown,
162
+ };
174
163
  return {
175
164
  ...base,
165
+ ...visuals,
176
166
  color: UNRELATED_COLOR,
177
- size: Math.max(5, (base.size || 9) * 0.72),
178
- label: null,
179
- forceLabel: false,
180
- highlighted: false,
181
167
  };
182
168
  };
183
169
 
184
170
  const edgeReducer = (edgeId, data) => {
185
171
  const edge = edgeById.get(edgeId);
186
172
  const base = { ...data };
187
- if (!selectedId || !edge) return { ...base, label: null, color: base.baseColor || DEFAULT_EDGE_COLOR };
173
+ if (!selectedId || !edge) {
174
+ return {
175
+ ...base,
176
+ label: null,
177
+ forceLabel: false,
178
+ color: base.baseColor || DEFAULT_EDGE_COLOR,
179
+ };
180
+ }
188
181
  const neighbors = getNeighbors();
189
- if (!edgeTouches(edge, selectedId, neighbors)) {
190
- return { ...base, label: null, color: UNRELATED_EDGE_COLOR, size: 0.5 };
182
+ const presentation = projectGraphEdgePresentation(edge, {
183
+ selectedId,
184
+ neighborIds: neighbors,
185
+ });
186
+ if (!presentation.relevant) {
187
+ return {
188
+ ...base,
189
+ label: null,
190
+ forceLabel: false,
191
+ color: UNRELATED_EDGE_COLOR,
192
+ size: 0.5,
193
+ };
191
194
  }
192
195
  return {
193
196
  ...base,
194
- label: edge.label || null,
195
- forceLabel: Boolean(edge.label),
197
+ label: presentation.showLabel ? edge.label : null,
198
+ forceLabel: presentation.forceLabel,
196
199
  color: edge.color || DEFAULT_EDGE_COLOR,
197
200
  size: 1.5,
198
201
  };
@@ -24,6 +24,9 @@ export const PROJECT_GRAPH_TYPE_COLORS = Object.freeze({
24
24
  });
25
25
 
26
26
  const EMPTY_ARRAY = Object.freeze([]);
27
+ const MAX_SELECTED_NODE_SIZE = 18;
28
+ const MAX_NEIGHBOR_NODE_SIZE = 12;
29
+ const MAX_UNRELATED_NODE_SIZE = 10;
27
30
 
28
31
  function isRecord(value) {
29
32
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -168,6 +171,81 @@ export function projectGraphNeighborIds(nodes, edges, selectedId) {
168
171
  return neighbors;
169
172
  }
170
173
 
174
+ function toIdSet(value) {
175
+ return value instanceof Set
176
+ ? value
177
+ : new Set(Array.isArray(value) ? value.filter(Boolean) : EMPTY_ARRAY);
178
+ }
179
+
180
+ /**
181
+ * Returns the visual state for one node in a selected graph neighborhood.
182
+ *
183
+ * A selected node is the only node that bypasses Sigma's label collision
184
+ * avoidance. Neighbors keep their labels available to Sigma's normal label
185
+ * grid, while their size and hover state stay bounded in dense neighborhoods.
186
+ */
187
+ export function projectGraphNodePresentation(node, { selectedId = null, neighborIds = EMPTY_ARRAY } = {}) {
188
+ const id = stringValue(node?.id);
189
+ const neighbors = toIdSet(neighborIds);
190
+ const state = id && id === selectedId
191
+ ? 'selected'
192
+ : id && neighbors.has(id)
193
+ ? 'neighbor'
194
+ : 'unrelated';
195
+ const baseSize = Math.min(finitePositive(node?.size, 9), MAX_SELECTED_NODE_SIZE);
196
+
197
+ if (state === 'selected') {
198
+ return {
199
+ state,
200
+ size: Math.min(MAX_SELECTED_NODE_SIZE, Math.max(10, baseSize + 3)),
201
+ forceLabel: true,
202
+ highlighted: true,
203
+ };
204
+ }
205
+
206
+ if (state === 'neighbor') {
207
+ return {
208
+ state,
209
+ size: Math.min(MAX_NEIGHBOR_NODE_SIZE, Math.max(6, baseSize)),
210
+ forceLabel: false,
211
+ highlighted: false,
212
+ };
213
+ }
214
+
215
+ return {
216
+ state,
217
+ size: Math.min(MAX_UNRELATED_NODE_SIZE, Math.max(4, baseSize * 0.72)),
218
+ label: null,
219
+ forceLabel: false,
220
+ highlighted: false,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Returns the label policy for one edge in a selected graph neighborhood.
226
+ * Dense neighborhoods omit edge labels; relation details remain available
227
+ * in the selected record panel. Small neighborhoods show relevant labels.
228
+ */
229
+ export function projectGraphEdgePresentation(edge, { selectedId = null, neighborIds = EMPTY_ARRAY } = {}) {
230
+ const neighbors = toIdSet(neighborIds);
231
+ const relevant = Boolean(
232
+ selectedId && edge && (
233
+ edge.source === selectedId ||
234
+ edge.target === selectedId ||
235
+ (neighbors.has(edge.source) && neighbors.has(edge.target))
236
+ ),
237
+ );
238
+ // Sigma's edge-label renderer does not apply the same collision grid as
239
+ // node labels. Hide labels for a dense neighborhood and leave the relation
240
+ // available through selection/details instead of painting a radial stack.
241
+ const denseNeighborhood = neighbors.size > 12;
242
+ return {
243
+ relevant,
244
+ showLabel: relevant && !denseNeighborhood && Boolean(stringValue(edge?.label)),
245
+ forceLabel: false,
246
+ };
247
+ }
248
+
171
249
  /**
172
250
  * Computes a stable clustered layout. The selected node is the anchor when
173
251
  * it exists; its direct neighbours form the first ring and the remaining
@@ -188,11 +266,28 @@ export function computeProjectGraphLayout(nodes, edges = EMPTY_ARRAY, { selected
188
266
  const neighbors = list
189
267
  .filter((node) => neighborIds.has(node.id))
190
268
  .sort(compareNodes);
191
- const neighborRadius = Math.max(1.5, 0.5 + neighbors.length * 0.38);
192
- neighbors.forEach((node, index) => {
193
- const angle = -Math.PI / 2 + (2 * Math.PI * index) / Math.max(1, neighbors.length);
194
- positions[node.id] = { x: Math.cos(angle) * neighborRadius, y: Math.sin(angle) * neighborRadius };
195
- });
269
+ // Keep high-degree selections readable by spreading neighbors over stable
270
+ // concentric rings. A single ring turns a person with dozens of relations
271
+ // into a dense necklace even when Sigma's label grid is working correctly.
272
+ const neighborRingCount = neighbors.length ? Math.ceil(neighbors.length / 24) : 0;
273
+ const neighborRingRadius = 2.6;
274
+ const neighborRingGap = 1.8;
275
+ let neighborOffset = 0;
276
+ for (let ringIndex = 0; ringIndex < neighborRingCount; ringIndex += 1) {
277
+ const remaining = neighbors.length - neighborOffset;
278
+ const ringsLeft = neighborRingCount - ringIndex;
279
+ const ringSize = Math.ceil(remaining / ringsLeft);
280
+ const radius = neighborRingRadius + ringIndex * neighborRingGap;
281
+ for (let index = 0; index < ringSize; index += 1) {
282
+ const angle = -Math.PI / 2 + (2 * Math.PI * index) / Math.max(1, ringSize) + ringIndex * 0.19;
283
+ const node = neighbors[neighborOffset + index];
284
+ positions[node.id] = { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius };
285
+ }
286
+ neighborOffset += ringSize;
287
+ }
288
+ const neighborRadius = neighborRingCount
289
+ ? neighborRingRadius + (neighborRingCount - 1) * neighborRingGap
290
+ : 1.5;
196
291
 
197
292
  const remaining = list.filter((node) => !positions[node.id]);
198
293
  const groups = new Map();
@@ -445,8 +445,13 @@ export function createProjectKnowledgeWorkspace({
445
445
  const payload = projectDetailPayload(detail);
446
446
  const project = payload?.project;
447
447
  if (!project) throw new TypeError('project detail is required');
448
- const entities = graphEntityMap(payload);
449
- const edges = graphEvidenceEdges(payload);
448
+ // Keep the project-bound projection separate from the mutable exploration
449
+ // graph. Reading a related entity may reveal arbitrary neighboring records,
450
+ // but those records are not evidence that they belong to this project.
451
+ const overviewEntities = graphEntityMap(payload);
452
+ const overviewEdges = graphEvidenceEdges(payload);
453
+ const entities = new Map(overviewEntities);
454
+ const edges = [...overviewEdges];
450
455
  let destroyed = false;
451
456
  let selectedId = project.id;
452
457
  let activeTab = 'overview';
@@ -576,7 +581,7 @@ export function createProjectKnowledgeWorkspace({
576
581
  ['目的', textOrNull(selectedEntity.goal)],
577
582
  ['有効期間', validityText(selectedEntity.validFrom, selectedEntity.validTo)],
578
583
  ]));
579
- panel.append(makeElement(doc, 'h4', { text: 'このプロジェクトとの関係' }), relationEvidence(doc, edges, selectedId, entities, payload.asOf, selectAndExplore));
584
+ panel.append(makeElement(doc, 'h4', { text: 'このプロジェクトとの関係' }), relationEvidence(doc, overviewEdges, selectedId, overviewEntities, payload.asOf, selectAndExplore));
580
585
  if (selectedId !== project.id && !fetched) {
581
586
  const failed = read?.state === 'failed';
582
587
  panel.append(workspaceNotice(doc, {
@@ -699,7 +704,7 @@ export function createProjectKnowledgeWorkspace({
699
704
  teardownGraph();
700
705
  tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
701
706
  panels.replaceChildren();
702
- const overview = renderOverview(doc, payload, context, edges, entities, selectAndExplore);
707
+ const overview = renderOverview(doc, payload, context, overviewEdges, overviewEntities, selectAndExplore);
703
708
  overview.id = 'bb-pkw-panel-overview';
704
709
  overview.hidden = activeTab !== 'overview';
705
710
  panels.append(overview);