@unson/brainbase-mcp 0.10.0 → 0.10.2

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.2",
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": "d8334b5e94cdc643f5de90ccfc883a5e67fdd9af"
340
340
  }
@@ -148,6 +148,19 @@ function failureText(error) {
148
148
  return textOrNull(error instanceof Error ? error.message : typeof error === 'string' ? error : null) ?? '理由不明';
149
149
  }
150
150
 
151
+ /**
152
+ * A host may know the selected project's identity before its Graph list is
153
+ * available. Keep this boundary deliberately small: the early shell only
154
+ * accepts an id and display name, and never treats a host summary as Graph
155
+ * data such as goal, status or participant counts.
156
+ */
157
+ function normalizeProjectSummary(value) {
158
+ if (!isRecord(value)) return null;
159
+ const id = textOrNull(value.id ?? value.code);
160
+ if (!id) return null;
161
+ return { id, name: textOrNull(value.name) ?? id };
162
+ }
163
+
151
164
  /**
152
165
  * Mounts 「プロジェクトと関係者」.
153
166
  *
@@ -201,6 +214,9 @@ function failureText(error) {
201
214
  * @param {(projectId: string) => Promise<object>} [options.loadProjectContext]
202
215
  * Optional read-only supplementary sections (tasks/knowledge). A failed
203
216
  * context read is kept separate from the Graph detail read.
217
+ * @param {Array<{ id: string, name?: string }> | (() => Array<{ id: string, name?: string }>)} [options.projectSummaries]
218
+ * Optional host-known project identities. These are used only to draw the
219
+ * selected project's early loading shell before the Graph list is read.
204
220
  * @param {(container: Element, options: object) => { destroy?: Function }} [options.mountProjectGraph]
205
221
  * Host-injected Sigma.js graph mount. When omitted, the shared browser
206
222
  * entry is used. The workspace keeps an accessible relation list when
@@ -232,6 +248,7 @@ export function createGraphProjectsView({
232
248
  onSelect,
233
249
  ownShare,
234
250
  loadProjectContext,
251
+ projectSummaries,
235
252
  mountProjectGraph,
236
253
  } = {}) {
237
254
  if (!root) throw new TypeError('root is required');
@@ -267,6 +284,7 @@ export function createGraphProjectsView({
267
284
  let knowledgeWorkspace = null;
268
285
  let contextRequest = 0;
269
286
  let detailRequest = 0;
287
+ let hostListRequest = 0;
270
288
  let viewDestroyed = false;
271
289
 
272
290
  const correction = createGraphCorrection({
@@ -289,6 +307,46 @@ export function createGraphProjectsView({
289
307
  return state.list.state === 'ok' ? state.list.payload.projects.find((project) => project.id === id) ?? null : null;
290
308
  }
291
309
 
310
+ function projectSummaryFor(id) {
311
+ const target = textOrNull(id);
312
+ if (!target) return null;
313
+ let summaries = projectSummaries;
314
+ try {
315
+ if (typeof summaries === 'function') summaries = summaries();
316
+ } catch {
317
+ summaries = null;
318
+ }
319
+ if (Array.isArray(summaries)) {
320
+ const summary = summaries.map(normalizeProjectSummary).find((item) => item?.id === target);
321
+ if (summary) return summary;
322
+ }
323
+ const listed = listItem(target);
324
+ return listed ? { id: listed.id, name: listed.name } : { id: target, name: target };
325
+ }
326
+
327
+ function renderProjectKnowledgeLoading(id, readState, { onRetry } = {}) {
328
+ const target = textOrNull(id);
329
+ if (!target) return null;
330
+ const project = projectSummaryFor(target);
331
+ const loading = makeElement(doc, 'section', {
332
+ className: 'bb-pkw bb-pkw-loading',
333
+ attrs: { 'aria-label': 'プロジェクトの知識', 'data-project-id': target },
334
+ });
335
+ loading.append(makeElement(doc, 'div', { className: 'bb-pkw-kicker', text: 'プロジェクトの記録' }));
336
+ const header = makeElement(doc, 'header', { className: 'bb-pkw-header' });
337
+ header.append(
338
+ makeElement(doc, 'h2', { text: project.name }),
339
+ makeElement(doc, 'p', { className: 'bb-pkw-lead', text: '目的・関係・判断の根拠を、実際に記録された情報からたどれます。' }),
340
+ );
341
+ loading.append(header);
342
+ const notice = graphStateNotice(doc, readState, {
343
+ onRetry,
344
+ loadingText: 'プロジェクトの記録を読み込んでいます。',
345
+ });
346
+ if (notice) loading.append(notice);
347
+ return loading;
348
+ }
349
+
292
350
  function openCorrection(at, open) {
293
351
  state.panelAt = at;
294
352
  open();
@@ -456,6 +514,9 @@ export function createGraphProjectsView({
456
514
  const stateNotice = graphStateNotice(doc, state.list, { onRetry: () => controller.load(), loadingText: 'プロジェクトを読み込んでいます。' });
457
515
  if (stateNotice) {
458
516
  children.push(stateNotice);
517
+ if (state.selectedId) {
518
+ children.push(renderProjectKnowledgeLoading(state.selectedId, state.list, { onRetry: () => controller.load() }));
519
+ }
459
520
  return children;
460
521
  }
461
522
  const { payload } = state.list;
@@ -486,6 +547,10 @@ export function createGraphProjectsView({
486
547
  onTabChange: setKnowledgeTabChrome,
487
548
  });
488
549
  children.push(knowledgeWorkspace.element);
550
+ } else if (state.selectedId) {
551
+ children.push(renderProjectKnowledgeLoading(state.selectedId, state.detail ?? { state: 'loading' }, {
552
+ onRetry: () => controller.loadDetail(state.selectedId),
553
+ }));
489
554
  }
490
555
 
491
556
  if (payload.projects.length === 0) {
@@ -623,7 +688,10 @@ export function createGraphProjectsView({
623
688
  const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
624
689
  const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
625
690
  if (!loaded) {
626
- children.push(graphStateNotice(doc, detail?.id === state.selectedId ? detail : { state: 'loading' }, {
691
+ const readState = detail?.id === state.selectedId
692
+ ? detail
693
+ : state.list.state === 'ok' ? { state: 'loading' } : state.list;
694
+ children.push(graphStateNotice(doc, readState, {
627
695
  onRetry: () => controller.loadDetail(state.selectedId),
628
696
  loadingText: 'このプロジェクトの関係者を読み込んでいます。',
629
697
  }));
@@ -692,13 +760,16 @@ export function createGraphProjectsView({
692
760
 
693
761
  /** Reads the host's project list (see `load`). */
694
762
  async function loadHostList({ keep = false } = {}) {
763
+ const request = ++hostListRequest;
764
+ if (viewDestroyed) return state.list;
695
765
  if (!(keep && state.list.state === 'ok')) {
696
766
  state.list = { state: 'loading' };
697
767
  controller.render();
698
768
  }
699
- state.list = normalizeProjectList(await client.read('/projects'));
769
+ const result = await client.read('/projects');
770
+ if (viewDestroyed || request !== hostListRequest) return state.list;
771
+ state.list = normalizeProjectList(result);
700
772
  if (state.list.state !== 'ok') {
701
- if (!keep) state.selectedId = null;
702
773
  controller.render();
703
774
  return state.list;
704
775
  }
@@ -711,16 +782,21 @@ export function createGraphProjectsView({
711
782
  contextRequest += 1;
712
783
  state.selectedId = projects[0]?.id ?? null;
713
784
  if (state.selectedId) {
785
+ if (viewDestroyed || request !== hostListRequest) return state.list;
714
786
  const reading = controller.loadDetail(state.selectedId);
715
787
  notifySelect(state.selectedId);
716
788
  await reading;
789
+ if (viewDestroyed || request !== hostListRequest) return state.list;
717
790
  return state.list;
718
791
  }
719
792
  } else if (state.detail?.id !== state.selectedId) {
720
793
  // The project the host asked for is listed; read it for the rail.
794
+ if (viewDestroyed || request !== hostListRequest) return state.list;
721
795
  await controller.loadDetail(state.selectedId);
796
+ if (viewDestroyed || request !== hostListRequest) return state.list;
722
797
  return state.list;
723
798
  }
799
+ if (viewDestroyed || request !== hostListRequest) return state.list;
724
800
  controller.render();
725
801
  return state.list;
726
802
  }
@@ -744,6 +820,7 @@ export function createGraphProjectsView({
744
820
  destroy() {
745
821
  if (viewDestroyed) return;
746
822
  viewDestroyed = true;
823
+ hostListRequest += 1;
747
824
  detailRequest += 1;
748
825
  contextRequest += 1;
749
826
  knowledgeWorkspace?.destroy();
@@ -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);