@unson/brainbase-mcp 0.41.11 → 0.41.13

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
@@ -105,7 +105,7 @@ brainbase onboard:candidates
105
105
 
106
106
  ### Google Workspace local-first adopter
107
107
 
108
- 常時稼働のMac mini、Google Workspace、補助Gmail、許可したDrive/local folder、Calendar/notesに散在するタスクを対象にする例です。全Driveやhome directory全体は走査しません。
108
+ 常時稼働のローカルホスト(例: Mac mini)、Google Workspace、必要なら補助Gmail、許可したDrive/local folder、Calendar/notesに散在するタスクを想定した例です。使わないタスクツール(例: Notion)は`--inactive-task-tool`で非アクティブ扱いにできます。全Driveやhome directory全体は走査しません。
109
109
 
110
110
  ```bash
111
111
  brainbase onboard:plan --profile google-workspace-local --host mac-mini --email google-workspace --secondary-email gmail --calendar google-calendar --drive google-drive --drive-folder "<folder-id>" --local-folder "/Users/owner/Notes" --tasks scattered-calendar-notes --inactive-task-tool notion
@@ -25,8 +25,8 @@
25
25
  },
26
26
  {
27
27
  "path": "README.md",
28
- "sha256": "55fc2b9c7945b81cfe3b7971bc1d067f8cd45d057fbf23dcf3dfae175838d1d3"
28
+ "sha256": "26eacd2a057380b4fd386b2176bd6e790a41c0bb8d684857c53bb5cd947e2d28"
29
29
  }
30
30
  ],
31
- "digest": "89b20a5aa122e73e9d2f7c99e1b029cb12b101b95574ee4560ce023ec3197304"
31
+ "digest": "38bf5aff53993b1ee737071ca7952d44e36ac2e5ed3a7adcac65fdd86e8954af"
32
32
  }
@@ -52,7 +52,7 @@ export const LOCAL_WEB_JUDGMENT_HISTORY_PREFIX = JUDGMENT_HISTORY_HTTP_PREFIX;
52
52
  export const LOCAL_WEB_FOUNDATION_BODY_LIMIT_BYTES = 64 * 1024;
53
53
  export const LOCAL_WEB_PROVENANCE_SOURCE_ID = 'brainbase-local-web';
54
54
  const CANONICAL_FILES = ['graph.json', 'relationships.json', 'personal-kg.jsonl', 'decisions.jsonl'];
55
- const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css'];
55
+ const BASE_UI_FILES = ['brainbase-tokens.css', 'local-web-shell.js', 'local-web-shell.css', 'workspace-kit.js', 'workspace-kit.css', 'native-scrollbars.js', 'native-scrollbars.css', 'area-nav.js', 'area-nav.css'];
56
56
  const CONTENT_TYPES = Object.freeze({
57
57
  '.js': 'text/javascript; charset=utf-8',
58
58
  '.css': 'text/css; charset=utf-8'
@@ -883,6 +883,8 @@ export function createGraphWebModule(context) {
883
883
  'project-graph-vendor.js',
884
884
  'graph-registry-view.js',
885
885
  'graph-registry-view.css',
886
+ 'graph-ontology-view.js',
887
+ 'graph-ontology-view.css',
886
888
  'people-and-companies-view.js',
887
889
  'people-and-companies-view.css'
888
890
  ],
@@ -938,10 +940,25 @@ const meta = (name) => document.querySelector(\`meta[name="\${name}"]\`)?.getAtt
938
940
  // Keep the query portion intact; the shell parses the Graph entity query and
939
941
  // only treats the entity id as a target of the Graph screen.
940
942
  const screenFromHash = () => location.hash.replace(/^#/, '') || undefined;
941
- const shell = createLocalWebShell({
943
+ // A screen that opened its own address (a record of 情報と関係) goes back with the
944
+ // browser's history, so 「一覧に戻る」 and the back button land on the same list.
945
+ // Each entry remembers how many records deep it is, which stays right when the
946
+ // owner also uses the browser's back and forward.
947
+ const depth = () => (Number.isInteger(history.state?.bbDepth) ? history.state.bbDepth : 0);
948
+ let shell = null;
949
+ const navigate = (target, { back = false } = {}) => {
950
+ if (back && depth() > 0) {
951
+ history.go(-depth());
952
+ return;
953
+ }
954
+ history.pushState({ bbDepth: back ? 0 : depth() + 1 }, '', \`#\${target}\`);
955
+ shell?.show(target);
956
+ };
957
+ shell = createLocalWebShell({
942
958
  root: document.getElementById('brainbase-local-web'),
943
959
  token: meta('brainbase-web-token'),
944
960
  initialScreen: screenFromHash(),
961
+ onNavigate: navigate,
945
962
  meetingMinutesStorage: ${JSON.stringify(meetingMinutesStorage ?? null)},
946
963
  meetingMinutesLineage: ${JSON.stringify(meetingMinutesLineage ?? null)},
947
964
  screens: ${leading ? `[${leading}, ...LOCAL_WEB_SCREENS${trailing ? `, ${trailing}` : ''}]` : '[...LOCAL_WEB_SCREENS, ...extensionScreens]'}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.11",
3
+ "version": "0.41.13",
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",
@@ -328,6 +328,8 @@
328
328
  "./ui/world-model-view.css": "./ui/world-model-view.css",
329
329
  "./ui/people-and-companies-view": "./ui/people-and-companies-view.js",
330
330
  "./ui/people-and-companies-view.css": "./ui/people-and-companies-view.css",
331
+ "./ui/area-nav": "./ui/area-nav.js",
332
+ "./ui/area-nav.css": "./ui/area-nav.css",
331
333
  "./ui/local-web-shell": "./ui/local-web-shell.js",
332
334
  "./ui/local-web-shell.css": "./ui/local-web-shell.css",
333
335
  "./ui/workspace-kit": "./ui/workspace-kit.js",
@@ -342,6 +344,8 @@
342
344
  "./ui/graph-projects-view.css": "./ui/graph-projects-view.css",
343
345
  "./ui/graph-registry-view": "./ui/graph-registry-view.js",
344
346
  "./ui/graph-registry-view.css": "./ui/graph-registry-view.css",
347
+ "./ui/graph-ontology-view": "./ui/graph-ontology-view.js",
348
+ "./ui/graph-ontology-view.css": "./ui/graph-ontology-view.css",
345
349
  "./ui/meeting-minutes-storage": "./ui/meeting-minutes-storage.js",
346
350
  "./ui/meeting-minutes-storage.css": "./ui/meeting-minutes-storage.css",
347
351
  "./ui/brainbase-tokens.css": "./ui/brainbase-tokens.css",
@@ -483,5 +487,5 @@
483
487
  "engines": {
484
488
  "node": ">=20"
485
489
  },
486
- "gitHead": "3849d107c129672e91d6c28cfa3b8b5053d7f53a"
490
+ "gitHead": "fcfea3958913012579029adf0bf785c45643c77b"
487
491
  }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Area navigation (ledger D10, stage 2): the area links on the left and the
3
+ * tabs of the current area above the page. Uses only the shared tokens in
4
+ * brainbase-tokens.css; a host gives the area links its own look through
5
+ * `linkClassName`.
6
+ */
7
+ .bb-area-nav {
8
+ display: grid;
9
+ gap: 2px;
10
+ }
11
+
12
+ .bb-area-link[hidden],
13
+ .bb-area-tabs[hidden] {
14
+ display: none;
15
+ }
16
+
17
+ .bb-area-link-label {
18
+ min-width: 0;
19
+ flex: 1 1 auto;
20
+ }
21
+
22
+ .bb-area-count {
23
+ min-width: 20px;
24
+ flex: 0 0 auto;
25
+ border-radius: 999px;
26
+ padding: 0 6px;
27
+ background: var(--bb-color-accent-soft);
28
+ color: var(--bb-color-accent);
29
+ font-size: 11px;
30
+ font-weight: 700;
31
+ line-height: 18px;
32
+ text-align: center;
33
+ }
34
+
35
+ .bb-area-tabs {
36
+ display: flex;
37
+ gap: var(--bb-space-1);
38
+ overflow-x: auto;
39
+ border-bottom: 1px solid var(--bb-color-border);
40
+ padding: 0 var(--bb-space-5);
41
+ background: var(--bb-color-paper);
42
+ scrollbar-width: none;
43
+ }
44
+
45
+ .bb-area-tab {
46
+ min-height: 44px;
47
+ display: inline-flex;
48
+ flex: 0 0 auto;
49
+ align-items: center;
50
+ gap: var(--bb-space-2);
51
+ border-bottom: 2px solid transparent;
52
+ padding: 0 var(--bb-space-3);
53
+ color: var(--bb-color-muted);
54
+ font-size: 13px;
55
+ font-weight: 620;
56
+ text-decoration: none;
57
+ white-space: nowrap;
58
+ }
59
+
60
+ .bb-area-tab:hover {
61
+ color: var(--bb-color-ink);
62
+ }
63
+
64
+ .bb-area-tab[aria-current="page"] {
65
+ border-bottom-color: var(--bb-color-accent);
66
+ color: var(--bb-color-accent);
67
+ }
68
+
69
+ .bb-area-tab:focus-visible,
70
+ .bb-area-link:focus-visible {
71
+ outline: 2px solid var(--bb-color-accent);
72
+ outline-offset: -2px;
73
+ }
74
+
75
+ @media (max-width: 760px) {
76
+ .bb-area-tabs {
77
+ padding: 0 var(--bb-space-4);
78
+ }
79
+ }
package/ui/area-nav.js ADDED
@@ -0,0 +1,249 @@
1
+ /*
2
+ * Area navigation shared by OSS Personal and the organization edition
3
+ * (brainbase-project ledger D10, stage 2).
4
+ *
5
+ * The left side lists only the areas; the screens of the current area are tabs
6
+ * above the page. An area link opens the screen last used in that area, which
7
+ * is remembered in the viewer's browser only (a display preference, not a fact
8
+ * of the organization). Every area link and tab is a link to a screen, so
9
+ * screen addresses never change. The part draws links only: showing and
10
+ * loading screens stays with the caller (`show` in, `onSelect` out).
11
+ */
12
+
13
+ export const AREA_NAV_CONTRACT_VERSION = 'brainbase.area-nav.v1';
14
+ export const AREA_NAV_MEMORY_KEY = 'brainbase.area-nav.last-screen.v1';
15
+
16
+ function makeElement(doc, tag, { className, text, attrs = {} } = {}) {
17
+ const element = doc.createElement(tag);
18
+ if (className) element.className = className;
19
+ if (text !== undefined && text !== null) element.textContent = String(text);
20
+ for (const [name, value] of Object.entries(attrs)) {
21
+ if (value === undefined || value === null || value === false) continue;
22
+ element.setAttribute(name, value === true ? '' : String(value));
23
+ }
24
+ return element;
25
+ }
26
+
27
+ /**
28
+ * The last screen per area, kept in the given storage (for example the
29
+ * browser's localStorage). A storage that is missing, full or blocked behaves
30
+ * as if nothing was remembered.
31
+ */
32
+ export function createAreaMemory(storage, key = AREA_NAV_MEMORY_KEY) {
33
+ const read = () => {
34
+ try {
35
+ const raw = storage?.getItem?.(key);
36
+ const value = raw ? JSON.parse(raw) : null;
37
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
38
+ } catch {
39
+ return {};
40
+ }
41
+ };
42
+ return Object.freeze({
43
+ get(areaId) {
44
+ const value = read()[areaId];
45
+ return typeof value === 'string' ? value : null;
46
+ },
47
+ set(areaId, screenId) {
48
+ try {
49
+ storage?.setItem?.(key, JSON.stringify({ ...read(), [areaId]: screenId }));
50
+ } catch {
51
+ // Not remembered; the area opens its default screen next time.
52
+ }
53
+ },
54
+ });
55
+ }
56
+
57
+ /** The browser's localStorage when it can be reached, otherwise null. */
58
+ export function browserAreaStorage() {
59
+ try {
60
+ return globalThis.localStorage ?? null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The screen an area link opens: the remembered screen if it is still shown
68
+ * there, else the area's default, else its first shown screen. External links
69
+ * (opened in another tab) are never a target.
70
+ */
71
+ export function resolveAreaTarget(area, { remembered = null, isShown = () => true } = {}) {
72
+ const candidates = area.screens.filter((screen) => !screen.external && isShown(screen.id)).map((screen) => screen.id);
73
+ if (remembered && candidates.includes(remembered)) return remembered;
74
+ if (area.defaultScreen && candidates.includes(area.defaultScreen)) return area.defaultScreen;
75
+ return candidates[0] ?? null;
76
+ }
77
+
78
+ /**
79
+ * @param {object} options
80
+ * @param {Document} options.document
81
+ * @param {Array<{id: string, label: string|null, defaultScreen?: string, screenIds: string[]}>} options.areas
82
+ * In menu order. A null label takes the label of the area's only screen.
83
+ * @param {Array<{id: string, label: string, href?: string, external?: boolean, hidden?: boolean}>} options.screens
84
+ * @param {{get(areaId: string): string|null, set(areaId: string, screenId: string): void}} [options.memory]
85
+ * @param {(screenId: string) => string} [options.hrefFor] Address of a screen (default `#<id>`).
86
+ * @param {(screenId: string, event: Event) => void} [options.onSelect]
87
+ * @param {(area: object) => Node|null} [options.iconFor] Icon in front of an area link.
88
+ * @param {string} [options.linkClassName] Extra class for the area links (the host's look).
89
+ */
90
+ export function createAreaNav({
91
+ document: doc,
92
+ areas: areaInput,
93
+ screens: screenInput,
94
+ memory = createAreaMemory(null),
95
+ hrefFor = (screenId) => `#${screenId}`,
96
+ onSelect,
97
+ iconFor,
98
+ linkClassName = '',
99
+ label = '画面',
100
+ } = {}) {
101
+ if (!doc || typeof doc.createElement !== 'function') throw new Error('document_unavailable');
102
+ const screens = new Map((screenInput ?? []).map((screen) => [screen.id, {
103
+ id: screen.id,
104
+ label: screen.label,
105
+ href: screen.href ?? null,
106
+ external: screen.external === true,
107
+ hidden: screen.hidden === true,
108
+ count: null,
109
+ waiting: false,
110
+ }]));
111
+ const areas = (areaInput ?? []).map((area) => ({
112
+ id: area.id,
113
+ label: area.label ?? null,
114
+ defaultScreen: area.defaultScreen ?? null,
115
+ screens: area.screenIds.map((id) => screens.get(id)).filter(Boolean),
116
+ })).filter((area) => area.screens.length > 0);
117
+ const areaByScreen = new Map();
118
+ for (const area of areas) for (const screen of area.screens) areaByScreen.set(screen.id, area);
119
+
120
+ let activeScreenId = null;
121
+ const isShown = (screenId) => screens.get(screenId)?.hidden === false;
122
+ const shownScreens = (area) => area.screens.filter((screen) => isShown(screen.id));
123
+ const targetFor = (areaId) => {
124
+ const area = areas.find((candidate) => candidate.id === areaId);
125
+ return area ? resolveAreaTarget(area, { remembered: memory.get(area.id), isShown }) : null;
126
+ };
127
+ const screenHref = (screen) => screen.href ?? hrefFor(screen.id);
128
+ const areaLabel = (area) => area.label ?? area.screens[0].label;
129
+
130
+ const countBadge = (count, ariaLabel) => makeElement(doc, 'span', {
131
+ className: 'bb-area-count',
132
+ text: count,
133
+ attrs: { 'aria-label': ariaLabel },
134
+ });
135
+
136
+ const nav = makeElement(doc, 'nav', { className: 'bb-area-nav', attrs: { 'aria-label': label, 'data-contract-version': AREA_NAV_CONTRACT_VERSION } });
137
+ const tabs = makeElement(doc, 'nav', { className: 'bb-area-tabs' });
138
+ tabs.hidden = true;
139
+ const areaLinks = new Map();
140
+
141
+ for (const area of areas) {
142
+ const link = makeElement(doc, 'a', {
143
+ className: ['bb-area-link', linkClassName].filter(Boolean).join(' '),
144
+ attrs: { 'data-area': area.id },
145
+ });
146
+ link.addEventListener('click', (event) => {
147
+ const target = targetFor(area.id);
148
+ if (target) onSelect?.(target, event);
149
+ });
150
+ areaLinks.set(area.id, link);
151
+ nav.append(link);
152
+ }
153
+
154
+ function renderAreaLink(area) {
155
+ const link = areaLinks.get(area.id);
156
+ const shown = shownScreens(area);
157
+ link.hidden = !shown.some((screen) => !screen.external);
158
+ const target = targetFor(area.id);
159
+ if (target) link.setAttribute('href', screenHref(screens.get(target)));
160
+ else link.removeAttribute('href');
161
+ if (areaByScreen.get(activeScreenId) === area) link.setAttribute('aria-current', 'page');
162
+ else link.removeAttribute('aria-current');
163
+ const waiting = shown.reduce((sum, screen) => sum + (screen.waiting && Number.isInteger(screen.count) ? screen.count : 0), 0);
164
+ const icon = iconFor?.(area) ?? null;
165
+ link.replaceChildren(...[
166
+ icon,
167
+ makeElement(doc, 'span', { className: 'bb-area-link-label', text: areaLabel(area) }),
168
+ waiting > 0 ? countBadge(waiting, `待っているもの${waiting}件`) : null,
169
+ ].filter(Boolean));
170
+ }
171
+
172
+ function renderTabs() {
173
+ const area = areaByScreen.get(activeScreenId) ?? null;
174
+ const shown = area ? shownScreens(area) : [];
175
+ if (!area || shown.length < 2) {
176
+ tabs.hidden = true;
177
+ tabs.replaceChildren();
178
+ tabs.removeAttribute('aria-label');
179
+ return;
180
+ }
181
+ tabs.hidden = false;
182
+ tabs.setAttribute('aria-label', `${areaLabel(area)}の画面`);
183
+ tabs.replaceChildren(...shown.map((screen) => {
184
+ const tab = makeElement(doc, 'a', {
185
+ className: 'bb-area-tab',
186
+ attrs: {
187
+ href: screenHref(screen),
188
+ 'data-screen-tab': screen.id,
189
+ 'aria-current': screen.id === activeScreenId ? 'page' : null,
190
+ target: screen.external ? '_blank' : null,
191
+ rel: screen.external ? 'noopener' : null,
192
+ },
193
+ });
194
+ tab.append(makeElement(doc, 'span', { text: screen.label }));
195
+ if (Number.isInteger(screen.count)) {
196
+ tab.append(countBadge(screen.count, screen.waiting ? `待っているもの${screen.count}件` : `${screen.count}件`));
197
+ }
198
+ if (!screen.external) tab.addEventListener('click', (event) => onSelect?.(screen.id, event));
199
+ return tab;
200
+ }));
201
+ }
202
+
203
+ function render() {
204
+ for (const area of areas) renderAreaLink(area);
205
+ renderTabs();
206
+ }
207
+
208
+ const controller = {
209
+ nav,
210
+ tabs,
211
+ get state() {
212
+ const area = areaByScreen.get(activeScreenId) ?? null;
213
+ return { activeScreenId, activeAreaId: area?.id ?? null };
214
+ },
215
+ areaOf(screenId) {
216
+ return areaByScreen.get(screenId)?.id ?? null;
217
+ },
218
+ targetFor,
219
+ /** Marks the shown screen; a screen in an area becomes that area's last screen. */
220
+ show(screenId) {
221
+ activeScreenId = screenId;
222
+ const area = areaByScreen.get(screenId);
223
+ const screen = screens.get(screenId);
224
+ if (area && screen && !screen.external && isShown(screenId)) memory.set(area.id, screenId);
225
+ render();
226
+ return controller;
227
+ },
228
+ setHidden(screenId, hidden) {
229
+ const screen = screens.get(screenId);
230
+ if (!screen) return controller;
231
+ screen.hidden = hidden === true;
232
+ render();
233
+ return controller;
234
+ },
235
+ /** `count` null removes it. `waiting` counts are added up on the area link too. */
236
+ setCount(screenId, count, { waiting = false } = {}) {
237
+ const screen = screens.get(screenId);
238
+ if (!screen) return controller;
239
+ screen.count = Number.isInteger(count) && count >= 0 ? count : null;
240
+ screen.waiting = waiting === true;
241
+ render();
242
+ return controller;
243
+ },
244
+ };
245
+ render();
246
+ return controller;
247
+ }
248
+
249
+ export default createAreaNav;
@@ -248,3 +248,12 @@
248
248
  .bb-fov-philosophy-card .bb-fov-original { border-top: 0; padding-top: 0; }
249
249
  .bb-fov-model-card .bb-fov-original { margin-top: 12px; padding-top: 10px; overflow-wrap: anywhere; }
250
250
  .bb-fov-objective-ledger .bb-ws-ledger-row:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 2px; }
251
+ /* Whose what an objective waits for, and what can be done with it (story-objective-row-actions-v1). */
252
+ .bb-fov-waiting { display: grid; gap: 2px; min-width: 0; }
253
+ .bb-fov-waiting strong { font-weight: 600; }
254
+ .bb-fov-waiting small { color: var(--bb-color-muted); font-size: 12px; line-height: 1.4; }
255
+ .bb-fov-ledger-hint { margin: 8px 0 0; color: var(--bb-color-muted); font-size: 12px; }
256
+ .bb-fov-actions { display: grid; gap: 8px; justify-items: start; padding: 12px; border: 1px solid var(--bb-color-border); border-radius: 8px; background: var(--bb-color-surface-subtle, rgb(0 0 0 / 2%)); }
257
+ .bb-fov-actions h3 { margin: 0; }
258
+ .bb-fov-action-note { margin: 0; color: var(--bb-color-muted); font-size: 13px; }
259
+ .bb-fov-action-note.is-unavailable { color: var(--bb-color-ink); }
@@ -507,13 +507,79 @@ function objectiveCard(doc, record, variableMap) {
507
507
  return card;
508
508
  }
509
509
 
510
- function objectiveReading(doc, record, variableMap, recordContexts) {
510
+ /**
511
+ * Whose what an objective waits for (story-objective-row-actions-v1), from its own record: the items its
512
+ * current state says were not confirmed when it was saved, or else the fields it has not recorded, and
513
+ * the accountable person who would settle them. Nothing is waited for when the record names nothing.
514
+ */
515
+ export function objectiveWaiting(record) {
516
+ const definition = recordDefinition(record);
517
+ const current = currentStateFor(record);
518
+ const accountable = nonEmpty(definition.accountableId);
519
+ const items = current.missing.length ? current.missing : missingObjectiveFields(record);
520
+ if (items.length === 0) return null;
521
+ const what = `${current.missing.length ? '未確認' : '未記録'}:${items.join('・')}`;
522
+ const who = accountable ? '責任者の確認待ち' : '責任者が未記録のため、確かめる人が決まっていません';
523
+ return { what, who, items, kind: current.missing.length ? 'unconfirmed' : 'unrecorded', accountableRecorded: Boolean(accountable) };
524
+ }
525
+
526
+ function waitingElement(doc, waiting) {
527
+ const cell = makeElement(doc, 'span', { className: 'bb-fov-waiting' });
528
+ if (!waiting) {
529
+ cell.append(makeElement(doc, 'span', { className: 'bb-fov-missing', text: '記録上はありません' }));
530
+ return cell;
531
+ }
532
+ cell.append(makeElement(doc, 'strong', { className: 'bb-fov-missing', text: waiting.what }), makeElement(doc, 'small', { text: waiting.who }));
533
+ return cell;
534
+ }
535
+
536
+ /**
537
+ * What the reader can do with this objective, from the host (`callbacks.objectiveAction`): a button that
538
+ * goes where it is corrected, or the reason it cannot be from here. Without a host action the screen says
539
+ * it only reads; it never shows a control that does nothing.
540
+ */
541
+ function objectiveActionElement(doc, record, recordContexts, callbacks, dialog) {
542
+ const definition = recordDefinition(record), details = detailsFor(record);
543
+ const part = makeElement(doc, 'section', { className: 'bb-fov-actions' });
544
+ part.append(makeElement(doc, 'h3', { text: 'この目的でできること' }));
545
+ const context = recordContexts?.get(record);
546
+ const action = typeof callbacks?.objectiveAction === 'function'
547
+ ? callbacks.objectiveAction({
548
+ objectiveId: definition.id ?? null,
549
+ revision: definition.revision ?? null,
550
+ title: formatAny(details.title) ?? formatAny(definition.meaning) ?? null,
551
+ scopeIds: context?.scopeIds ?? [],
552
+ record,
553
+ })
554
+ : null;
555
+ if (action && typeof action.onSelect === 'function') {
556
+ part.append(workspaceButton(doc, {
557
+ text: `${action.label ?? '目的を直す・現状を記録する'} ›`,
558
+ variant: 'primary',
559
+ onClick: () => { dialog()?.close(); action.onSelect(); },
560
+ }));
561
+ part.append(makeElement(doc, 'p', { className: 'bb-fov-action-note', text: action.description ?? '目指す状態・評価基準・現状を直し、新しい版として保存します。保存すると、この一覧の「待っていること」が変わります。' }));
562
+ } else {
563
+ part.append(makeElement(doc, 'p', {
564
+ className: 'bb-fov-action-note is-unavailable',
565
+ text: nonEmpty(action?.unavailable) ?? 'この画面は読むだけです。ここからは記録を変えられません。',
566
+ }));
567
+ }
568
+ return part;
569
+ }
570
+
571
+ function objectiveReading(doc, record, variableMap, recordContexts, callbacks = {}, dialog = () => null) {
511
572
  const definition = recordDefinition(record), details = detailsFor(record);
512
573
  const body = makeElement(doc, 'div', { className: 'bb-fov-reading' });
513
574
  const section = (title, content) => {
514
575
  const part = makeElement(doc, 'section');
515
576
  part.append(makeElement(doc, 'h3', { text: title }), content); body.append(part);
516
577
  };
578
+ const waiting = objectiveWaiting(record);
579
+ section('待っていること', waiting
580
+ ? workspaceDefinition(doc, [['何を', waiting.what], ['誰が', waiting.who]])
581
+ : makeElement(doc, 'p', { text: '記録上、待っていることはありません。達成・採用・検証済みを意味しません。' }));
582
+ body.append(objectiveActionElement(doc, record, recordContexts, callbacks, dialog));
517
583
  section('達成条件', workspaceDefinition(doc, [
518
584
  ['目指す状態', formatAny(definition.desiredState)],
519
585
  ['評価基準', criteriaElement(doc, definition, details, variableMap)],
@@ -560,18 +626,21 @@ function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
560
626
  viewState.dialog?.close();
561
627
  viewState.dialog = workspaceDialog(doc, {
562
628
  title: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD, opener,
563
- content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts),
629
+ content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts, callbacks, () => viewState.dialog),
564
630
  });
565
631
  },
566
632
  cells: [
567
633
  { primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD },
568
634
  current.text ?? current.status ?? '未記録',
569
- { text: current.missing.length ? current.missing.join('・') : missingObjectiveFields(record).join('・') || '記録なし', className: 'bb-fov-missing' },
635
+ waitingElement(doc, objectiveWaiting(record)),
570
636
  formatDate(current.asOf),
571
637
  ],
572
638
  };
573
639
  });
574
- section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '確認待ち', '確認日'], rows }));
640
+ section.append(workspaceLedger(doc, { className: 'bb-fov-objective-ledger', ariaLabel: '登録された目的', columns: ['目的', '現状', '待っていること', '確認日'], rows }));
641
+ section.append(makeElement(doc, 'p', { className: 'bb-fov-ledger-hint', text: typeof callbacks?.objectiveAction === 'function'
642
+ ? '行を押すと、目的の詳細と、この目的でできること(直す・現状を記録する)が開きます。'
643
+ : '行を押すと、目的の詳細が開きます。この画面は読むだけです。' }));
575
644
  return section;
576
645
  }
577
646
 
@@ -912,13 +981,14 @@ export function renderFoundationCollectionOverview(root, sources, {
912
981
  if (existing) {
913
982
  const context = contexts.get(existing);
914
983
  if (!context.sources.includes(source.label)) context.sources.push(source.label);
984
+ if (!context.scopeIds.includes(source.scopeId)) context.scopeIds.push(source.scopeId);
915
985
  context.label = `${shared ? '組織共通 / ' : ''}${context.sources.join('・')}`;
916
986
  continue;
917
987
  }
918
988
  const record = { ...original };
919
989
  seen.set(key, record);
920
990
  catalog[kind].push(record);
921
- contexts.set(record, { key, variableMap, sources: [source.label], label: `${shared ? '組織共通 / ' : ''}${source.label}` });
991
+ contexts.set(record, { key, variableMap, sources: [source.label], scopeIds: [source.scopeId], label: `${shared ? '組織共通 / ' : ''}${source.label}` });
922
992
  }
923
993
  }
924
994
  }
@@ -0,0 +1,88 @@
1
+ /*
2
+ * 「情報の種類とつながり方」. Load after workspace-kit.css and graph-view-shared.css.
3
+ * Column templates of the kinds and relations ledgers, with and without the
4
+ * count column, and a 760px variant.
5
+ * Uses only the shared tokens in brainbase-tokens.css.
6
+ */
7
+ .bb-go .bb-go-caption,
8
+ .bb-go .bb-go-note {
9
+ margin: 0 0 10px;
10
+ color: var(--bb-color-muted);
11
+ font-size: 12px;
12
+ }
13
+
14
+ .bb-go .bb-ws-section-title {
15
+ margin-top: 30px;
16
+ }
17
+
18
+ .bb-go > .bb-graph-plain-list {
19
+ color: var(--bb-color-muted);
20
+ }
21
+
22
+ .bb-go > .bb-graph-audit {
23
+ margin-top: 14px;
24
+ }
25
+
26
+ .bb-go-type-ledger .bb-ws-ledger-row {
27
+ grid-template-columns: 160px minmax(0, 1fr);
28
+ }
29
+
30
+ .bb-go-type-ledger.is-counted .bb-ws-ledger-row {
31
+ grid-template-columns: 160px minmax(0, 1fr) 96px;
32
+ }
33
+
34
+ .bb-go-relation-ledger .bb-ws-ledger-row {
35
+ grid-template-columns: 140px minmax(0, 1.4fr) minmax(0, 1.6fr);
36
+ }
37
+
38
+ .bb-go-relation-ledger.is-counted .bb-ws-ledger-row {
39
+ grid-template-columns: 140px minmax(0, 1.4fr) minmax(0, 1.6fr) minmax(0, .8fr);
40
+ }
41
+
42
+ .bb-go-type-ledger .bb-ws-ledger-row,
43
+ .bb-go-relation-ledger .bb-ws-ledger-row {
44
+ min-height: 48px;
45
+ align-items: start;
46
+ }
47
+
48
+ /* Meanings and kinds are read, not scanned: let them wrap. */
49
+ .bb-go-type-ledger .bb-ws-ledger-row > span,
50
+ .bb-go-relation-ledger .bb-ws-ledger-row > span {
51
+ white-space: normal;
52
+ }
53
+
54
+ .bb-go-type-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head) > :first-child,
55
+ .bb-go-relation-ledger .bb-ws-ledger-row:not(.bb-ws-ledger-head) > :first-child {
56
+ color: var(--bb-color-ink);
57
+ font-weight: 690;
58
+ }
59
+
60
+ .bb-go-kinds {
61
+ margin: 0;
62
+ padding: 0;
63
+ list-style: none;
64
+ }
65
+
66
+ .bb-go-kinds > li + li {
67
+ margin-top: 4px;
68
+ }
69
+
70
+ @media (max-width: 760px) {
71
+ .bb-go-type-ledger .bb-ws-ledger-row,
72
+ .bb-go-type-ledger.is-counted .bb-ws-ledger-row {
73
+ grid-template-columns: 96px minmax(0, 1fr);
74
+ }
75
+
76
+ .bb-go-type-ledger.is-counted .bb-ws-ledger-row > :nth-child(3) {
77
+ display: none;
78
+ }
79
+
80
+ .bb-go-relation-ledger .bb-ws-ledger-row,
81
+ .bb-go-relation-ledger.is-counted .bb-ws-ledger-row {
82
+ grid-template-columns: 96px minmax(0, 1fr);
83
+ }
84
+
85
+ .bb-go-relation-ledger .bb-ws-ledger-row > :nth-child(n + 3) {
86
+ display: none;
87
+ }
88
+ }