@unson/brainbase-mcp 0.41.13 → 0.43.0

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.
@@ -1148,12 +1148,45 @@ function appendJudgmentBasis(doc, references, labels) {
1148
1148
  return wrap;
1149
1149
  }
1150
1150
 
1151
- function appendOwnerJudgment(doc, references) {
1151
+ function personalMemoryExcerpt(value) {
1152
+ const body = String(value ?? '').replace(/\s+/gu, ' ').trim();
1153
+ return body.length > 60 ? `${body.slice(0, 60)}…` : body;
1154
+ }
1155
+
1156
+ /**
1157
+ * The used memories the host resolved for this viewer: an opening and a link
1158
+ * to あなたの記憶. Only the owner's host resolves them; without that, or when
1159
+ * the lookup failed, this stays the count only.
1160
+ */
1161
+ function ownerMemoryLinks(doc, references, memories) {
1162
+ if (!(memories instanceof Map)) return null;
1163
+ const found = [];
1164
+ for (const reference of Array.isArray(references) ? references : []) {
1165
+ if (judgmentReferenceKind(reference) !== PERSONAL_KNOWLEDGE_KIND) continue;
1166
+ const entry = memories.get(text(reference?.ref));
1167
+ if (entry?.status === 'found') found.push(entry);
1168
+ }
1169
+ if (found.length === 0) return null;
1170
+ const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-owner-memories', attrs: { 'aria-label': '使った記憶' } });
1171
+ for (const entry of found) {
1172
+ const item = makeWorkspaceElement(doc, 'li');
1173
+ item.append(makeWorkspaceElement(doc, 'a', { text: entry.excerpt, attrs: { href: entry.href } }));
1174
+ list.append(item);
1175
+ }
1176
+ return list;
1177
+ }
1178
+
1179
+ function appendOwnerJudgment(doc, references, memories = null) {
1152
1180
  const { owner } = describeJudgmentBasis(references);
1153
1181
  const wrap = makeWorkspaceElement(doc, 'div', { className: `bb-jh-owner is-${owner.state}` });
1154
1182
  if (owner.state === 'applied') {
1155
1183
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: `あなたの判断の材料(個人KG)を${owner.count}件使いました。` }));
1156
- wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。個人KGの中身はここには出しません。' }));
1184
+ const links = ownerMemoryLinks(doc, references, memories);
1185
+ if (links) {
1186
+ wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。使った記憶はあなたにだけ見えています。' }), links);
1187
+ } else {
1188
+ wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。個人KGの中身はここには出しません。' }));
1189
+ }
1157
1190
  } else if (owner.state === 'not_applied') {
1158
1191
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: 'あなたの判断の材料(個人KG)は使っていません。' }));
1159
1192
  wrap.append(makeWorkspaceElement(doc, 'p', { text: 'Graphの哲学・目的・世界モデルと、AIの判断で進めました。' }));
@@ -1467,7 +1500,7 @@ function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
1467
1500
  }));
1468
1501
  rail.append(workspaceRailBlock(doc, {
1469
1502
  title: '本人の考え',
1470
- content: appendOwnerJudgment(doc, record.selectedReferences),
1503
+ content: appendOwnerJudgment(doc, record.selectedReferences, options.personalMemories),
1471
1504
  }));
1472
1505
  rail.append(workspaceRailBlock(doc, {
1473
1506
  title: '判断の理由',
@@ -1907,6 +1940,10 @@ export function createJudgmentHistoryUI({
1907
1940
  // Optional host port: (references: {ref, kind}[]) => Promise<Map|object of ref -> name>.
1908
1941
  // The host resolves names under the viewer's current permission.
1909
1942
  resolveReferenceLabels = null,
1943
+ // Optional owner-only port: (refs: string[]) => Promise<Map|object of ref -> { excerpt, href }>.
1944
+ // A host passes it only to the owner; the 本人の考え block then shows the
1945
+ // opening of each used memory with a link to あなたの記憶.
1946
+ resolvePersonalMemory = null,
1910
1947
  } = {}) {
1911
1948
  if (!root) throw new TypeError('root is required');
1912
1949
  const doc = safeDocument(explicitDocument);
@@ -1946,6 +1983,7 @@ export function createJudgmentHistoryUI({
1946
1983
  skeletonTimer: null,
1947
1984
  slowTimer: null,
1948
1985
  labels: new Map(),
1986
+ personalMemories: new Map(),
1949
1987
  };
1950
1988
 
1951
1989
  let filterPopover = null;
@@ -2254,6 +2292,37 @@ export function createJudgmentHistoryUI({
2254
2292
  void load({ append: false });
2255
2293
  }
2256
2294
 
2295
+ function requestPersonalMemories(rows) {
2296
+ if (typeof resolvePersonalMemory !== 'function') return;
2297
+ const pending = [];
2298
+ for (const row of rows) {
2299
+ if (row.mode !== 'normal' || !Array.isArray(row.references)) continue;
2300
+ for (const reference of row.references) {
2301
+ const ref = text(reference?.ref);
2302
+ if (!ref || judgmentReferenceKind(reference) !== PERSONAL_KNOWLEDGE_KIND || state.personalMemories.has(ref)) continue;
2303
+ state.personalMemories.set(ref, { status: 'loading' });
2304
+ pending.push(ref);
2305
+ }
2306
+ }
2307
+ if (!pending.length) return;
2308
+ const settle = (resolved) => {
2309
+ if (state.disposed) return;
2310
+ const lookup = resolved instanceof Map
2311
+ ? (ref) => resolved.get(ref)
2312
+ : isRecord(resolved) ? (ref) => resolved[ref] : () => null;
2313
+ for (const ref of pending) {
2314
+ const entry = lookup(ref);
2315
+ const excerpt = isRecord(entry) ? personalMemoryExcerpt(entry.excerpt) : '';
2316
+ const href = isRecord(entry) ? text(entry.href) : '';
2317
+ state.personalMemories.set(ref, excerpt && href ? { status: 'found', excerpt, href } : { status: 'missing' });
2318
+ }
2319
+ render();
2320
+ };
2321
+ Promise.resolve()
2322
+ .then(() => resolvePersonalMemory([...pending]))
2323
+ .then(settle, () => settle(null));
2324
+ }
2325
+
2257
2326
  function requestReferenceLabels(rows) {
2258
2327
  const pending = [];
2259
2328
  for (const row of rows) {
@@ -2440,6 +2509,7 @@ export function createJudgmentHistoryUI({
2440
2509
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
2441
2510
  : 'この期間の判断履歴はありません。';
2442
2511
  requestReferenceLabels(aggregate.rows);
2512
+ requestPersonalMemories(aggregate.rows);
2443
2513
  const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
2444
2514
  state.lastFocusedKey = key;
2445
2515
  state.selectedKey = key;
@@ -2507,6 +2577,7 @@ export function createJudgmentHistoryUI({
2507
2577
  onSubmitFeedback: (controls) => void submitFeedback(selected, controls),
2508
2578
  onCopyReference: (payload) => void copyReference(payload),
2509
2579
  labels: state.labels,
2580
+ personalMemories: state.personalMemories,
2510
2581
  }
2511
2582
  : {};
2512
2583
  if (nativeDrawer) {
@@ -67,9 +67,11 @@ body {
67
67
  letter-spacing: -.035em;
68
68
  }
69
69
 
70
+ /* The menu fills the rail so that 設定 sits at its bottom (ledger D10). */
70
71
  .bb-shell-nav {
71
- display: grid;
72
- gap: 2px;
72
+ flex: 1 1 auto;
73
+ display: flex;
74
+ flex-direction: column;
73
75
  }
74
76
 
75
77
  .bb-shell-nav-link {
@@ -26,6 +26,8 @@ import { createMeetingMinutesUI } from './meeting-minutes.js';
26
26
  import { createMeetingMinutesStorageExtension } from './meeting-minutes-storage.js';
27
27
  import { createMeetingMinutesLineageExtension } from './meeting-minutes-lineage-http.js';
28
28
  import { createWorldModelView } from './world-model-view.js';
29
+ import { createPersonalMemoryUI, parsePersonalMemoryRoute, personalMemoryRouteTarget } from './personal-memory.js';
30
+ import { createPersonalMemoryHttpPort } from './personal-memory-http-port.js';
29
31
  import { createGraphClient } from './graph-view-shared.js';
30
32
  import { installNativeScrollbars } from './native-scrollbars.js';
31
33
  import { browserAreaStorage, createAreaMemory, createAreaNav } from './area-nav.js';
@@ -53,66 +55,69 @@ const NAV_ICON_PATHS = Object.freeze({
53
55
  minutes: ['M6 3.5h9l3 3V20.5H6z', 'M15 3.5v4h3', 'M9 12h6m-6 3h6'],
54
56
  // Two people (the organization edition's 人と会社 uses the same shape).
55
57
  people: ['M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M2.5 20a6.5 6.5 0 0 1 13 0', 'M16 4.5a3.5 3.5 0 0 1 0 6.5', 'M18 14a6.5 6.5 0 0 1 3.5 6'],
58
+ // あなたの記憶 (ledger D15): one person and a lock, seen only by the owner.
59
+ memory: ['M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z', 'M3.5 20a6.5 6.5 0 0 1 10-5.5', 'M15 15h6v5h-6z', 'M16.5 15v-1.5a1.5 1.5 0 0 1 3 0V15'],
56
60
  // Kinds of information and how they connect (settings, ledger D14): three linked boxes.
57
61
  ontology: ['M4 4h6v6H4z', 'M14 14h6v6h-6z', 'M14 4h6v6h-6z', 'M10 7h4', 'M17 10v4'],
58
62
  // World view (extension id `world`, ledger P17).
59
63
  world: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z', 'M3 12h18', 'M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3z'],
60
- // Areas whose screens have their own icons (ledger D10, stage 2): a briefcase, and
61
- // the organization edition's gear.
62
- 'area:work': ['M4 8h16v11H4z', 'M9 8V5h6v3', 'M4 13h16'],
64
+ // 設定 (ledger D10): the organization edition's gear.
63
65
  'area:settings': ['M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915', 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
64
66
  });
65
67
  // Screens without their own icon (for example a private product's extension).
66
68
  const DEFAULT_NAV_ICON_PATHS = Object.freeze(['M5 20V10m7 10V4m7 16v-7']);
67
69
  const SVG_NS = 'http://www.w3.org/2000/svg';
68
70
 
69
- // The menu's areas (brainbase-project ledger D10): the same order and names as
70
- // the organization edition. The left side lists only the areas; the screens of
71
- // the current area are tabs above the page (stage 2). 目的と現状 is the only
72
- // screen of its area and keeps its own name. Inside an area, `order` lists the
73
- // screens first in that order; `defaultScreen` opens when nothing is remembered.
74
- const NAV_AREAS = Object.freeze([
75
- Object.freeze({ id: 'purpose', label: null, order: ['objectives'], icon: 'objectives' }),
76
- Object.freeze({ id: 'work', label: '仕事', order: ['projects', 'minutes'], defaultScreen: 'projects', icon: 'area:work' }),
77
- Object.freeze({ id: 'outcomes', label: '成果', order: ['value-proofs'], icon: 'value-proofs' }),
78
- Object.freeze({ id: 'information', label: '情報と関係', order: ['graph', 'people'], defaultScreen: 'graph', icon: 'graph' }),
79
- Object.freeze({ id: 'settings', label: '設定', order: ['ontology'], defaultScreen: 'ontology', icon: 'area:settings' }),
71
+ // The menu's sections (brainbase-project ledger D10, 2026-10-17 revision): the
72
+ // destinations themselves under small headings, in the organization edition's
73
+ // order, with 設定 at the bottom. Screens OSS Personal does not have are not
74
+ // listed. `screenId` makes the heading open that screen (プロジェクトと関係者);
75
+ // `area` turns the section's screens into one line with tabs (設定). Inside a
76
+ // section, `order` lists the screens first in that order.
77
+ const NAV_SECTIONS = Object.freeze([
78
+ Object.freeze({ id: 'meetings', label: '会議', order: ['minutes'] }),
79
+ Object.freeze({ id: 'project', label: 'プロジェクト', screenId: 'projects', order: ['objectives'] }),
80
+ Object.freeze({ id: 'explore', label: '調べる', order: ['value-proofs', 'graph', 'people', 'memory'] }),
81
+ Object.freeze({ id: 'settings', label: null, placement: 'footer', order: ['ontology'], area: Object.freeze({ label: '設定', defaultScreen: 'ontology', icon: 'area:settings' }) }),
80
82
  ]);
81
83
 
84
+ const sectionOf = (screen) => NAV_SECTIONS.find((section) => section.id === screen.section) ?? null;
85
+
82
86
  // Menu order only: the screens' own order (and so the first screen) stays as given.
83
- // Screens without an area keep their place — those before the first area-bound
84
- // screen come first (a leading extension such as the world), the rest come last.
85
- function navLayout(screens) {
86
- const firstInArea = screens.findIndex((screen) => NAV_AREAS.some((area) => area.id === screen.area));
87
- if (firstInArea === -1) return { leading: [...screens], areas: [], trailing: [] };
88
- const leading = screens.slice(0, firstInArea);
89
- const trailing = screens.slice(firstInArea).filter((screen) => !NAV_AREAS.some((area) => area.id === screen.area));
90
- const areas = NAV_AREAS.map((area) => {
91
- const members = screens.filter((screen) => screen.area === area.id);
87
+ // Screens without a section keep their place — those before the first sectioned
88
+ // screen come first (a leading extension such as the world), the rest come after
89
+ // the sections and before 設定.
90
+ function navAreas(screens) {
91
+ const own = (screen, section = null) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id, ...(section ? { section } : {}) });
92
+ const firstSectioned = screens.findIndex((screen) => sectionOf(screen));
93
+ const leading = firstSectioned === -1 ? [...screens] : screens.slice(0, firstSectioned);
94
+ const trailing = firstSectioned === -1 ? [] : screens.slice(firstSectioned).filter((screen) => !sectionOf(screen));
95
+ const sectioned = NAV_SECTIONS.map((section) => {
96
+ const members = screens.filter((screen) => screen.section === section.id && screen.id !== section.screenId);
92
97
  const rank = (screen) => {
93
- const index = area.order.indexOf(screen.id);
94
- return index === -1 ? area.order.length + members.indexOf(screen) : index;
98
+ const index = section.order.indexOf(screen.id);
99
+ return index === -1 ? section.order.length + members.indexOf(screen) : index;
95
100
  };
96
- return { area, screens: [...members].sort((a, b) => rank(a) - rank(b)) };
97
- }).filter((entry) => entry.screens.length > 0);
98
- return { leading, areas, trailing };
101
+ const ordered = [...members].sort((a, b) => rank(a) - rank(b));
102
+ if (ordered.length === 0) return { section, areas: [] };
103
+ if (section.area) {
104
+ return { section, areas: [{ id: section.id, label: section.area.label, defaultScreen: section.area.defaultScreen, screenIds: ordered.map((screen) => screen.id), icon: section.area.icon, section: section.id }] };
105
+ }
106
+ return { section, areas: ordered.map((screen) => own(screen, section.id)) };
107
+ });
108
+ const main = sectioned.filter(({ section }) => section.placement !== 'footer').flatMap(({ areas }) => areas);
109
+ const footer = sectioned.filter(({ section }) => section.placement === 'footer').flatMap(({ areas }) => areas);
110
+ return [...leading.map((screen) => own(screen)), ...main, ...trailing.map((screen) => own(screen)), ...footer];
99
111
  }
100
112
 
101
- // Every menu line is an area: a screen without an area is an area of its own.
102
- function navAreas(screens) {
103
- const layout = navLayout(screens);
104
- const own = (screen) => ({ id: screen.id, label: null, screenIds: [screen.id], icon: screen.id });
105
- return [
106
- ...layout.leading.map(own),
107
- ...layout.areas.map(({ area, screens: members }) => ({
108
- id: area.id,
109
- label: area.label,
110
- defaultScreen: area.defaultScreen,
111
- screenIds: members.map((screen) => screen.id),
112
- icon: area.icon,
113
- })),
114
- ...layout.trailing.map(own),
115
- ];
113
+ // The sections handed to the menu part; a heading opens its screen only when that screen exists.
114
+ function navSections(screens) {
115
+ return NAV_SECTIONS.map((section) => ({
116
+ id: section.id,
117
+ label: section.label,
118
+ placement: section.placement ?? 'main',
119
+ ...(section.screenId && screens.some((screen) => screen.id === section.screenId) ? { screenId: section.screenId } : {}),
120
+ }));
116
121
  }
117
122
 
118
123
  function makeIcon(doc, id) {
@@ -163,11 +168,14 @@ export function parseLocalWebTarget(target) {
163
168
  // プロジェクトと関係者 one project (`project` = Graph project id); other screens ignore them.
164
169
  const decisionId = screenId === 'value-proofs' ? params.get('decision')?.trim() || null : null;
165
170
  const projectId = screenId === 'projects' ? params.get('project')?.trim() || null : null;
171
+ // あなたの記憶 keeps its filters and the opened memory (never the search words).
172
+ const memoryRoute = screenId === 'memory' ? parsePersonalMemoryRoute(params) : null;
166
173
  return {
167
174
  screenId,
168
175
  entityId: screenId === 'graph' ? params.get('entity_id')?.trim() || null : null,
169
176
  ...(decisionId ? { decisionId } : {}),
170
177
  ...(projectId ? { projectId } : {}),
178
+ ...(memoryRoute ? { memoryRoute } : {}),
171
179
  };
172
180
  }
173
181
 
@@ -228,6 +236,26 @@ function judgmentReferenceLabelReader(context) {
228
236
  };
229
237
  }
230
238
 
239
+ function personalMemoryPort(context) {
240
+ return createPersonalMemoryHttpPort({ fetcher: context.fetcher, token: context.token });
241
+ }
242
+
243
+ /** The opening of each used memory, for 判断の履歴's 本人の考え; the local host has one owner. */
244
+ function personalMemoryResolver(context) {
245
+ const port = personalMemoryPort(context);
246
+ return async (refs) => {
247
+ const list = await port.listMemories({});
248
+ if (list.status !== 'ok') throw new Error(list.reason);
249
+ const wanted = new Set(refs);
250
+ const found = new Map();
251
+ for (const item of list.items) {
252
+ if (!wanted.has(item.id)) continue;
253
+ found.set(item.id, { excerpt: item.text, href: `#${personalMemoryRouteTarget({ memoryId: item.id })}` });
254
+ }
255
+ return found;
256
+ };
257
+ }
258
+
231
259
  function mountToday(container, context) {
232
260
  return createJudgmentHistoryUI({
233
261
  root: makePage(container, context),
@@ -238,6 +266,23 @@ function mountToday(container, context) {
238
266
  token: context.token,
239
267
  basePath: '/api/judgment-history',
240
268
  resolveReferenceLabels: judgmentReferenceLabelReader(context),
269
+ resolvePersonalMemory: personalMemoryResolver(context),
270
+ });
271
+ }
272
+
273
+ function mountPersonalMemory(container, context) {
274
+ return createPersonalMemoryUI({
275
+ root: makePage(container, context),
276
+ rail: context.rail,
277
+ page: context.page,
278
+ document: context.document,
279
+ port: personalMemoryPort(context),
280
+ judgmentHref: (decisionId) => `#value-proofs?decision=${encodeURIComponent(decisionId)}`,
281
+ candidatesCommand: 'brainbase onboard:candidates',
282
+ route: context.initialEntityId ?? undefined,
283
+ onRouteChange: context.navigate
284
+ ? (route, options) => context.navigate(personalMemoryRouteTarget(route), options)
285
+ : undefined,
241
286
  });
242
287
  }
243
288
 
@@ -332,17 +377,19 @@ function mountGraphScreen(screenId, createView, extra = {}) {
332
377
  }
333
378
 
334
379
  export const LOCAL_WEB_SCREENS = Object.freeze([
335
- Object.freeze({ id: 'value-proofs', label: '判断の履歴', area: 'outcomes', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
336
- Object.freeze({ id: 'minutes', label: '議事録', area: 'work', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
337
- Object.freeze({ id: 'objectives', label: '目的と現状', area: 'purpose', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
338
- Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', area: 'work', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
380
+ Object.freeze({ id: 'value-proofs', label: '判断の履歴', section: 'explore', usesGraph: false, rail: true, source: '判断の記録', mount: mountToday }),
381
+ Object.freeze({ id: 'minutes', label: '議事録', section: 'meetings', usesGraph: false, rail: true, source: '正本の議事録', mount: mountMeetingMinutes }),
382
+ Object.freeze({ id: 'objectives', label: '目的と現状', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountObjectives }),
383
+ Object.freeze({ id: 'projects', label: 'プロジェクトと関係者', section: 'project', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('projects', createGraphProjectsView) }),
339
384
  // A record of 情報と関係 opens as its own screen at `#graph?entity_id=…`; the list is `#graph`.
340
- Object.freeze({ id: 'graph', label: '情報と関係', area: 'information', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
385
+ Object.freeze({ id: 'graph', label: '情報と関係', section: 'explore', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('graph', createGraphRegistryView, (context) => ({
341
386
  onRouteChange: (entityId, options) => context.navigate?.(entityId ? `graph?entity_id=${encodeURIComponent(entityId)}` : 'graph', options),
342
387
  })) }),
343
- Object.freeze({ id: 'people', label: '人と会社', area: 'information', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
388
+ Object.freeze({ id: 'people', label: '人と会社', section: 'explore', usesGraph: true, rail: true, source: '手元のGraph', mount: mountGraphScreen('people', createPeopleAndCompaniesView, { canCorrect: true }) }),
389
+ // Only the owner sees it (ledger D15); the local host has one owner, so it is always listed here.
390
+ Object.freeze({ id: 'memory', label: 'あなたの記憶', section: 'explore', usesGraph: false, rail: true, ownerOnly: true, source: '手元の個人KG', mount: mountPersonalMemory }),
344
391
  // The definitions behind 情報と関係: infrequent management work, so under settings (ledger D14).
345
- Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', area: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
392
+ Object.freeze({ id: 'ontology', label: '情報の種類とつながり方', section: 'settings', usesGraph: true, rail: false, source: '手元のGraph', mount: mountGraphScreen('ontology', createGraphOntologyView, { canCorrect: true }) }),
346
393
  ]);
347
394
 
348
395
  function renderGraphGate(doc, slot, status, onRecheck, label) {
@@ -428,10 +475,14 @@ export function createLocalWebShell({
428
475
  meetingMinutesLineage = null,
429
476
  areaMemory = createAreaMemory(browserAreaStorage()),
430
477
  onNavigate,
478
+ // A host that serves more than one person passes false for anyone but the owner:
479
+ // owner-only screens (あなたの記憶) are then neither listed nor mounted.
480
+ isOwner = true,
431
481
  } = {}) {
432
482
  if (!root) throw new TypeError('root is required');
433
483
  const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
434
484
  if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
485
+ screens = screens.filter((screen) => isOwner === true || screen.ownerOnly !== true);
435
486
  const request = typeof fetcher === 'function'
436
487
  ? fetcher
437
488
  : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
@@ -466,6 +517,7 @@ export function createLocalWebShell({
466
517
  const areaNav = createAreaNav({
467
518
  document: doc,
468
519
  areas: areaList,
520
+ sections: navSections(screens),
469
521
  screens: screens.map((screen) => ({ id: screen.id, label: screen.label })),
470
522
  memory: areaMemory,
471
523
  linkClassName: 'bb-shell-nav-link',
@@ -522,6 +574,7 @@ export function createLocalWebShell({
522
574
  if (!entityId && screen.id === 'graph' && view?.state?.detail && typeof view.closeEntity === 'function') view.closeEntity();
523
575
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
524
576
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
577
+ if (entityId && screen.id === 'memory' && typeof view?.applyRoute === 'function') view.applyRoute(entityId);
525
578
  return;
526
579
  }
527
580
  const slot = slots.get(screen.id);
@@ -535,6 +588,7 @@ export function createLocalWebShell({
535
588
  const page = Object.freeze({ crumbs: Object.freeze(['あなたのBrainbase', screen.label]), source: screen.source ?? null });
536
589
  const view = screen.mount(slot, Object.freeze({ ...context, rail, page, initialEntityId: entityId })) ?? true;
537
590
  mounted.set(screen.id, view);
591
+ if (screen.id === 'memory' && typeof view?.load === 'function') void view.load();
538
592
  if (entityId && screen.id === 'value-proofs' && typeof view?.openDecision === 'function') view.openDecision(entityId);
539
593
  if (entityId && screen.id === 'projects' && typeof view?.openProject === 'function') void view.openProject(entityId);
540
594
  updateRail();
@@ -550,7 +604,9 @@ export function createLocalWebShell({
550
604
  const screen = screens.find((candidate) => candidate.id === parsed.screenId) ?? screens[0];
551
605
  pendingEntityIds.set(screen.id, screen.id === 'graph' ? parsed.entityId
552
606
  : screen.id === 'value-proofs' ? parsed.decisionId ?? null
553
- : screen.id === 'projects' ? parsed.projectId ?? null : null);
607
+ : screen.id === 'projects' ? parsed.projectId ?? null
608
+ // あなたの記憶 receives its whole route (filters and the opened memory).
609
+ : screen.id === 'memory' ? parsed.memoryRoute ?? null : null);
554
610
  active = screen.id;
555
611
  for (const [screenId, slot] of slots) slot.hidden = screenId !== screen.id;
556
612
  areaNav.show(screen.id);
@@ -0,0 +1,72 @@
1
+ /*
2
+ * The local Web host's port for 「あなたの記憶」 (ui/personal-memory.js).
3
+ *
4
+ * Every read carries the launch token: the memory bodies are private, so the
5
+ * host guards these reads as it guards writes. The list is read whole and
6
+ * filtered here, so the words being searched for never go into a request URL.
7
+ * The local host has a single owner, so `isOwner` is always true.
8
+ */
9
+
10
+ import { filterPersonalMemories } from './personal-memory.js';
11
+
12
+ export function createPersonalMemoryHttpPort({
13
+ fetcher,
14
+ token,
15
+ basePath = '/api/personal-memory',
16
+ tokenHeader = 'x-brainbase-review-token',
17
+ } = {}) {
18
+ const request = typeof fetcher === 'function'
19
+ ? fetcher
20
+ : typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
21
+ const base = String(basePath).replace(/\/+$/u, '');
22
+
23
+ async function read(path) {
24
+ if (!request) return { status: 0, body: null, failure: 'fetch_unavailable' };
25
+ try {
26
+ const response = await request(path, { method: 'GET', headers: token ? { [tokenHeader]: token } : {} });
27
+ let body = null;
28
+ try {
29
+ body = await response.json();
30
+ } catch {
31
+ body = null;
32
+ }
33
+ return { status: response.status, ok: response.ok === true, body };
34
+ } catch (error) {
35
+ return { status: 0, body: null, failure: error instanceof Error ? error.message : 'request_failed' };
36
+ }
37
+ }
38
+
39
+ function unavailable(result) {
40
+ return {
41
+ status: 'unavailable',
42
+ reason: result.failure ?? result.body?.reason ?? result.body?.error?.code ?? `http_${result.status}`,
43
+ };
44
+ }
45
+
46
+ return {
47
+ isOwner: () => true,
48
+ async listMemories(filter = {}) {
49
+ const result = await read(base);
50
+ if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
51
+ return { status: 'ok', items: filterPersonalMemories(result.body.items, filter), nextCursor: null };
52
+ },
53
+ async readMemory(id) {
54
+ const result = await read(`${base}/${encodeURIComponent(id)}`);
55
+ if (result.status === 404 && result.body?.status === 'not_found') return { status: 'not_found' };
56
+ if (!result.ok || result.body?.status !== 'ok' || !result.body.item) return unavailable(result);
57
+ return { status: 'ok', item: result.body.item };
58
+ },
59
+ async listJudgmentsUsing(id, limit) {
60
+ const result = await read(`${base}/${encodeURIComponent(id)}/judgments?limit=${encodeURIComponent(String(limit))}`);
61
+ if (!result.ok || result.body?.status !== 'ok' || !Array.isArray(result.body.items)) return unavailable(result);
62
+ return { status: 'ok', items: result.body.items, total: result.body.total };
63
+ },
64
+ async countJudgmentsUsing() {
65
+ const result = await read(`${base}?view=judgment-counts`);
66
+ if (!result.ok || result.body?.status !== 'ok' || !result.body.counts) return unavailable(result);
67
+ return { status: 'ok', counts: result.body.counts };
68
+ },
69
+ };
70
+ }
71
+
72
+ export default createPersonalMemoryHttpPort;