@unson/brainbase-mcp 0.41.6 → 0.41.8

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.
@@ -287,6 +287,7 @@
287
287
  -webkit-line-clamp: 2;
288
288
  }
289
289
  .knowledge-outcome .knowledge-row-meta { display: flex; flex-wrap: wrap; gap: .15rem .55rem; min-width: 0; }
290
+ .knowledge-outcome .knowledge-row-project { color: var(--knowledge-ink); font-size: .68rem; font-weight: 600; overflow-wrap: anywhere; }
290
291
  .knowledge-outcome .knowledge-row-type,
291
292
  .knowledge-outcome .knowledge-row-scope,
292
293
  .knowledge-outcome .knowledge-row-state { color: var(--knowledge-muted); font-size: .68rem; white-space: nowrap; }
@@ -451,3 +452,20 @@
451
452
  .knowledge-outcome .knowledge-skeleton-line { animation: none; }
452
453
  .knowledge-outcome .knowledge-button { transition: none; }
453
454
  }
455
+
456
+ .knowledge-outcome .knowledge-project-filter {
457
+ display: grid;
458
+ gap: .25rem;
459
+ min-width: min(16rem, 100%);
460
+ color: var(--knowledge-muted);
461
+ font-size: .72rem;
462
+ }
463
+ .knowledge-outcome .knowledge-list-truncated { margin-top: .25rem; color: var(--knowledge-muted); font-size: .72rem; }
464
+ .knowledge-outcome .knowledge-overview-owner {
465
+ display: grid;
466
+ gap: .5rem;
467
+ margin-top: .75rem;
468
+ border-top: 1px solid var(--knowledge-line);
469
+ padding-top: .75rem;
470
+ }
471
+ .knowledge-outcome .knowledge-overview-project { color: var(--knowledge-muted); font-size: .78rem; }
@@ -380,6 +380,7 @@ export function normalizeKnowledgeItem(item) {
380
380
  return {
381
381
  id: fieldValue(item, 'id', 'knowledge_id', 'canonical_id') ?? null,
382
382
  type: fieldValue(item, 'type', 'kind', 'entity_type') ?? null,
383
+ project_code: fieldValue(item, 'project_code', 'projectCode') ?? null,
383
384
  title: fieldValue(item, 'title', 'name', 'subject') ?? null,
384
385
  summary: fieldValue(item, 'summary', 'statement', 'body') ?? null,
385
386
  content: fieldValue(item, 'content', 'canonical_content') ?? null,
@@ -411,12 +412,31 @@ export function normalizeKnowledgeCollection(payload) {
411
412
  records,
412
413
  absence_confirmed: absenceConfirmed,
413
414
  searched_scope: Array.isArray(body.searched_scope) ? body.searched_scope : null,
415
+ truncated: body.truncated === true,
414
416
  query: fieldValue(body, 'query', 'q') ?? null,
415
417
  warnings: Array.isArray(body.warnings) ? body.warnings : null,
416
418
  raw: payload,
417
419
  };
418
420
  }
419
421
 
422
+ const HEADING_MAX_LENGTH = 80;
423
+
424
+ /**
425
+ * The heading shown for a knowledge record. A record without a title uses
426
+ * the first line of its own summary or body instead of a generic label, so
427
+ * the list still tells records apart. Nothing is invented: when none of the
428
+ * three exists the heading says the title is unconfirmed.
429
+ */
430
+ export function knowledgeItemHeading(item) {
431
+ const title = optionalText(item?.title).trim();
432
+ if (title) return title;
433
+ for (const value of [item?.summary, item?.content]) {
434
+ const firstLine = optionalText(value).split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
435
+ if (firstLine) return firstLine.length > HEADING_MAX_LENGTH ? `${firstLine.slice(0, HEADING_MAX_LENGTH)}…` : firstLine;
436
+ }
437
+ return 'タイトル未確認';
438
+ }
439
+
420
440
  function isRecord(value) {
421
441
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
422
442
  }
@@ -755,7 +775,8 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
755
775
  queryLabel.append(query);
756
776
  const scopeFieldLabel = makeElement('label', { text: '範囲' });
757
777
  const scope = makeElement('select', { className: 'knowledge-control', attrs: { name: 'scope' } });
758
- for (const value of ['project', 'organization', 'all']) append(scope, makeElement('option', { text: scopeLabel(value), value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
778
+ const scopeOptionLabel = (value) => optionalText(callbacks.scopeLabels?.[value]).trim() || scopeLabel(value);
779
+ for (const value of ['project', 'organization', 'all']) append(scope, makeElement('option', { text: scopeOptionLabel(value), value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
759
780
  scope.value = filters.scope || 'all';
760
781
  scopeFieldLabel.append(scope);
761
782
  const statusLabel = makeElement('label', { text: '状態' });
@@ -781,6 +802,18 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
781
802
  return form;
782
803
  }
783
804
 
805
+ /**
806
+ * Where a record belongs, for lists that span several projects. Organization
807
+ * wide records say so instead of naming the project row that stores them.
808
+ */
809
+ export function knowledgeItemProjectLabel(item, projectLabel) {
810
+ if (optionalText(item?.scope).trim().toLowerCase() === 'organization') return '組織共通';
811
+ const code = optionalText(item?.project_code).trim();
812
+ if (!code) return 'プロジェクト未確認';
813
+ const label = typeof projectLabel === 'function' ? optionalText(projectLabel(code)).trim() : '';
814
+ return label || code;
815
+ }
816
+
784
817
  export function renderKnowledgeList(root, model = {}, callbacks = {}) {
785
818
  clear(root);
786
819
  const collection = model.records === undefined && (model.items || model.results || model.state) ? normalizeKnowledgeCollection(model) : model;
@@ -792,15 +825,24 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
792
825
  && ['ready', 'results', EMPTY].includes(state);
793
826
  const section = makeElement('section', { className: 'knowledge-list-panel', attrs: { 'aria-labelledby': 'knowledge-list-title' } });
794
827
  const header = makeElement('div', { className: 'knowledge-panel-head' });
795
- append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: countKnown ? `${collection.records.length}件` : UNKNOWN }));
796
- header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認' }));
828
+ const countText = countKnown
829
+ ? `${collection.records.length}件${collection.truncated ? '(新しい順の上限まで)' : ''}`
830
+ : UNKNOWN;
831
+ const scopeText = typeof callbacks.scopeText === 'function'
832
+ ? callbacks.scopeText(collection)
833
+ : searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認';
834
+ append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: countText }));
835
+ header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: scopeText }));
836
+ if (collection.truncated && countKnown) {
837
+ header.firstChild.append(makeElement('p', { className: 'knowledge-list-truncated', text: '表示は新しい順の上限までです。キーワードやプロジェクトで絞ると、ほかの記録も見られます。' }));
838
+ }
797
839
  section.append(header);
798
840
  if (state === 'loading') {
799
841
  const skeleton = makeElement('div', { className: 'knowledge-list-rows', attrs: { 'aria-label': '知識を読み込み中' } });
800
842
  for (let index = 0; index < 4; index += 1) skeleton.append(skeletonRow());
801
843
  section.append(skeleton);
802
844
  } else if (state === EMPTY) {
803
- section.append(makeElement('div', { className: 'knowledge-empty', text: 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
845
+ section.append(makeElement('div', { className: 'knowledge-empty', text: callbacks.emptyText ?? 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
804
846
  } else if (state === 'permission_denied') {
805
847
  // Keep the upstream reason and recovery action visible. Replacing the
806
848
  // payload with a generic detail makes a denied scope look like an
@@ -818,13 +860,14 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
818
860
  const hasSelectedId = callbacks.selectedId !== undefined && callbacks.selectedId !== null;
819
861
  if (!collection.records.length) rows.append(makeElement('div', { className: 'knowledge-empty', text: '検索結果を確認できません。空の一覧とは扱いません。' }));
820
862
  for (const item of collection.records) {
821
- const title = item.title ? text(item.title) : 'タイトル未確認';
863
+ const title = knowledgeItemHeading(item);
822
864
  const row = makeElement('button', { className: 'knowledge-list-row', attrs: { type: 'button', 'aria-pressed': String(item.id === callbacks.selectedId), 'aria-controls': 'knowledge-selected-detail', title } });
823
865
  const main = makeElement('span', { className: 'knowledge-row-main' });
824
866
  const meta = makeElement('span', { className: 'knowledge-row-meta' });
867
+ if (callbacks.showProject) meta.append(makeElement('span', { className: 'knowledge-row-project', text: knowledgeItemProjectLabel(item, callbacks.projectLabel) }));
825
868
  append(meta,
826
869
  makeElement('span', { className: 'knowledge-row-type', text: item.type ? typeLabel(item.type) : UNKNOWN }),
827
- makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
870
+ callbacks.showProject ? null : makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
828
871
  makeElement('span', { className: 'knowledge-row-state', text: item.lifecycle.status !== UNKNOWN ? lifecycleStatusLabel(item.lifecycle.status) : UNKNOWN }),
829
872
  );
830
873
  append(main,
@@ -895,14 +938,14 @@ export function renderKnowledgeDetail(root, item, callbacks = {}) {
895
938
  }
896
939
  append(section,
897
940
  makeElement('span', { className: 'knowledge-eyebrow', text: item.type ? typeLabel(item.type) : UNKNOWN }),
898
- makeElement('h3', { text: item.title ? text(item.title) : 'タイトル未確認', attrs: { id: 'knowledge-detail-title' } }),
941
+ makeElement('h3', { text: knowledgeItemHeading(item), attrs: { id: 'knowledge-detail-title' } }),
899
942
  );
900
943
  const summary = makeElement('p', { className: 'knowledge-long-text', text: item.summary ? text(item.summary) : '要旨未確認' });
901
944
  section.append(summary);
902
945
  renderDefinitionList(section, [
903
946
  ['正本ID', item.id ? text(item.id) : UNKNOWN],
904
947
  ['版', item.version !== null && item.version !== undefined && String(item.version).trim() !== '' ? String(item.version) : UNKNOWN],
905
- ['範囲', item.scope ? scopeLabel(item.scope) : UNKNOWN],
948
+ ['範囲', item.scope ? (optionalText(callbacks.scopeLabels?.[optionalText(item.scope).trim().toLowerCase()]).trim() || scopeLabel(item.scope)) : UNKNOWN],
906
949
  ['責任者', ownerValue(item.owner)],
907
950
  ['有効状態', lifecycleStatusLabel(item.lifecycle.status)],
908
951
  ['適用', item.applicability.state ? applicabilityStateLabel(item.applicability.state) : UNKNOWN],
@@ -1383,6 +1426,212 @@ function defaultCandidateBody(values, context = {}) {
1383
1426
  };
1384
1427
  }
1385
1428
 
1429
+ function projectOptions(projects) {
1430
+ const list = typeof projects === 'function' ? projects() : projects;
1431
+ if (!Array.isArray(list)) return [];
1432
+ const options = [];
1433
+ for (const project of list) {
1434
+ const code = optionalText(fieldValue(project, 'code', 'project_code', 'projectCode') ?? (typeof project === 'string' ? project : '')).trim();
1435
+ if (!code || options.some((option) => option.code === code)) continue;
1436
+ const name = optionalText(fieldValue(project, 'name', 'display_name', 'displayName', 'label')).trim();
1437
+ options.push({ code, name: name || code });
1438
+ }
1439
+ return options;
1440
+ }
1441
+
1442
+ /**
1443
+ * Renders the optional project filter shared by the organization-wide list
1444
+ * and the single project workspace. The empty value means every project the
1445
+ * viewer may read; it never stands for an implicitly chosen project.
1446
+ */
1447
+ export function renderKnowledgeProjectFilter(root, model = {}, callbacks = {}) {
1448
+ clear(root);
1449
+ const label = makeElement('label', { className: 'knowledge-project-filter', text: 'プロジェクト' });
1450
+ const select = makeElement('select', { className: 'knowledge-control', attrs: { name: 'knowledge_project', 'aria-label': 'プロジェクトで絞り込む' } });
1451
+ const selected = optionalText(model.selected).trim();
1452
+ const options = projectOptions(model.projects);
1453
+ append(select, makeElement('option', { text: 'すべてのプロジェクト', value: '', attrs: { value: '', ...(selected ? {} : { selected: 'selected' }) } }));
1454
+ for (const option of options) {
1455
+ append(select, makeElement('option', { text: option.name === option.code ? option.code : `${option.name}(${option.code})`, value: option.code, attrs: { value: option.code, ...(option.code === selected ? { selected: 'selected' } : {}) } }));
1456
+ }
1457
+ if (selected && !options.some((option) => option.code === selected)) {
1458
+ append(select, makeElement('option', { text: selected, value: selected, attrs: { value: selected, selected: 'selected' } }));
1459
+ }
1460
+ select.value = selected;
1461
+ select.addEventListener('change', () => callbacks.onChange?.(optionalText(select.value).trim() || null));
1462
+ label.append(select);
1463
+ root.append(label);
1464
+ return label;
1465
+ }
1466
+
1467
+ /**
1468
+ * Creates the organization-wide knowledge list. It reads every project the
1469
+ * viewer may read plus organization-wide records from one host path, shows
1470
+ * the project each record belongs to, and only reads. Registration and
1471
+ * version changes stay in the single project workspace: choosing a project
1472
+ * calls `onSelectProject`, and the host mounts `createKnowledgeOutcomeController`.
1473
+ *
1474
+ * @param {object} options
1475
+ * @param {string} [options.itemsPath] Host path for the organization-wide list.
1476
+ * @param {Array|Function} [options.projects] Projects offered in the filter.
1477
+ * @param {(code: string|null, context?: object) => void} [options.onSelectProject]
1478
+ */
1479
+ export function createKnowledgeOverviewController(options = {}) {
1480
+ const { root, api, onSelectProject } = options;
1481
+ if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
1482
+ const labels = resolveKnowledgeUiLabels(options.labels);
1483
+ const itemsBase = optionalText(options.itemsPath).trim() || `/api/knowledge/${KNOWLEDGE_ROUTES.items}`;
1484
+ const state = {
1485
+ filters: { q: '', scope: 'all', status: 'active' },
1486
+ list: { state: 'loading', records: null, absence_confirmed: false, searched_scope: null },
1487
+ selected: null,
1488
+ detail: { state: 'idle' },
1489
+ };
1490
+ let selectionGeneration = 0;
1491
+ const projectName = (code) => projectOptions(options.projects).find((option) => option.code === code)?.name ?? null;
1492
+
1493
+ function itemsPath() {
1494
+ const query = new URLSearchParams();
1495
+ if (state.filters.q) query.set('q', state.filters.q);
1496
+ if (state.filters.scope) query.set('scope', state.filters.scope);
1497
+ if (state.filters.status) query.set('status', state.filters.status);
1498
+ const encoded = query.toString();
1499
+ return `${itemsBase}${encoded ? `?${encoded}` : ''}`;
1500
+ }
1501
+
1502
+ function emptyText() {
1503
+ const filtered = Boolean(state.filters.q) || state.filters.scope !== 'all' || state.filters.status !== 'active';
1504
+ return filtered
1505
+ ? 'この条件に一致する知識・判断はありません。'
1506
+ : '見られる範囲に、有効な知識・判断の記録はありません。';
1507
+ }
1508
+
1509
+ function scopeText(collection) {
1510
+ const count = Array.isArray(collection.searched_scope) ? collection.searched_scope.length : null;
1511
+ return count === null
1512
+ ? '読んだ範囲: 未確認'
1513
+ : `読んだ範囲: 見られる${count}件のプロジェクトと組織共通`;
1514
+ }
1515
+
1516
+ function openProject(code, context = {}) {
1517
+ if (!code) return;
1518
+ onSelectProject?.(code, context);
1519
+ }
1520
+
1521
+ function render() {
1522
+ clear(root);
1523
+ const shell = makeElement('div', { className: 'knowledge-outcome knowledge-overview' });
1524
+ const header = makeElement('header', { className: 'knowledge-outcome-head' });
1525
+ const headerMain = makeElement('div', { className: 'knowledge-header-main' });
1526
+ append(headerMain,
1527
+ makeElement('span', { className: 'knowledge-eyebrow', text: 'Brainbase Outcome' }),
1528
+ makeElement('h2', { text: '知識・判断' }),
1529
+ makeElement('p', { text: '見られるすべてのプロジェクトと組織共通の知識・判断を一覧にします。登録や改訂は、プロジェクトを選ぶとできます。' }),
1530
+ );
1531
+ const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
1532
+ renderKnowledgeProjectFilter(headerMeta, { projects: options.projects, selected: null }, { onChange: (code) => openProject(code, { reason: 'filter' }) });
1533
+ append(header, headerMain, headerMeta);
1534
+ shell.append(header);
1535
+ const body = makeElement('div', { className: 'knowledge-outcome-body knowledge-workspace', attrs: { 'data-view': 'discovery' } });
1536
+ const primary = makeElement('section', { className: 'knowledge-outcome-primary knowledge-master', attrs: { 'aria-label': '知識の一覧' } });
1537
+ const discovery = makeElement('section', { className: 'knowledge-discovery-view', attrs: { 'aria-labelledby': 'knowledge-discovery-title' } });
1538
+ const discoveryHeader = makeElement('div', { className: 'knowledge-view-heading' });
1539
+ append(discoveryHeader, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.discovery }));
1540
+ discoveryHeader.firstChild.append(makeElement('h3', { id: 'knowledge-discovery-title', text: '知識・判断を探す' }), makeElement('p', { text: '選んだ記録の本文・出典・所属プロジェクトを確認できます。' }));
1541
+ discovery.append(discoveryHeader);
1542
+ const filters = makeElement('div', { className: 'knowledge-filter-region knowledge-discovery-toolbar' });
1543
+ renderDiscoveryFilters(filters, state.filters, {
1544
+ scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通', all: 'すべて' },
1545
+ onChange: (next) => { state.filters = next; void loadItems(); },
1546
+ });
1547
+ discovery.append(filters);
1548
+ const list = makeElement('div', { className: 'knowledge-discovery-list' });
1549
+ renderKnowledgeList(list, state.list, {
1550
+ labels,
1551
+ showProject: true,
1552
+ projectLabel: projectName,
1553
+ scopeText,
1554
+ emptyText: emptyText(),
1555
+ selectedId: state.selected?.id,
1556
+ onSelect: (item) => { void selectItem(item); },
1557
+ onRetry: () => loadItems(),
1558
+ });
1559
+ discovery.append(list);
1560
+ primary.append(discovery);
1561
+ body.append(primary);
1562
+ const detail = makeElement('aside', { className: 'knowledge-outcome-detail knowledge-inspector', attrs: { 'aria-label': labels.inspector, id: 'knowledge-selected-detail', tabindex: '-1' } });
1563
+ const inspectorHeading = makeElement('div', { className: 'knowledge-inspector-heading' });
1564
+ append(inspectorHeading, makeElement('span', { className: 'knowledge-view-kicker', text: labels.inspector }), makeElement('span', { className: 'knowledge-inspector-state', text: state.selected ? labels.selected : labels.unselected }));
1565
+ detail.append(inspectorHeading);
1566
+ const detailContent = makeElement('div', { className: 'knowledge-inspector-detail' });
1567
+ const hasSelectableKnowledge = normaliseStatus(state.list.state) === 'ready' && Array.isArray(state.list.records) && state.list.records.length > 0;
1568
+ renderKnowledgeDetail(detailContent, state.selected, {
1569
+ labels,
1570
+ detailState: state.detail,
1571
+ scopeLabels: { project: 'プロジェクトの記録', organization: '組織共通' },
1572
+ showSelectionGuide: Boolean(state.selected) || hasSelectableKnowledge,
1573
+ onRetry: state.selected ? () => void selectItem(state.selected) : null,
1574
+ });
1575
+ detail.append(detailContent);
1576
+ if (state.selected) {
1577
+ const owner = makeElement('div', { className: 'knowledge-overview-owner' });
1578
+ const code = optionalText(state.selected.project_code).trim();
1579
+ append(owner, makeElement('p', { className: 'knowledge-overview-project', text: `所属: ${knowledgeItemProjectLabel(state.selected, projectName)}` }));
1580
+ if (code) {
1581
+ const open = makeElement('button', { className: 'knowledge-button secondary', text: 'このプロジェクトで開く', attrs: { type: 'button' } });
1582
+ open.addEventListener('click', () => openProject(code, { reason: 'record', item_id: state.selected.id }));
1583
+ owner.append(open);
1584
+ }
1585
+ detail.append(owner);
1586
+ }
1587
+ body.append(detail); shell.append(body); root.append(shell);
1588
+ options.onStateChange?.(state);
1589
+ return shell;
1590
+ }
1591
+
1592
+ async function loadItems() {
1593
+ selectionGeneration += 1;
1594
+ state.selected = null;
1595
+ state.detail = { state: 'idle' };
1596
+ state.list = { ...state.list, state: 'loading' }; render();
1597
+ try { state.list = normalizeKnowledgeCollection(await callKnowledgeApi(api, itemsPath())); }
1598
+ catch (error) { state.list = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', records: null, message: apiErrorText(error), error: error.code }; }
1599
+ render(); return state.list;
1600
+ }
1601
+
1602
+ async function selectItem(item) {
1603
+ const requestGeneration = ++selectionGeneration;
1604
+ state.selected = item ?? null;
1605
+ const code = optionalText(item?.project_code).trim();
1606
+ if (!item?.id || !code) {
1607
+ // A record whose project is unknown cannot be read through a project
1608
+ // route. Show what the list returned and say the canonical read is
1609
+ // unavailable instead of presenting it as the confirmed detail.
1610
+ state.detail = item ? { state: 'unknown', message: '所属プロジェクトを確認できないため、正本の詳細を読めません。' } : { state: 'idle' };
1611
+ render(); return null;
1612
+ }
1613
+ state.detail = { state: 'loading' }; render();
1614
+ try {
1615
+ const path = typeof options.detailPath === 'function'
1616
+ ? options.detailPath(code, item.id)
1617
+ : knowledgePath(code, `${KNOWLEDGE_ROUTES.items}/${encodeURIComponent(item.id)}`);
1618
+ const payload = await callKnowledgeApi(api, path);
1619
+ if (requestGeneration !== selectionGeneration) return null;
1620
+ const read = normalizeKnowledgeItem(payload?.item ?? payload?.record ?? payload?.data ?? payload);
1621
+ const detailed = read ? { ...read, project_code: read.project_code ?? code } : item;
1622
+ state.selected = detailed; state.detail = { state: 'ready', item: detailed }; render(); return detailed;
1623
+ } catch (error) {
1624
+ if (requestGeneration !== selectionGeneration) return null;
1625
+ state.detail = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; render(); return null;
1626
+ }
1627
+ }
1628
+
1629
+ const controller = { state, render, loadItems, selectItem, paths: { items: itemsPath } };
1630
+ render();
1631
+ if (options.autoLoad !== false) void loadItems();
1632
+ return controller;
1633
+ }
1634
+
1386
1635
  /**
1387
1636
  * Creates the knowledge outcome workspace.
1388
1637
  *
@@ -1452,7 +1701,14 @@ export function createKnowledgeOutcomeController(options = {}) {
1452
1701
  makeElement('p', { text: '正本を探し、由来と状態を確認して、必要なときだけ更新します。' }),
1453
1702
  );
1454
1703
  const headerMeta = makeElement('div', { className: 'knowledge-header-meta' });
1455
- headerMeta.append(makeElement('span', { className: 'knowledge-project-context', text: `プロジェクト: ${code}` }));
1704
+ if (options.projectFilter) {
1705
+ // The host decides what choosing another project, or every project,
1706
+ // means. Showing the filter keeps the chosen project explicit instead
1707
+ // of a fixed label that cannot be changed from here.
1708
+ renderKnowledgeProjectFilter(headerMeta, { projects: options.projectFilter.projects, selected: code }, { onChange: (next) => options.projectFilter.onChange?.(next) });
1709
+ } else {
1710
+ headerMeta.append(makeElement('span', { className: 'knowledge-project-context', text: `プロジェクト: ${code}` }));
1711
+ }
1456
1712
  append(header, headerMain, headerMeta);
1457
1713
  const nav = makeElement('nav', { className: 'knowledge-outcome-nav', attrs: { 'aria-label': '知識の操作' } });
1458
1714
  for (const [value, label] of [['discovery', '探す'], ['capture', '登録案を作る'], ['preview', '公開前テスト'], ['destination', '保存先']]) {
@@ -108,10 +108,20 @@ function linkLine(doc, link, label, extra) {
108
108
  return li;
109
109
  }
110
110
 
111
+ /** The record fields a gap is read from, in the words of the task screen. */
112
+ const SOURCE_FIELD_TEXT = Object.freeze({
113
+ assignee_person_id: '担当欄',
114
+ description: '本文',
115
+ source_refs: '出典リンク',
116
+ review_at: '見直し予定',
117
+ updated_at: '最終更新',
118
+ relation: '関係',
119
+ });
120
+
111
121
  function sourceText(source, readAt) {
112
122
  if (!source) return null;
113
123
  const system = source.system === 'task_api' ? 'タスクの記録' : '組織のGraph';
114
- const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.join('・')}` : '';
124
+ const fields = source.fields?.length && !source.fields.includes('*') ? `の ${source.fields.map((field) => SOURCE_FIELD_TEXT[field] ?? '記録の欄').join('・')}` : '';
115
125
  const at = readAt ? `(${shortTime(readAt)}に読み込み)` : '';
116
126
  return `${system}${fields}${at}`;
117
127
  }
@@ -332,7 +342,10 @@ export function workSiteBlocks(doc, { business, site, readAt, taskHref }) {
332
342
  ? [el(doc, 'a', { className: 'bb-world-rail-link', text: '「タスク」でこの仕事を開く', attrs: { href } }), { text: '担当・期限・状態はタスク画面で直せます。保存先はタスクの正本で、世界にはもう一度読むと反映されます。本文の人物を担当欄へ接続するのも、そこで行います。', className: 'bb-world-rail-note' }]
333
343
  : { text: 'この画面からは直せません。タスクを管理している画面で直してください。', className: 'bb-world-rail-note' },
334
344
  }));
335
- blocks.push(workspaceRailBlock(doc, { title: '出典', content: workspaceDefinition(doc, [['記録', `${site.task_id}(版 ${site.source.version ?? '不明'})`], ['事業', business.name]]) }));
345
+ // The task's id and version are for tracing, so they wait behind the record details.
346
+ const record = el(doc, 'details', { className: 'bb-world-original' });
347
+ record.append(el(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['タスクID', site.task_id], ['版', site.source.version ?? '不明']]));
348
+ blocks.push(workspaceRailBlock(doc, { title: '出典', content: [workspaceDefinition(doc, [['記録', 'タスクの記録'], ['事業', business.name]]), record] }));
336
349
  return blocks;
337
350
  }
338
351