@unson/brainbase-mcp 0.19.0 → 0.20.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.
@@ -693,6 +693,7 @@ export function createGraphWebModule(context) {
693
693
  'graph-own-share.js',
694
694
  'graph-projects-view.css',
695
695
  'project-workspace.js',
696
+ 'project-overview.js',
696
697
  'project-workspace.css',
697
698
  'project-graph.js',
698
699
  'project-graph-entry.js',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
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",
@@ -289,6 +289,7 @@
289
289
  },
290
290
  "./contracts/foundation/graph-history.sql": "./contracts/foundation/graph-history.sql",
291
291
  "./ui/project-workspace": "./ui/project-workspace.js",
292
+ "./ui/project-overview": "./ui/project-overview.js",
292
293
  "./ui/project-graph": "./ui/project-graph.js",
293
294
  "./ui/project-workspace.css": "./ui/project-workspace.css",
294
295
  "./ui/project-graph-vendor.js": "./ui/project-graph-vendor.js"
@@ -363,5 +364,5 @@
363
364
  "engines": {
364
365
  "node": ">=20"
365
366
  },
366
- "gitHead": "7cd7be975882fd1cbc797b7992b27a221f9fc02f"
367
+ "gitHead": "eae9755b000357391dc4f1244537e8b6bfd7925c"
367
368
  }
@@ -0,0 +1,263 @@
1
+ /*
2
+ * Optional host projection for the first project overview surface.
3
+ *
4
+ * This module only renders values supplied by an authorized host. It does not
5
+ * derive relationships, counts, next actions, or material membership from the
6
+ * shared Graph payload.
7
+ */
8
+
9
+ import { formatDateTime, makeElement, textOrNull } from './graph-view-shared.js';
10
+ import { workspaceNotice } from './workspace-kit.js';
11
+
12
+ const MATERIAL_STATES = new Set(['ok', 'partial', 'unknown', 'failed', 'unavailable', 'loading']);
13
+ const BODY_STATES = new Set(['retrieved', 'unretrieved', 'unavailable', 'failed', 'unknown']);
14
+
15
+ const MATERIAL_STATE_LABELS = Object.freeze({
16
+ ok: '取得済み',
17
+ partial: '一部取得',
18
+ unknown: '未確認',
19
+ failed: '読み取り失敗',
20
+ unavailable: '未接続',
21
+ loading: '読み込み中',
22
+ });
23
+
24
+ const BODY_STATE_LABELS = Object.freeze({
25
+ retrieved: '本文取得済み',
26
+ unretrieved: '本文未取得',
27
+ unavailable: '本文未接続',
28
+ failed: '本文読取失敗',
29
+ unknown: '本文状態未確認',
30
+ });
31
+
32
+ function cleanText(value) {
33
+ return textOrNull(value);
34
+ }
35
+
36
+ function normalizeTotal(value) {
37
+ if (Number.isInteger(value) && value >= 0) return value;
38
+ if (typeof value === 'string' && /^\d+$/u.test(value)) return Number(value);
39
+ return null;
40
+ }
41
+
42
+ function normalizeDate(value) {
43
+ return cleanText(value);
44
+ }
45
+
46
+ function normalizeMaterialItem(item) {
47
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
48
+ const id = cleanText(item.id);
49
+ const title = cleanText(item.title) ?? (id ? id : '資料名未確認');
50
+ const relation = cleanText(item.relation);
51
+ const source = cleanText(item.source);
52
+ const bodyState = BODY_STATES.has(item.bodyState) ? item.bodyState : 'unknown';
53
+ const contentAsOf = normalizeDate(item.contentAsOf);
54
+ const updatedAt = normalizeDate(item.updatedAt);
55
+ if (!id && !relation && !source && !contentAsOf && !updatedAt && bodyState === 'unknown' && title === '資料名未確認') return null;
56
+ return { id, title, relation, source, bodyState, contentAsOf, updatedAt };
57
+ }
58
+
59
+ function normalizeMaterials(value) {
60
+ const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
61
+ const state = MATERIAL_STATES.has(input.state) ? input.state : 'unknown';
62
+ return {
63
+ state,
64
+ items: Array.isArray(input.items) ? input.items.map(normalizeMaterialItem).filter(Boolean) : [],
65
+ total: normalizeTotal(input.total),
66
+ note: cleanText(input.note),
67
+ };
68
+ }
69
+
70
+ function normalizeCheck(item) {
71
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
72
+ const id = cleanText(item.id);
73
+ const title = cleanText(item.title) ?? (id ? id : '確認事項未確認');
74
+ return {
75
+ id,
76
+ title,
77
+ summary: cleanText(item.summary),
78
+ recordId: cleanText(item.recordId),
79
+ };
80
+ }
81
+
82
+ function normalizeAbout(value) {
83
+ const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
84
+ return {
85
+ definition: cleanText(input.definition),
86
+ owner: cleanText(input.owner),
87
+ finalApprover: cleanText(input.finalApprover),
88
+ goal: cleanText(input.goal),
89
+ source: cleanText(input.source),
90
+ asOf: normalizeDate(input.asOf),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Normalize the optional host projection without turning malformed or missing
96
+ * fields into successful empty data.
97
+ */
98
+ export function normalizeProjectOverview(value) {
99
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
100
+ return {
101
+ about: normalizeAbout(value.about),
102
+ checks: Array.isArray(value.checks) ? value.checks.map(normalizeCheck).filter(Boolean) : [],
103
+ directMaterials: normalizeMaterials(value.directMaterials),
104
+ relatedMaterials: normalizeMaterials(value.relatedMaterials),
105
+ };
106
+ }
107
+
108
+ function displayDate(value) {
109
+ if (!value) return '未確認';
110
+ return Number.isNaN(Date.parse(value)) ? '日時未確認' : formatDateTime(value);
111
+ }
112
+
113
+ function stateLabel(state) {
114
+ return MATERIAL_STATE_LABELS[state] ?? '未確認';
115
+ }
116
+
117
+ function renderAbout(doc, about) {
118
+ const section = makeElement(doc, 'section', {
119
+ className: 'bb-pkw-overview-section bb-pkw-overview-about',
120
+ attrs: { 'aria-label': 'このプロジェクトについて' },
121
+ });
122
+ section.append(makeElement(doc, 'h3', { text: 'このプロジェクトについて' }));
123
+ const facts = makeElement(doc, 'dl', { className: 'bb-pkw-overview-facts' });
124
+ for (const [label, value] of [
125
+ ['何のプロジェクトか', about.definition],
126
+ ['責任者', about.owner],
127
+ ['最終決裁', about.finalApprover],
128
+ ['現在の目標', about.goal],
129
+ ]) {
130
+ const row = makeElement(doc, 'div', { className: 'bb-pkw-overview-fact' });
131
+ row.append(makeElement(doc, 'dt', { text: label }), makeElement(doc, 'dd', { text: value ?? '未確認' }));
132
+ facts.append(row);
133
+ }
134
+ section.append(facts);
135
+ const metadata = makeElement(doc, 'p', { className: 'bb-pkw-overview-meta' });
136
+ metadata.append(
137
+ makeElement(doc, 'span', { text: `出典: ${about.source ?? '未確認'}` }),
138
+ makeElement(doc, 'span', { text: `読み取った時点: ${displayDate(about.asOf)}` }),
139
+ );
140
+ section.append(metadata);
141
+ return section;
142
+ }
143
+
144
+ function renderChecks(doc, checks, actions = {}) {
145
+ const section = makeElement(doc, 'section', {
146
+ className: 'bb-pkw-overview-section bb-pkw-overview-checks',
147
+ attrs: { 'aria-label': '次に確認すること' },
148
+ });
149
+ section.append(makeElement(doc, 'h3', { text: '次に確認すること' }));
150
+ if (checks.length === 0) {
151
+ section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '確認事項は未確認です。' }));
152
+ return section;
153
+ }
154
+ const list = makeElement(doc, 'ol', { className: 'bb-pkw-overview-check-list' });
155
+ for (const check of checks) {
156
+ const item = makeElement(doc, 'li', { className: 'bb-pkw-overview-check' });
157
+ // `id` identifies the host-side check; it is not necessarily a Graph ID.
158
+ // Only an explicit canonical recordId may open the existing Graph detail.
159
+ const targetId = check.recordId;
160
+ const title = typeof actions.openGraphEntity === 'function' && targetId
161
+ ? makeElement(doc, 'button', { className: 'bb-pkw-overview-link', text: check.title, attrs: { type: 'button', 'data-record-id': targetId } })
162
+ : makeElement(doc, 'strong', { text: check.title });
163
+ if (typeof actions.openGraphEntity === 'function' && targetId) title.addEventListener('click', () => actions.openGraphEntity(targetId));
164
+ item.append(title);
165
+ if (check.summary) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: check.summary }));
166
+ list.append(item);
167
+ }
168
+ section.append(list);
169
+ return section;
170
+ }
171
+
172
+ function renderMaterialState(doc, materials) {
173
+ const state = materials.state;
174
+ const total = materials.total;
175
+ let text;
176
+ if (state === 'ok') {
177
+ if (total === 0 && materials.items.length === 0) text = '確認済み 0件';
178
+ else if (total === null) text = materials.items.length > 0 ? `表示 ${materials.items.length}件 · 総数未確認` : '件数未確認';
179
+ else text = `表示 ${materials.items.length}件 · 総数 ${total}件${total > materials.items.length ? ' · 続き未取得' : ''}`;
180
+ } else if (total !== null) {
181
+ text = `表示 ${materials.items.length}件 · 総数 ${total}件 · ${stateLabel(state)}`;
182
+ } else {
183
+ text = materials.items.length > 0 ? `表示 ${materials.items.length}件 · 総数未確認 · ${stateLabel(state)}` : stateLabel(state);
184
+ }
185
+ return makeElement(doc, 'p', { className: 'bb-pkw-overview-material-state', text });
186
+ }
187
+
188
+ function materialNotice(doc, materials) {
189
+ if (materials.state === 'ok') return null;
190
+ const tone = materials.state === 'failed' ? 'danger' : materials.state === 'unknown' || materials.state === 'partial' ? 'warning' : 'info';
191
+ return workspaceNotice(doc, {
192
+ label: stateLabel(materials.state),
193
+ text: materials.note ?? (materials.state === 'partial' ? '一部の資料だけ取得されています。' : '資料の取得状態を確認できません。'),
194
+ tone,
195
+ role: materials.state === 'failed' ? 'alert' : 'status',
196
+ });
197
+ }
198
+
199
+ function renderMaterialTable(doc, materials, actions = {}) {
200
+ const table = makeElement(doc, 'div', {
201
+ className: 'bb-pkw-overview-material-table',
202
+ attrs: { role: 'table', 'aria-label': '資料一覧' },
203
+ });
204
+ const columns = ['資料', '関係', '出典', '本文', '内容時点', '記録更新'];
205
+ const header = makeElement(doc, 'div', { className: 'bb-pkw-overview-material-row bb-pkw-overview-material-header', attrs: { role: 'row' } });
206
+ for (const column of columns) header.append(makeElement(doc, 'span', { text: column, attrs: { role: 'columnheader' } }));
207
+ table.append(header);
208
+ if (materials.items.length === 0) {
209
+ const empty = materials.state === 'ok' && materials.total === 0
210
+ ? '確認済みの資料はありません。'
211
+ : '資料の行は未確認です。';
212
+ table.append(makeElement(doc, 'p', { className: 'bb-pkw-muted bb-pkw-overview-material-empty', text: empty }));
213
+ return table;
214
+ }
215
+ for (const material of materials.items) {
216
+ const row = makeElement(doc, 'div', { className: 'bb-pkw-overview-material-row', attrs: { role: 'row' } });
217
+ const title = typeof actions.openGraphEntity === 'function' && material.id
218
+ ? makeElement(doc, 'button', { className: 'bb-pkw-overview-link', text: material.title, attrs: { type: 'button', 'data-record-id': material.id } })
219
+ : makeElement(doc, 'strong', { text: material.title });
220
+ if (typeof actions.openGraphEntity === 'function' && material.id) title.addEventListener('click', () => actions.openGraphEntity(material.id));
221
+ row.append(
222
+ makeElement(doc, 'span', { attrs: { role: 'cell' } }),
223
+ makeElement(doc, 'span', { text: material.relation ?? '関係未確認', attrs: { role: 'cell' } }),
224
+ makeElement(doc, 'span', { text: material.source ?? '出典未確認', attrs: { role: 'cell' } }),
225
+ makeElement(doc, 'span', { text: BODY_STATE_LABELS[material.bodyState] ?? BODY_STATE_LABELS.unknown, attrs: { role: 'cell' } }),
226
+ makeElement(doc, 'time', { text: displayDate(material.contentAsOf), attrs: { role: 'cell', ...(material.contentAsOf ? { datetime: material.contentAsOf } : {}) } }),
227
+ makeElement(doc, 'time', { text: displayDate(material.updatedAt), attrs: { role: 'cell', ...(material.updatedAt ? { datetime: material.updatedAt } : {}) } }),
228
+ );
229
+ row.children[0].append(title);
230
+ table.append(row);
231
+ }
232
+ return table;
233
+ }
234
+
235
+ function renderMaterials(doc, title, materials, actions = {}) {
236
+ const section = makeElement(doc, 'section', {
237
+ className: 'bb-pkw-overview-section bb-pkw-overview-materials',
238
+ attrs: { 'aria-label': title },
239
+ });
240
+ const heading = makeElement(doc, 'div', { className: 'bb-pkw-overview-section-heading' });
241
+ heading.append(makeElement(doc, 'h3', { text: title }), renderMaterialState(doc, materials));
242
+ section.append(heading);
243
+ const notice = materialNotice(doc, materials);
244
+ if (notice) section.append(notice);
245
+ if (materials.note && materials.state === 'ok') section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: materials.note }));
246
+ section.append(renderMaterialTable(doc, materials, actions));
247
+ return section;
248
+ }
249
+
250
+ /** Render the host-provided overview in the required cognitive order. */
251
+ export function renderProjectOverview(doc, value, { projectName = 'プロジェクト', actions = {} } = {}) {
252
+ const overview = normalizeProjectOverview(value);
253
+ if (!overview) return null;
254
+ const name = cleanText(projectName) ?? 'プロジェクト';
255
+ const root = makeElement(doc, 'div', { className: 'bb-pkw-overview-projection', attrs: { 'aria-label': 'プロジェクト概要' } });
256
+ root.append(
257
+ renderAbout(doc, overview.about),
258
+ renderChecks(doc, overview.checks, actions),
259
+ renderMaterials(doc, `${name}に直接紐づく資料`, overview.directMaterials, actions),
260
+ renderMaterials(doc, '関連資料', overview.relatedMaterials, actions),
261
+ );
262
+ return root;
263
+ }
@@ -1187,3 +1187,178 @@ button.bb-pkw-context-task-title:focus-visible { outline: 3px solid var(--bb-col
1187
1187
  border-radius: 0;
1188
1188
  padding: 14px 16px;
1189
1189
  }
1190
+
1191
+ /* Optional host projection: context first, then the evidence ledgers. */
1192
+ .bb-pkw-overview-projection {
1193
+ display: grid;
1194
+ gap: var(--bb-space-5);
1195
+ min-width: 0;
1196
+ }
1197
+
1198
+ .bb-pkw-overview-section {
1199
+ display: grid;
1200
+ gap: var(--bb-space-3);
1201
+ min-width: 0;
1202
+ border-top: 1px solid var(--bb-color-border-strong);
1203
+ padding-top: var(--bb-space-4);
1204
+ }
1205
+
1206
+ .bb-pkw-overview-section h3 {
1207
+ margin: 0;
1208
+ color: var(--bb-color-ink);
1209
+ font-size: 16px;
1210
+ font-weight: 760;
1211
+ letter-spacing: -.02em;
1212
+ }
1213
+
1214
+ .bb-pkw-overview-facts {
1215
+ display: grid;
1216
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1217
+ gap: var(--bb-space-3);
1218
+ margin: 0;
1219
+ }
1220
+
1221
+ .bb-pkw-overview-fact {
1222
+ display: grid;
1223
+ gap: 4px;
1224
+ min-width: 0;
1225
+ }
1226
+
1227
+ .bb-pkw-overview-fact dt,
1228
+ .bb-pkw-overview-fact dd {
1229
+ margin: 0;
1230
+ }
1231
+
1232
+ .bb-pkw-overview-fact dt {
1233
+ color: var(--bb-color-muted);
1234
+ font-size: 11px;
1235
+ font-weight: 720;
1236
+ }
1237
+
1238
+ .bb-pkw-overview-fact dd {
1239
+ overflow-wrap: anywhere;
1240
+ color: var(--bb-color-ink);
1241
+ font-size: 13px;
1242
+ line-height: 1.6;
1243
+ }
1244
+
1245
+ .bb-pkw-overview-meta {
1246
+ display: flex;
1247
+ flex-wrap: wrap;
1248
+ gap: 4px var(--bb-space-4);
1249
+ margin: 0;
1250
+ color: var(--bb-color-muted);
1251
+ font-size: 11px;
1252
+ }
1253
+
1254
+ .bb-pkw-overview-check-list {
1255
+ display: grid;
1256
+ gap: 0;
1257
+ margin: 0;
1258
+ padding: 0 0 0 20px;
1259
+ }
1260
+
1261
+ .bb-pkw-overview-check {
1262
+ min-width: 0;
1263
+ border-bottom: 1px solid var(--bb-color-border);
1264
+ padding: var(--bb-space-3) 0;
1265
+ }
1266
+
1267
+ .bb-pkw-overview-check:first-child { padding-top: 0; }
1268
+ .bb-pkw-overview-check:last-child { border-bottom: 0; padding-bottom: 0; }
1269
+
1270
+ .bb-pkw-overview-check p {
1271
+ margin: 4px 0 0;
1272
+ }
1273
+
1274
+ .bb-pkw-overview-link {
1275
+ max-width: 100%;
1276
+ overflow-wrap: anywhere;
1277
+ border: 0;
1278
+ padding: 0;
1279
+ color: var(--bb-color-accent-strong);
1280
+ background: transparent;
1281
+ font: inherit;
1282
+ font-weight: 720;
1283
+ text-align: left;
1284
+ text-decoration: none;
1285
+ cursor: pointer;
1286
+ }
1287
+
1288
+ .bb-pkw-overview-link:hover {
1289
+ color: var(--bb-color-accent);
1290
+ text-decoration: underline;
1291
+ text-underline-offset: 3px;
1292
+ }
1293
+
1294
+ .bb-pkw-overview-link:focus-visible {
1295
+ outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 45%, var(--bb-color-panel));
1296
+ outline-offset: 2px;
1297
+ }
1298
+
1299
+ .bb-pkw-overview-section-heading {
1300
+ display: flex;
1301
+ flex-wrap: wrap;
1302
+ gap: var(--bb-space-2);
1303
+ align-items: baseline;
1304
+ justify-content: space-between;
1305
+ min-width: 0;
1306
+ }
1307
+
1308
+ .bb-pkw-overview-material-state {
1309
+ margin: 0;
1310
+ color: var(--bb-color-muted);
1311
+ font-size: 11px;
1312
+ }
1313
+
1314
+ .bb-pkw-overview-material-table {
1315
+ min-width: 0;
1316
+ overflow-x: auto;
1317
+ border-top: 1px solid var(--bb-color-border-strong);
1318
+ }
1319
+
1320
+ .bb-pkw-overview-material-row {
1321
+ display: grid;
1322
+ grid-template-columns: minmax(220px, 2fr) minmax(110px, 1fr) minmax(130px, 1.15fr) minmax(120px, 1fr) minmax(135px, 1fr) minmax(135px, 1fr);
1323
+ gap: var(--bb-space-2);
1324
+ align-items: center;
1325
+ min-width: 900px;
1326
+ border-bottom: 1px solid var(--bb-color-border);
1327
+ padding: var(--bb-space-3) 0;
1328
+ color: var(--bb-color-muted);
1329
+ font-size: 11px;
1330
+ }
1331
+
1332
+ .bb-pkw-overview-material-header {
1333
+ border-bottom-color: var(--bb-color-border-strong);
1334
+ padding-block: var(--bb-space-2);
1335
+ font-weight: 720;
1336
+ }
1337
+
1338
+ .bb-pkw-overview-material-row > span,
1339
+ .bb-pkw-overview-material-row > time {
1340
+ min-width: 0;
1341
+ overflow-wrap: anywhere;
1342
+ }
1343
+
1344
+ .bb-pkw-overview-material-row > span:first-child {
1345
+ color: var(--bb-color-ink);
1346
+ }
1347
+
1348
+ .bb-pkw-overview-material-row:last-child { border-bottom: 0; }
1349
+
1350
+ .bb-pkw-overview-material-empty {
1351
+ margin: 0;
1352
+ padding: var(--bb-space-3) 0;
1353
+ }
1354
+
1355
+ @media (max-width: 760px) {
1356
+ .bb-pkw-overview-facts {
1357
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1358
+ }
1359
+
1360
+ .bb-pkw-overview-section-heading {
1361
+ align-items: flex-start;
1362
+ flex-direction: column;
1363
+ }
1364
+ }
@@ -25,6 +25,7 @@ import {
25
25
  validityText,
26
26
  } from './graph-view-shared.js';
27
27
  import { workspaceDefinition, workspaceNotice } from './workspace-kit.js';
28
+ import { normalizeProjectOverview, renderProjectOverview } from './project-overview.js';
28
29
 
29
30
  export const PROJECT_WORKSPACE_CONTRACT_VERSION = 'brainbase.project-workspace.v1';
30
31
 
@@ -243,6 +244,31 @@ function graphEvidenceEdges(detail) {
243
244
  return projectKnowledgeEdges(detail);
244
245
  }
245
246
 
247
+ /**
248
+ * Keep overview-only canonical IDs available for the existing detail action.
249
+ * These are loading placeholders, not Graph entities or inferred relations;
250
+ * the host must still provide readEntity before a projection-only ID can be
251
+ * selected.
252
+ */
253
+ function projectOverviewProjectionRecords(overview) {
254
+ const records = new Map();
255
+ if (!overview) return records;
256
+ const add = (id, title) => {
257
+ const target = cleanId(id);
258
+ if (!target || records.has(target)) return;
259
+ records.set(target, {
260
+ id: target,
261
+ type: 'unknown',
262
+ name: cleanName(title, target),
263
+ });
264
+ };
265
+ for (const check of overview.checks) add(check.recordId, check.title);
266
+ for (const material of [...overview.directMaterials.items, ...overview.relatedMaterials.items]) {
267
+ add(material.id, material.title);
268
+ }
269
+ return records;
270
+ }
271
+
246
272
  function resultEntity(result) {
247
273
  if (!isRecord(result)) return null;
248
274
  const payload = result.state === 'ok' && isRecord(result.payload) ? result.payload : result;
@@ -966,6 +992,12 @@ function renderOverview(doc, detail, context, entities, actions = {}) {
966
992
  const payload = projectDetailPayload(detail);
967
993
  const project = payload?.project;
968
994
  const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
995
+ const overview = normalizeProjectOverview(payload?.overview);
996
+ if (overview) {
997
+ panel.append(renderProjectOverview(doc, overview, { projectName: project?.name, actions }));
998
+ panel.append(renderContextDirectory(doc, context, actions));
999
+ return panel;
1000
+ }
969
1001
  panel.append(renderDenseOverview(doc, detail, context, entities, actions));
970
1002
  panel.append(renderContextDirectory(doc, context, actions));
971
1003
  const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
@@ -1011,6 +1043,7 @@ export function createProjectKnowledgeWorkspace({
1011
1043
  // but those records are not evidence that they belong to this project.
1012
1044
  const overviewEntities = graphEntityMap(payload);
1013
1045
  const overviewEdges = graphEvidenceEdges(payload);
1046
+ const overviewProjectionRecords = projectOverviewProjectionRecords(normalizeProjectOverview(payload?.overview));
1014
1047
  const entities = new Map(overviewEntities);
1015
1048
  const edges = [...overviewEdges];
1016
1049
  let destroyed = false;
@@ -1137,7 +1170,8 @@ export function createProjectKnowledgeWorkspace({
1137
1170
 
1138
1171
  function select(id, { fromGraph = false } = {}) {
1139
1172
  const target = cleanId(id);
1140
- if (!target || !entities.has(target) || destroyed) return;
1173
+ const canReadOverviewProjection = overviewProjectionRecords.has(target) && typeof readEntity === 'function';
1174
+ if (!target || (!entities.has(target) && !canReadOverviewProjection) || destroyed) return;
1141
1175
  // A list click synchronizes Sigma by calling its `select`, which notifies
1142
1176
  // this workspace again. Keep the second notification from starting a
1143
1177
  // duplicate entity read (or resetting a completed detail read).
@@ -1155,7 +1189,8 @@ export function createProjectKnowledgeWorkspace({
1155
1189
 
1156
1190
  function selectAndExplore(id) {
1157
1191
  const target = cleanId(id);
1158
- if (!target || !entities.has(target) || destroyed) return;
1192
+ const canReadOverviewProjection = overviewProjectionRecords.has(target) && typeof readEntity === 'function';
1193
+ if (!target || (!entities.has(target) && !canReadOverviewProjection) || destroyed) return;
1159
1194
  activeTab = 'graph';
1160
1195
  select(target);
1161
1196
  render();
@@ -1201,7 +1236,7 @@ export function createProjectKnowledgeWorkspace({
1201
1236
  const panel = root.querySelector?.('.bb-pkw-selection') ?? selectedPanel;
1202
1237
  if (!panel) return;
1203
1238
  panel.replaceChildren();
1204
- const entity = entities.get(selectedId);
1239
+ const entity = entities.get(selectedId) ?? overviewProjectionRecords.get(selectedId);
1205
1240
  if (!entity) return;
1206
1241
  const read = entityRead?.id === selectedId ? entityRead : null;
1207
1242
  const fetched = selectedId === project.id ? { entity: project, incoming: [], outgoing: [] } : read?.entity ? read : null;