@unson/brainbase-mcp 0.18.0 → 0.19.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.
@@ -0,0 +1,766 @@
1
+ /*
2
+ * Foundation overview: the first read-only view of a selected project.
3
+ *
4
+ * The host supplies an authenticated, scope-bound `port.readCatalog()`; this
5
+ * module never fetches a database, writes a record, or treats a failed read as
6
+ * an empty catalog. The screen deliberately keeps recorded facts, missing
7
+ * definitions, and hypotheses visually separate.
8
+ */
9
+
10
+ import {
11
+ workspaceButton,
12
+ workspaceDefinition,
13
+ workspaceLedger,
14
+ workspaceMetrics,
15
+ workspaceNotice,
16
+ workspacePageHeader,
17
+ workspaceSectionTitle,
18
+ } from './workspace-kit.js';
19
+
20
+ export const FOUNDATION_OVERVIEW_CONTRACT_VERSION = 'foundation-overview.v1';
21
+ export const FOUNDATION_OVERVIEW_PAGE_SIZE = 50;
22
+
23
+ const ADOPTION_LABELS = Object.freeze({
24
+ draft: '草案',
25
+ proposed: '提案中',
26
+ approved: '承認済み',
27
+ retired: '終了',
28
+ });
29
+ const EPISTEMIC_LABELS = Object.freeze({
30
+ unverified: '未検証',
31
+ hypothesis: '仮説',
32
+ supported: '支持',
33
+ verified: '検証済み',
34
+ refuted: '反証',
35
+ });
36
+ const VALIDATION_LABELS = Object.freeze({
37
+ unverified: '未検証',
38
+ in_progress: '検証中',
39
+ supported: '支持',
40
+ verified: '検証済み',
41
+ refuted: '反証',
42
+ });
43
+ const USE_LABELS = Object.freeze({
44
+ draft: '草案',
45
+ judgment: '判断',
46
+ evaluation: '評価',
47
+ execution: '実行',
48
+ });
49
+ const PHILOSOPHY_STATUS_LABELS = Object.freeze({
50
+ active: '有効',
51
+ draft: '草案',
52
+ proposed: '提案中',
53
+ inactive: '停止',
54
+ retired: '終了',
55
+ archived: '終了',
56
+ });
57
+ const OPERATOR_LABELS = Object.freeze({ at_least: '以上', at_most: '以下', equals: '一致' });
58
+
59
+ export class FoundationOverviewError extends Error {
60
+ constructor(code, message) {
61
+ super(message);
62
+ this.name = 'FoundationOverviewError';
63
+ this.code = code;
64
+ }
65
+ }
66
+
67
+ function isObject(value) {
68
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
69
+ }
70
+
71
+ function nonEmpty(value) {
72
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
73
+ }
74
+
75
+ function getDocument(explicit) {
76
+ const value = explicit ?? (typeof document === 'undefined' ? null : document);
77
+ if (!value || typeof value.createElement !== 'function') throw new TypeError('document_unavailable');
78
+ return value;
79
+ }
80
+
81
+ function makeElement(doc, tag, { className, text, attrs = {}, value, hidden, disabled } = {}) {
82
+ const element = doc.createElement(tag);
83
+ if (className) element.className = className;
84
+ if (text !== undefined && text !== null) element.textContent = String(text);
85
+ for (const [name, attrValue] of Object.entries(attrs)) {
86
+ if (attrValue === undefined || attrValue === null || attrValue === false) continue;
87
+ element.setAttribute(name, attrValue === true ? '' : String(attrValue));
88
+ }
89
+ if (value !== undefined && 'value' in element) element.value = String(value);
90
+ if (hidden !== undefined) element.hidden = Boolean(hidden);
91
+ if (disabled !== undefined) element.disabled = Boolean(disabled);
92
+ return element;
93
+ }
94
+
95
+ function append(parent, ...children) {
96
+ parent.append(...children.filter((child) => child !== null && child !== undefined));
97
+ return parent;
98
+ }
99
+
100
+ function firstValue(source, ...keys) {
101
+ if (!isObject(source)) return undefined;
102
+ for (const key of keys) {
103
+ if (source[key] !== undefined && source[key] !== null) return source[key];
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ function error(code, message) {
109
+ return new FoundationOverviewError(code, message);
110
+ }
111
+
112
+ function validateFoundationRecords(value, type, label) {
113
+ if (!Array.isArray(value)) throw error('invalid_schema', `${label}の形式を確認できません。`);
114
+ for (const [index, item] of value.entries()) {
115
+ if (!isObject(item) || !isObject(item.definition)
116
+ || item.definition.type !== type
117
+ || !nonEmpty(item.definition.id)
118
+ || !nonEmpty(item.definition.revision)
119
+ || !nonEmpty(item.digest)) {
120
+ throw error('invalid_schema', `${label}の${index + 1}件目の形式が不正です。`);
121
+ }
122
+ }
123
+ return value;
124
+ }
125
+
126
+ function validatePhilosophyRecords(value) {
127
+ if (!Array.isArray(value)) throw error('invalid_schema', '哲学の形式を確認できません。');
128
+ for (const [index, item] of value.entries()) {
129
+ if (!isObject(item)
130
+ || item.kind !== 'philosophy'
131
+ || !nonEmpty(item.id)
132
+ || !nonEmpty(item.revision)
133
+ || !nonEmpty(item.digest)
134
+ || !isObject(item.payload)) {
135
+ throw error('invalid_schema', `哲学の${index + 1}件目の形式が不正です。`);
136
+ }
137
+ }
138
+ return value;
139
+ }
140
+
141
+ /** Validate the host contract without converting an unreadable response into an empty catalog. */
142
+ export function normalizeFoundationCatalog(value, expectedScopeId) {
143
+ if (!isObject(value) || value.contractVersion !== FOUNDATION_OVERVIEW_CONTRACT_VERSION) {
144
+ throw error('invalid_schema', '目的と現状の応答形式を確認できません。');
145
+ }
146
+ const scopeId = nonEmpty(value.scopeId);
147
+ if (!scopeId) throw error('invalid_schema', '対象範囲が応答にありません。');
148
+ if (scopeId !== expectedScopeId) {
149
+ throw error('scope_mismatch', '選択した対象範囲と応答の対象範囲が一致しません。');
150
+ }
151
+ validateFoundationRecords(value.objectives, 'objective', '目的');
152
+ validateFoundationRecords(value.variables, 'variable', '変数');
153
+ validateFoundationRecords(value.models, 'model', '世界モデル');
154
+ validatePhilosophyRecords(value.philosophies);
155
+ return {
156
+ contractVersion: value.contractVersion,
157
+ scopeId,
158
+ objectives: value.objectives,
159
+ variables: value.variables,
160
+ models: value.models,
161
+ philosophies: value.philosophies,
162
+ };
163
+ }
164
+
165
+ function formatDate(value) {
166
+ if (typeof value !== 'string' || !value.trim()) return '未確認';
167
+ const time = Date.parse(value);
168
+ if (Number.isNaN(time)) return value;
169
+ return new Date(time).toISOString().slice(0, 10);
170
+ }
171
+
172
+ function formatPeriod(value) {
173
+ if (!isObject(value) || typeof value.from !== 'string' || typeof value.until !== 'string') return null;
174
+ return `${formatDate(value.from)} 〜 ${formatDate(value.until)}`;
175
+ }
176
+
177
+ function formatAny(value) {
178
+ if (value === undefined || value === null || value === '') return null;
179
+ if (typeof value === 'string') return value.trim() || null;
180
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
181
+ if (Array.isArray(value)) return value.map((item) => formatAny(item)).filter(Boolean).join('、') || null;
182
+ if (isObject(value)) {
183
+ const preferred = firstValue(value, 'text', 'description', 'statement', 'label', 'name', 'value');
184
+ if (preferred !== undefined && preferred !== value) return formatAny(preferred);
185
+ try { return JSON.stringify(value); } catch { return null; }
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function formatStructuredValue(value) {
191
+ if (value === undefined || value === null || value === '') return null;
192
+ if (Array.isArray(value)) return value.map((item) => formatStructuredValue(item)).filter(Boolean).join('、') || null;
193
+ if (isObject(value)) {
194
+ const preferred = firstValue(value, 'text', 'description', 'statement', 'label', 'name', 'value');
195
+ if (preferred !== undefined && preferred !== value) return formatStructuredValue(preferred);
196
+ return Object.entries(value)
197
+ .map(([key, item]) => {
198
+ const formatted = formatStructuredValue(item);
199
+ return formatted ? `${key}: ${formatted}` : null;
200
+ })
201
+ .filter(Boolean)
202
+ .join('、') || null;
203
+ }
204
+ return formatAny(value);
205
+ }
206
+
207
+ const SCOPE_LABELS = Object.freeze({
208
+ project: 'プロジェクト',
209
+ organization: '組織',
210
+ company: '組織',
211
+ });
212
+
213
+ function formatScope(value) {
214
+ if (!isObject(value)) return formatStructuredValue(value);
215
+ const type = formatAny(value.type);
216
+ const id = formatAny(value.id);
217
+ if (type && id) return `${SCOPE_LABELS[type] ?? type}: ${id}`;
218
+ return formatStructuredValue(value);
219
+ }
220
+
221
+ function recordDefinition(record) { return isObject(record?.definition) ? record.definition : {}; }
222
+
223
+ function foundationKey(ref) {
224
+ if (!isObject(ref) || !nonEmpty(ref.id) || !nonEmpty(ref.type) || !nonEmpty(ref.revision)) return null;
225
+ return `${ref.type}:${ref.id}@${ref.revision}`;
226
+ }
227
+
228
+ function referenceLabel(ref) {
229
+ const id = nonEmpty(ref?.id) ?? '不明な変数';
230
+ const revision = nonEmpty(ref?.revision) ?? '?';
231
+ return `${id}@${revision}`;
232
+ }
233
+
234
+ function statusText(definition) {
235
+ const status = ADOPTION_LABELS[definition.adoptionState] ?? '状態未確認';
236
+ const epistemic = EPISTEMIC_LABELS[definition.epistemicState];
237
+ const validation = VALIDATION_LABELS[definition.validationState];
238
+ const uses = Array.isArray(definition.authorizedUses)
239
+ ? definition.authorizedUses.map((use) => USE_LABELS[use] ?? String(use)).join('、')
240
+ : '未確認';
241
+ return [
242
+ `登録状態: ${status}`,
243
+ epistemic ? `認識: ${epistemic}` : null,
244
+ validation ? `検証: ${validation}` : null,
245
+ `許可用途: ${uses || '未確認'}`,
246
+ ].filter(Boolean).join(' / ');
247
+ }
248
+
249
+ function statusElement(doc, definition, className = '') {
250
+ return makeElement(doc, 'p', { className: `bb-fov-status${className ? ` ${className}` : ''}`, text: statusText(definition) });
251
+ }
252
+
253
+ function detailsFor(record) {
254
+ return isObject(record?.details) ? record.details : {};
255
+ }
256
+
257
+ function currentStateFor(record) {
258
+ const definition = recordDefinition(record);
259
+ const details = detailsFor(record);
260
+ const current = firstValue(details, 'current_state', 'currentState')
261
+ ?? firstValue(definition, 'current_state', 'currentState');
262
+ const asOf = firstValue(details, 'as_of', 'asOf', 'recorded_at', 'recordedAt')
263
+ ?? firstValue(record, 'as_of', 'asOf', 'recorded_at', 'recordedAt');
264
+ const savedAt = firstValue(details, 'saved_at', 'savedAt', 'persisted_at', 'persistedAt')
265
+ ?? firstValue(record, 'saved_at', 'savedAt', 'persisted_at', 'persistedAt');
266
+ if (isObject(current)) {
267
+ const text = formatAny(firstValue(current, 'text', 'description', 'statement', 'value', 'summary'));
268
+ const status = formatAny(firstValue(current, 'status'));
269
+ const evidenceScope = formatAny(firstValue(current, 'evidence_scope', 'evidenceScope'));
270
+ const missingValue = firstValue(current, 'missing');
271
+ const missing = Array.isArray(missingValue)
272
+ ? missingValue.map((item) => formatAny(item)).filter(Boolean)
273
+ : (formatAny(missingValue) ? [formatAny(missingValue)] : []);
274
+ return {
275
+ text,
276
+ status,
277
+ evidenceScope,
278
+ missing,
279
+ asOf: firstValue(current, 'as_of', 'asOf', 'recorded_at', 'recordedAt') ?? asOf,
280
+ savedAt: firstValue(current, 'saved_at', 'savedAt') ?? savedAt,
281
+ recorded: Boolean(text || status || evidenceScope || missing.length || asOf),
282
+ };
283
+ }
284
+ const text = formatAny(current);
285
+ return { text, status: null, evidenceScope: null, missing: [], asOf, savedAt, recorded: Boolean(text) };
286
+ }
287
+
288
+ function criteriaList(doc, definition, variableMap) {
289
+ const list = makeElement(doc, 'ul', { className: 'bb-fov-list' });
290
+ if (!Array.isArray(definition.criteria) || definition.criteria.length === 0) {
291
+ list.append(makeElement(doc, 'li', { className: 'is-unrecorded', text: '評価基準は未記録です。' }));
292
+ return list;
293
+ }
294
+ for (const criterion of definition.criteria) {
295
+ const ref = criterion?.variableRef;
296
+ const exact = variableMap.get(foundationKey(ref));
297
+ const exactDefinition = exact ? recordDefinition(exact) : null;
298
+ const name = exactDefinition
299
+ ? (formatAny(exactDefinition.meaning) ?? referenceLabel(ref))
300
+ : referenceLabel(ref);
301
+ const variableDetails = exactDefinition
302
+ ? [
303
+ ['定義', exactDefinition.definition],
304
+ ['評価規準', exactDefinition.rubric],
305
+ ['値域', exactDefinition.valueSpace],
306
+ ].map(([label, value]) => {
307
+ const text = formatStructuredValue(value);
308
+ return text ? `${label}: ${text}` : null;
309
+ }).filter(Boolean)
310
+ : [];
311
+ const operator = OPERATOR_LABELS[criterion?.operator] ?? '条件';
312
+ const target = formatAny(criterion?.target);
313
+ const status = exact ? '参照確認済み' : '参照未確認(基準変数の版が一致しません)';
314
+ list.append(makeElement(doc, 'li', {
315
+ className: exact ? '' : 'is-unrecorded',
316
+ text: `${name}${variableDetails.length > 0 ? `(${variableDetails.join(' / ')})` : ''}(${referenceLabel(ref)}) ${operator}${target === null ? '' : ` ${target}`} — ${status}`,
317
+ }));
318
+ }
319
+ return list;
320
+ }
321
+
322
+ function criteriaElement(doc, definition, details, variableMap) {
323
+ const wrap = makeElement(doc, 'div', { className: 'bb-fov-criteria' });
324
+ const recordedValue = firstValue(details, 'criteria_text', 'criteriaText');
325
+ const recordedTexts = Array.isArray(recordedValue)
326
+ ? recordedValue.map((item) => formatAny(item)).filter(Boolean)
327
+ : [formatAny(recordedValue)].filter(Boolean);
328
+ if (recordedTexts.length === 1) {
329
+ wrap.append(makeElement(doc, 'p', { className: 'bb-fov-criteria-text', text: recordedTexts[0] }));
330
+ } else if (recordedTexts.length > 1) {
331
+ const recordedList = makeElement(doc, 'ul', { className: 'bb-fov-list bb-fov-criteria-text-list' });
332
+ for (const text of recordedTexts) recordedList.append(makeElement(doc, 'li', { text }));
333
+ wrap.append(recordedList);
334
+ }
335
+ if (Array.isArray(definition.criteria) && definition.criteria.length > 0) {
336
+ wrap.append(criteriaList(doc, definition, variableMap));
337
+ } else if (recordedTexts.length === 0) {
338
+ wrap.append(criteriaList(doc, definition, variableMap));
339
+ }
340
+ return wrap;
341
+ }
342
+
343
+ function missingObjectiveFields(record) {
344
+ const definition = recordDefinition(record);
345
+ const current = currentStateFor(record);
346
+ const gaps = [];
347
+ if (!nonEmpty(definition.desiredState)) gaps.push('目指す状態');
348
+ if (!Array.isArray(definition.criteria) || definition.criteria.length === 0) gaps.push('評価基準');
349
+ if (!formatPeriod(definition.evaluationPeriod)) gaps.push('評価期間');
350
+ if (!nonEmpty(definition.accountableId)) gaps.push('責任者');
351
+ if (!current.recorded) gaps.push('記録上の現状');
352
+ return gaps;
353
+ }
354
+
355
+ function recordedStateElement(doc, record) {
356
+ const current = currentStateFor(record);
357
+ const body = makeElement(doc, 'div', { className: 'bb-fov-current-state' });
358
+ body.append(makeElement(doc, 'strong', {
359
+ text: current.text ?? (current.status ? `状態: ${current.status}` : current.recorded ? '保存時点の記録' : '未記録'),
360
+ }));
361
+ const details = [];
362
+ if (current.status && current.text) details.push(['状態', current.status]);
363
+ if (current.evidenceScope) details.push(['証拠範囲', current.evidenceScope]);
364
+ if (current.asOf !== undefined && current.asOf !== null) details.push(['記録時点', formatDate(current.asOf)]);
365
+ if (current.missing.length > 0) {
366
+ const missing = makeElement(doc, 'ul', { className: 'bb-fov-list bb-fov-current-missing' });
367
+ for (const item of current.missing) missing.append(makeElement(doc, 'li', { text: item }));
368
+ details.push(['保存時点で未確認', missing]);
369
+ }
370
+ if (details.length > 0) body.append(workspaceDefinition(doc, details));
371
+ body.append(makeElement(doc, 'p', {
372
+ className: current.recorded ? 'bb-fov-recorded-notice' : 'bb-fov-recorded-notice is-unrecorded',
373
+ text: current.recorded
374
+ ? `保存時点の記録です。記録時点: ${formatDate(current.asOf)}。${current.savedAt ? `保存日時: ${formatDate(current.savedAt)}。` : ''}`
375
+ : '記録上の現状は未記録です。',
376
+ }));
377
+ return body;
378
+ }
379
+
380
+ function objectiveCard(doc, record, variableMap) {
381
+ const definition = recordDefinition(record);
382
+ const details = detailsFor(record);
383
+ const title = formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id;
384
+ const gaps = missingObjectiveFields(record);
385
+ const current = currentStateFor(record);
386
+ const evaluationPeriod = formatAny(firstValue(details, 'evaluation_period_note', 'evaluationPeriodNote'))
387
+ ?? formatPeriod(definition.evaluationPeriod);
388
+ const evaluator = formatAny(firstValue(details, 'evaluator_note', 'evaluatorNote'));
389
+ const accountable = formatAny(definition.accountableId);
390
+ const beneficiary = formatAny(firstValue(details, 'beneficiary_description', 'beneficiaryDescription'))
391
+ ?? (Array.isArray(definition.beneficiaryIds) ? definition.beneficiaryIds.join('、') : null);
392
+ const card = makeElement(doc, 'article', { className: 'bb-fov-objective-card' });
393
+ card.append(makeElement(doc, 'h3', { text: title }), statusElement(doc, definition));
394
+ card.append(workspaceDefinition(doc, [
395
+ ['目指す状態', formatAny(definition.desiredState)],
396
+ ['評価基準', criteriaElement(doc, definition, details, variableMap)],
397
+ ['評価期間', evaluationPeriod],
398
+ ['評価者', evaluator],
399
+ ['責任者', accountable],
400
+ ['対象・受益者', beneficiary],
401
+ ['記録上の現状', recordedStateElement(doc, record)],
402
+ ]));
403
+ const gap = makeElement(doc, 'div', { className: 'bb-fov-gap' });
404
+ if (gaps.length > 0) {
405
+ gap.append(makeElement(doc, 'strong', { text: 'まだ確かめられていない点' }));
406
+ const list = makeElement(doc, 'ul', { className: 'bb-fov-list' });
407
+ for (const item of gaps) list.append(makeElement(doc, 'li', { text: `${item}: 未確定` }));
408
+ gap.append(list);
409
+ } else if (current.missing.length > 0) {
410
+ gap.append(makeElement(doc, 'p', { text: '保存時点で未確認の項目があります。' }));
411
+ } else {
412
+ gap.append(makeElement(doc, 'p', { text: '不足項目は記録上ありません。達成・採用・検証済みを意味しません。' }));
413
+ }
414
+ card.append(gap);
415
+ return card;
416
+ }
417
+
418
+ function renderObjectives(doc, catalog, viewState, callbacks) {
419
+ const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '目的' } });
420
+ section.append(workspaceSectionTitle(doc, {
421
+ title: '目的',
422
+ lead: '目指す状態、評価の定義、記録上の現状を分けて確認します。',
423
+ }));
424
+ if (catalog.objectives.length === 0) {
425
+ section.append(workspaceNotice(doc, { label: '目的', text: 'この対象範囲には登録された目的がありません。' }));
426
+ return section;
427
+ }
428
+ const variableMap = new Map(catalog.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
429
+ const rows = catalog.objectives.map((record) => {
430
+ const definition = recordDefinition(record);
431
+ const details = detailsFor(record);
432
+ const key = `${definition.id}@${definition.revision}`;
433
+ return {
434
+ key,
435
+ selected: key === viewState.objectiveKey,
436
+ onSelect: () => {
437
+ viewState.objectiveKey = key;
438
+ callbacks.render?.();
439
+ },
440
+ cells: [
441
+ { primary: formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id, secondary: `${definition.id}@${definition.revision}` },
442
+ formatAny(definition.desiredState) ?? '目指す状態: 未記録',
443
+ formatAny(definition.adoptionState) ? (ADOPTION_LABELS[definition.adoptionState] ?? definition.adoptionState) : '状態未確認',
444
+ ],
445
+ };
446
+ });
447
+ section.append(workspaceLedger(doc, {
448
+ className: 'bb-fov-objective-ledger',
449
+ ariaLabel: '登録された目的',
450
+ columns: ['目的', '目指す状態', '登録状態'],
451
+ rows,
452
+ }));
453
+ const selected = catalog.objectives.find((record) => {
454
+ const definition = recordDefinition(record);
455
+ return `${definition.id}@${definition.revision}` === viewState.objectiveKey;
456
+ }) ?? catalog.objectives[0];
457
+ if (selected) {
458
+ const selectedDefinition = recordDefinition(selected);
459
+ viewState.objectiveKey = `${selectedDefinition.id}@${selectedDefinition.revision}`;
460
+ if (catalog.objectives.length > 1) {
461
+ section.append(workspaceNotice(doc, {
462
+ label: '詳細表示',
463
+ text: `目的${catalog.objectives.length}件のうち1件を選択して詳細を表示しています。`,
464
+ }));
465
+ }
466
+ section.append(objectiveCard(doc, selected, variableMap));
467
+ }
468
+ return section;
469
+ }
470
+
471
+ function philosophyApplicability(payload) {
472
+ const text = formatAny(firstValue(payload, 'applicability_text', 'applicabilityText'));
473
+ if (text) return text;
474
+ const legacy = firstValue(payload, 'judgmentApplicability', 'judgment_applicability');
475
+ if (!isObject(legacy)) return formatAny(legacy);
476
+ const description = formatAny(firstValue(legacy, 'text', 'description', 'statement', 'applicability_text', 'applicabilityText'));
477
+ if (description) return description;
478
+ const scope = formatScope(legacy.scope);
479
+ const validFrom = legacy.validFrom ?? legacy.valid_from;
480
+ const validUntil = legacy.validUntil ?? legacy.valid_until;
481
+ const period = validFrom
482
+ ? `${formatDate(validFrom)}から${validUntil ? ` ${formatDate(validUntil)}まで` : ''}`
483
+ : null;
484
+ return [scope, period ? `適用期間: ${period}` : null].filter(Boolean).join(' / ') || formatStructuredValue(legacy);
485
+ }
486
+
487
+ function philosophyCard(doc, record) {
488
+ const payload = record.payload;
489
+ const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card' });
490
+ const title = formatAny(payload.title) ?? record.id;
491
+ card.append(makeElement(doc, 'h3', { text: title }));
492
+ const rawStatus = formatAny(payload.status);
493
+ const status = rawStatus ? (PHILOSOPHY_STATUS_LABELS[rawStatus] ?? `未確認(${rawStatus})`) : '状態未確認';
494
+ card.append(makeElement(doc, 'p', { className: 'bb-fov-status', text: `状態: ${status} / 版: ${record.revision}` }));
495
+ if (formatAny(payload.statement)) card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: formatAny(payload.statement) }));
496
+ if (formatAny(payload.summary)) card.append(makeElement(doc, 'p', { text: formatAny(payload.summary) }));
497
+ const applicability = record.applicability?.scope
498
+ ?? record.currentScope
499
+ ?? firstValue(payload.judgmentApplicability, 'scope');
500
+ card.append(workspaceDefinition(doc, [
501
+ ['判断への適用', philosophyApplicability(payload) ?? '未記録'],
502
+ ['適用範囲', formatScope(applicability) ?? '未記録'],
503
+ ]));
504
+ return card;
505
+ }
506
+
507
+ function renderPhilosophies(doc, catalog) {
508
+ const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '哲学' } });
509
+ section.append(workspaceSectionTitle(doc, {
510
+ title: '哲学',
511
+ lead: '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。',
512
+ }));
513
+ if (catalog.philosophies.length === 0) {
514
+ section.append(workspaceNotice(doc, { label: '哲学', text: 'この対象範囲には登録された哲学がありません。' }));
515
+ return section;
516
+ }
517
+ const grid = makeElement(doc, 'div', { className: 'bb-fov-philosophy-grid' });
518
+ for (const record of catalog.philosophies) grid.append(philosophyCard(doc, record));
519
+ section.append(grid);
520
+ return section;
521
+ }
522
+
523
+ function searchTextForModel(record) {
524
+ const definition = recordDefinition(record);
525
+ return [
526
+ definition.id,
527
+ definition.revision,
528
+ definition.meaning,
529
+ definition.relationship,
530
+ definition.uncertainty,
531
+ definition.epistemicState,
532
+ definition.validationState,
533
+ ...(Array.isArray(definition.authorizedUses) ? definition.authorizedUses : []),
534
+ ...(Array.isArray(definition.inputVariableRefs) ? definition.inputVariableRefs.flatMap((ref) => [ref.id, ref.revision]) : []),
535
+ ...(Array.isArray(definition.outputVariableRefs) ? definition.outputVariableRefs.flatMap((ref) => [ref.id, ref.revision]) : []),
536
+ ].filter(Boolean).join(' ').toLocaleLowerCase();
537
+ }
538
+
539
+ function renderReferenceList(doc, refs, variableMap, label) {
540
+ const list = makeElement(doc, 'ul', { className: 'bb-fov-ref-list' });
541
+ if (!Array.isArray(refs) || refs.length === 0) {
542
+ list.append(makeElement(doc, 'li', { className: 'is-unrecorded', text: `${label}: 未接続の定義です。` }));
543
+ return list;
544
+ }
545
+ for (const ref of refs) {
546
+ const exact = variableMap.get(foundationKey(ref));
547
+ const name = exact ? formatAny(recordDefinition(exact).meaning) ?? referenceLabel(ref) : referenceLabel(ref);
548
+ list.append(makeElement(doc, 'li', {
549
+ className: exact ? '' : 'is-unrecorded',
550
+ text: exact ? `${name}(${referenceLabel(ref)})` : `${referenceLabel(ref)} — 未接続(変数の版が一致しません)`,
551
+ }));
552
+ }
553
+ return list;
554
+ }
555
+
556
+ function modelCard(doc, record, variableMap) {
557
+ const definition = recordDefinition(record);
558
+ const card = makeElement(doc, 'article', { className: 'bb-fov-model-card' });
559
+ card.append(makeElement(doc, 'h3', { text: formatAny(definition.meaning) ?? definition.id }));
560
+ card.append(makeElement(doc, 'code', { className: 'bb-fov-record-id', text: `${definition.id}@${definition.revision}` }));
561
+ card.append(statusElement(doc, definition));
562
+ card.append(workspaceDefinition(doc, [
563
+ ['関係', formatAny(definition.relationship)],
564
+ ['適用範囲', formatAny(definition.applicability ?? definition.scope)],
565
+ ['不確かさ', formatAny(definition.uncertainty)],
566
+ ['入力変数', renderReferenceList(doc, definition.inputVariableRefs, variableMap, '入力変数')],
567
+ ['出力変数', renderReferenceList(doc, definition.outputVariableRefs, variableMap, '出力変数')],
568
+ ]));
569
+ return card;
570
+ }
571
+
572
+ function renderModels(doc, catalog, viewState, callbacks) {
573
+ const section = makeElement(doc, 'section', { className: 'bb-fov-section', attrs: { 'aria-label': '世界モデル' } });
574
+ section.append(workspaceSectionTitle(doc, {
575
+ title: '世界モデル',
576
+ lead: '変数の関係として登録された見方です。仮説・未検証・許可用途を、それぞれの状態として表示します。',
577
+ }));
578
+ const searchWrap = makeElement(doc, 'div', { className: 'bb-fov-model-search' });
579
+ const label = makeElement(doc, 'label', { text: 'モデルを検索', attrs: { for: 'bb-fov-model-search' } });
580
+ const input = makeElement(doc, 'input', {
581
+ className: 'bb-fov-search-input',
582
+ value: viewState.query,
583
+ attrs: { id: 'bb-fov-model-search', name: 'model-search', type: 'search', autocomplete: 'off', placeholder: '名前、関係、状態、変数を検索' },
584
+ });
585
+ input.addEventListener('input', () => {
586
+ viewState.query = String(input.value ?? '');
587
+ viewState.page = 0;
588
+ renderResults();
589
+ });
590
+ searchWrap.append(label, input);
591
+ section.append(searchWrap);
592
+
593
+ const results = makeElement(doc, 'div', { className: 'bb-fov-model-results' });
594
+ section.append(results);
595
+ const renderResults = () => {
596
+ results.replaceChildren();
597
+ const query = viewState.query.trim().toLocaleLowerCase();
598
+ const filtered = query ? catalog.models.filter((record) => searchTextForModel(record).includes(query)) : catalog.models;
599
+ if (filtered.length === 0) {
600
+ results.append(workspaceNotice(doc, { label: '検索結果', text: query ? '条件に一致する世界モデルはありません。' : 'この対象範囲には登録された世界モデルがありません。' }));
601
+ return;
602
+ }
603
+ const pages = Math.max(1, Math.ceil(filtered.length / FOUNDATION_OVERVIEW_PAGE_SIZE));
604
+ viewState.page = Math.min(Math.max(viewState.page, 0), pages - 1);
605
+ const start = viewState.page * FOUNDATION_OVERVIEW_PAGE_SIZE;
606
+ const pageItems = filtered.slice(start, start + FOUNDATION_OVERVIEW_PAGE_SIZE);
607
+ results.append(workspaceNotice(doc, {
608
+ label: '表示範囲',
609
+ text: `${start + 1}〜${start + pageItems.length}件を表示しています。登録は採用・検証済みを意味しません。`,
610
+ }));
611
+ const variableMap = new Map(catalog.variables.map((record) => [foundationKey(recordDefinition(record)), record]));
612
+ const cards = makeElement(doc, 'div', { className: 'bb-fov-model-grid' });
613
+ for (const record of pageItems) cards.append(modelCard(doc, record, variableMap));
614
+ results.append(cards);
615
+ if (pages > 1) {
616
+ const pager = makeElement(doc, 'nav', { className: 'bb-fov-pager', attrs: { 'aria-label': '世界モデルのページ' } });
617
+ const previous = workspaceButton(doc, {
618
+ text: '前の50件',
619
+ variant: 'quiet',
620
+ disabled: viewState.page === 0,
621
+ onClick: () => { viewState.page -= 1; renderResults(); },
622
+ });
623
+ const next = workspaceButton(doc, {
624
+ text: '次の50件',
625
+ variant: 'quiet',
626
+ disabled: viewState.page >= pages - 1,
627
+ onClick: () => { viewState.page += 1; renderResults(); },
628
+ });
629
+ pager.append(previous, makeElement(doc, 'span', { text: `${viewState.page + 1} / ${pages}` }), next);
630
+ results.append(pager);
631
+ }
632
+ };
633
+ renderResults();
634
+ return section;
635
+ }
636
+
637
+ function renderLoading(doc, state, callbacks) {
638
+ const notice = workspaceNotice(doc, { label: '読み込み', text: '目的・哲学・世界モデルを読み込んでいます。' });
639
+ notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
640
+ return notice;
641
+ }
642
+
643
+ function renderFailure(doc, state, callbacks) {
644
+ const label = state.status === 'scope_mismatch' ? '対象範囲' : state.status === 'invalid' ? '応答形式' : '取得失敗';
645
+ const message = state.error?.message ?? '目的・哲学・世界モデルを確認できません。';
646
+ const notice = workspaceNotice(doc, { label, tone: 'danger', text: message, role: 'alert' });
647
+ notice.append(makeElement(doc, 'p', { text: '確認できない状態を登録なし・0件として表示していません。' }));
648
+ notice.append(workspaceButton(doc, { text: '再試行', variant: 'quiet', onClick: callbacks.load }));
649
+ return notice;
650
+ }
651
+
652
+ export function renderFoundationOverview(root, state, {
653
+ document: explicitDocument,
654
+ scopeId,
655
+ viewState = { query: '', page: 0 },
656
+ callbacks = {},
657
+ showHeader = true,
658
+ } = {}) {
659
+ if (!root) throw new TypeError('root is required');
660
+ const doc = getDocument(explicitDocument);
661
+ root.replaceChildren();
662
+ const surface = makeElement(doc, 'section', {
663
+ className: 'bb-fov',
664
+ attrs: { 'data-contract-version': FOUNDATION_OVERVIEW_CONTRACT_VERSION, 'aria-label': '目的と現状' },
665
+ });
666
+ if (showHeader) {
667
+ surface.append(workspacePageHeader(doc, {
668
+ crumbs: ['Brainbase', '目的と現状'],
669
+ title: '目的と現状',
670
+ lead: '選択したプロジェクトの目的、哲学、世界モデルを、登録状態と未確認の点を分けて読みます。',
671
+ source: `正本: ${scopeId}`,
672
+ }));
673
+ }
674
+ surface.append(workspaceNotice(doc, {
675
+ label: '対象範囲',
676
+ text: scopeId,
677
+ }));
678
+ if (state.status === 'loading') {
679
+ surface.append(renderLoading(doc, state, callbacks));
680
+ root.append(surface);
681
+ return surface;
682
+ }
683
+ if (state.status !== 'ready') {
684
+ surface.append(renderFailure(doc, state, callbacks));
685
+ root.append(surface);
686
+ return surface;
687
+ }
688
+ const catalog = state.catalog;
689
+ surface.append(workspaceNotice(doc, {
690
+ label: '読み方',
691
+ text: '登録は達成・採用・検証済みを意味しません。記録上の現状は保存時点の記述です。',
692
+ }));
693
+ surface.append(workspaceMetrics(doc, [
694
+ { label: '目的', value: catalog.objectives.length, note: '登録された定義' },
695
+ { label: '哲学', value: catalog.philosophies.length, note: '登録された版' },
696
+ { label: '世界モデル', value: catalog.models.length, note: '検索対象' },
697
+ ], { ariaLabel: 'Foundationの登録数' }));
698
+ surface.append(renderObjectives(doc, catalog, viewState, callbacks));
699
+ surface.append(renderPhilosophies(doc, catalog));
700
+ surface.append(renderModels(doc, catalog, viewState, callbacks));
701
+ root.append(surface);
702
+ return surface;
703
+ }
704
+
705
+ /** Mount the read-only overview and ignore responses from a destroyed generation. */
706
+ export function createFoundationOverview({ root, port, scopeId, document: explicitDocument, autoLoad = true, showHeader = true } = {}) {
707
+ if (!root) throw new TypeError('root is required');
708
+ if (!port || typeof port.readCatalog !== 'function') throw new TypeError('port.readCatalog is required');
709
+ const doc = getDocument(explicitDocument);
710
+ const selectedScope = nonEmpty(scopeId);
711
+ if (!selectedScope) throw new TypeError('scopeId is required');
712
+ const viewState = { query: '', page: 0 };
713
+ const state = { status: 'loading', catalog: null, error: null };
714
+ let generation = 0;
715
+ let destroyed = false;
716
+ const controller = {
717
+ get state() { return state; },
718
+ render() {
719
+ if (destroyed) return controller;
720
+ renderFoundationOverview(root, state, {
721
+ document: doc,
722
+ scopeId: selectedScope,
723
+ viewState,
724
+ callbacks: { load: () => controller.load(), render: () => controller.render() },
725
+ showHeader,
726
+ });
727
+ return controller;
728
+ },
729
+ async load() {
730
+ if (destroyed) return state;
731
+ const requestGeneration = ++generation;
732
+ state.status = 'loading';
733
+ state.catalog = null;
734
+ state.error = null;
735
+ controller.render();
736
+ try {
737
+ const response = await port.readCatalog();
738
+ if (destroyed || requestGeneration !== generation) return state;
739
+ state.catalog = normalizeFoundationCatalog(response, selectedScope);
740
+ state.status = 'ready';
741
+ } catch (cause) {
742
+ if (destroyed || requestGeneration !== generation) return state;
743
+ const code = cause?.code === 'scope_mismatch' ? 'scope_mismatch'
744
+ : cause?.code === 'invalid_schema' ? 'invalid'
745
+ : cause?.code === 'authorization_denied' || cause?.code === 'forbidden' ? 'denied' : 'error';
746
+ state.status = code;
747
+ state.error = {
748
+ code,
749
+ message: cause instanceof Error && cause.message ? cause.message : '目的・哲学・世界モデルを確認できません。',
750
+ };
751
+ }
752
+ controller.render();
753
+ return state;
754
+ },
755
+ destroy() {
756
+ destroyed = true;
757
+ generation += 1;
758
+ root.replaceChildren();
759
+ },
760
+ };
761
+ controller.render();
762
+ if (autoLoad) void controller.load();
763
+ return controller;
764
+ }
765
+
766
+ export default createFoundationOverview;