@unson/brainbase-mcp 0.23.7 → 0.23.9

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.
@@ -347,7 +347,7 @@ export function createWorldExtension(options = {}) {
347
347
  return {
348
348
  id: WORLD_EXTENSION_ID,
349
349
  uiDir: fileURLToPath(new URL('../ui/world/', import.meta.url)),
350
- uiFiles: ['world-view.js', 'world-view.css', 'world-vendor.js', 'world-placement.js', 'world-work-rail.js', 'world-district.js', 'world-scenery.js'],
350
+ uiFiles: ['world-view.js', 'world-view.css', 'world-canvas-ui.js', 'world-canvas-ui.css', 'world-canvas-notes.js', 'world-vendor.js', 'world-placement.js', 'world-work-rail.js', 'world-district.js', 'world-scenery.js'],
351
351
  screenEntry: 'world-view.js',
352
352
  // P17: the world sits above the home and opens first.
353
353
  navPosition: 'first',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.23.7",
3
+ "version": "0.23.9",
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",
@@ -423,5 +423,5 @@
423
423
  "engines": {
424
424
  "node": ">=20"
425
425
  },
426
- "gitHead": "5dd3fb22d8a4b559ccb0e0ee14a6e9cdac6a4b4a"
426
+ "gitHead": "ffddfb19cbde2520ba00f8370a251de08cd92ab5"
427
427
  }
@@ -154,6 +154,7 @@
154
154
  .knowledge-outcome .knowledge-outcome-primary,
155
155
  .knowledge-outcome .knowledge-outcome-detail {
156
156
  display: grid;
157
+ align-content: start;
157
158
  gap: 0;
158
159
  min-width: 0;
159
160
  padding: 1.15rem 0 0;
@@ -169,7 +170,7 @@
169
170
  }
170
171
  .knowledge-outcome .knowledge-discovery-view,
171
172
  .knowledge-outcome .knowledge-focused-view { min-width: 0; }
172
- .knowledge-outcome .knowledge-discovery-view { display: grid; gap: 0; }
173
+ .knowledge-outcome .knowledge-discovery-view { display: grid; align-content: start; gap: 0; }
173
174
  .knowledge-outcome .knowledge-discovery-toolbar { margin-top: 1rem; }
174
175
  .knowledge-outcome .knowledge-discovery-list { min-width: 0; }
175
176
 
@@ -249,7 +250,7 @@
249
250
  .knowledge-outcome .knowledge-list-rows { display: grid; gap: 0; margin-top: 0; }
250
251
  .knowledge-outcome .knowledge-list-row {
251
252
  display: grid;
252
- grid-template-columns: minmax(0, 1fr) auto auto auto;
253
+ grid-template-columns: minmax(0, 1fr) auto;
253
254
  gap: .45rem;
254
255
  align-items: center;
255
256
  width: 100%;
@@ -265,13 +266,45 @@
265
266
  }
266
267
  .knowledge-outcome .knowledge-list-row:hover,
267
268
  .knowledge-outcome .knowledge-list-row[aria-pressed="true"] { box-shadow: inset 3px 0 var(--knowledge-green); background: var(--knowledge-green-soft); }
268
- .knowledge-outcome .knowledge-row-title { min-width: 0; overflow: hidden; font-size: .8rem; font-weight: 720; text-overflow: ellipsis; white-space: nowrap; }
269
+ .knowledge-outcome .knowledge-list-row:focus-visible { box-shadow: inset 3px 0 var(--knowledge-green), 0 0 0 2px color-mix(in srgb, var(--knowledge-green) 24%, transparent); }
270
+ .knowledge-outcome .knowledge-row-main { display: grid; min-width: 0; gap: .2rem; }
271
+ .knowledge-outcome .knowledge-row-title {
272
+ display: -webkit-box;
273
+ min-width: 0;
274
+ overflow: hidden;
275
+ font-size: .8rem;
276
+ font-weight: 720;
277
+ text-overflow: ellipsis;
278
+ white-space: normal;
279
+ overflow-wrap: anywhere;
280
+ line-clamp: 2;
281
+ -webkit-box-orient: vertical;
282
+ -webkit-line-clamp: 2;
283
+ }
284
+ .knowledge-outcome .knowledge-row-meta { display: flex; flex-wrap: wrap; gap: .15rem .55rem; min-width: 0; }
269
285
  .knowledge-outcome .knowledge-row-type,
270
286
  .knowledge-outcome .knowledge-row-scope,
271
287
  .knowledge-outcome .knowledge-row-state { color: var(--knowledge-muted); font-size: .68rem; white-space: nowrap; }
288
+ .knowledge-outcome .knowledge-row-chevron { align-self: center; color: var(--knowledge-quiet); font-size: 1.2rem; font-weight: 500; line-height: 1; }
289
+ .knowledge-outcome .knowledge-list-row:hover .knowledge-row-chevron,
290
+ .knowledge-outcome .knowledge-list-row:focus-visible .knowledge-row-chevron,
291
+ .knowledge-outcome .knowledge-list-row[aria-pressed="true"] .knowledge-row-chevron { color: var(--knowledge-green); }
292
+ .knowledge-outcome .knowledge-detail-jump {
293
+ display: none;
294
+ margin-top: .65rem;
295
+ color: var(--knowledge-green);
296
+ font-size: .76rem;
297
+ font-weight: 700;
298
+ text-underline-offset: .15em;
299
+ }
300
+ .knowledge-outcome .knowledge-detail-jump:focus-visible { outline: 3px solid color-mix(in srgb, var(--knowledge-green) 28%, transparent); outline-offset: 2px; }
301
+ .knowledge-outcome #knowledge-selected-detail { scroll-margin-top: 5rem; }
272
302
  .knowledge-outcome .knowledge-empty,
273
303
  .knowledge-outcome .knowledge-detail-empty,
274
304
  .knowledge-outcome .knowledge-unknown-copy { color: var(--knowledge-muted); padding: 1.1rem .25rem; font-size: .8rem; }
305
+ .knowledge-outcome .knowledge-selection-guide { padding-top: .25rem; }
306
+ .knowledge-outcome .knowledge-selection-guide h4 { margin-top: 1.1rem; color: var(--knowledge-muted); }
307
+ .knowledge-outcome .knowledge-selection-guide-list { display: grid; gap: .45rem; margin: .55rem 0 0; padding: 0 0 0 1.1rem; color: var(--knowledge-ink); font-size: .78rem; }
275
308
  .knowledge-outcome .knowledge-skeleton-row { display: grid; grid-template-columns: 4rem minmax(0, 1fr) 3rem; gap: .6rem; padding: .8rem .35rem; }
276
309
  .knowledge-outcome .knowledge-skeleton-line { display: block; height: .72rem; border-radius: 999px; background: linear-gradient(90deg, var(--knowledge-canvas), #fff, var(--knowledge-canvas)); background-size: 220% 100%; animation: knowledge-shimmer 1.4s linear infinite; }
277
310
  .knowledge-outcome .knowledge-skeleton-line.short { width: 4rem; }
@@ -290,7 +323,7 @@
290
323
  border-radius: 0;
291
324
  background: transparent;
292
325
  }
293
- .knowledge-outcome .knowledge-detail-panel > h3 { margin-top: .15rem; }
326
+ .knowledge-outcome .knowledge-detail-panel > h3 { margin-top: .15rem; overflow-wrap: anywhere; }
294
327
  .knowledge-outcome .knowledge-detail-panel > .knowledge-eyebrow { margin-top: .15rem; }
295
328
  .knowledge-outcome .knowledge-definition { display: grid; grid-template-columns: minmax(5rem, .48fr) minmax(0, 1fr); gap: .3rem .7rem; margin: .95rem 0; font-size: .76rem; }
296
329
  .knowledge-outcome .knowledge-definition dt { color: var(--knowledge-muted); }
@@ -379,6 +412,7 @@
379
412
  .knowledge-outcome .knowledge-outcome-body { grid-template-columns: minmax(0, 1fr); }
380
413
  .knowledge-outcome .knowledge-outcome-primary { padding-right: 0; }
381
414
  .knowledge-outcome .knowledge-outcome-detail { margin-top: 1.4rem; padding: 1.15rem 0 0; border-top: 1px solid var(--knowledge-line); border-left: 0; }
415
+ .knowledge-outcome .knowledge-detail-jump { display: inline-flex; align-items: center; min-height: 2.75rem; }
382
416
  }
383
417
 
384
418
  @media (max-width: 700px) {
@@ -387,10 +421,6 @@
387
421
  .knowledge-outcome .knowledge-filters,
388
422
  .knowledge-outcome .knowledge-comparison { grid-template-columns: minmax(0, 1fr); }
389
423
  .knowledge-outcome .knowledge-filter-actions { grid-column: auto; }
390
- .knowledge-outcome .knowledge-list-row { grid-template-columns: minmax(0, 1fr) auto; }
391
- .knowledge-outcome .knowledge-row-type,
392
- .knowledge-outcome .knowledge-row-scope { grid-column: auto; }
393
- .knowledge-outcome .knowledge-row-state { grid-column: 2; grid-row: 1; }
394
424
  .knowledge-outcome .knowledge-preview-result { grid-template-columns: minmax(0, 1fr) auto; }
395
425
  .knowledge-outcome .knowledge-preview-result > :last-child { grid-column: 1 / -1; }
396
426
  }
@@ -66,12 +66,67 @@ const STATE_LABELS = Object.freeze({
66
66
  const TYPE_LABELS = Object.freeze({
67
67
  fact: '事実',
68
68
  decision: '判断',
69
+ judgment: '判断',
70
+ knowledge: '知識',
69
71
  principle: '原則',
70
72
  procedure: '手順',
71
73
  term: '用語',
72
74
  policy: '方針',
73
75
  });
74
76
 
77
+ const SCOPE_LABELS = Object.freeze({
78
+ project: 'このプロジェクト',
79
+ organization: '組織共通',
80
+ org: '組織共通',
81
+ all: 'すべて',
82
+ personal: '個人',
83
+ });
84
+
85
+ const LIFECYCLE_STATUS_LABELS = Object.freeze({
86
+ active: '有効',
87
+ inactive: '無効',
88
+ retired: '失効',
89
+ superseded: '置換済み',
90
+ draft: '下書き',
91
+ candidate: '未確定候補',
92
+ });
93
+
94
+ const SOURCE_KIND_LABELS = Object.freeze({
95
+ team_document: 'チーム文書',
96
+ document: '文書',
97
+ doc: '文書',
98
+ meeting: '会議',
99
+ repository: 'リポジトリ',
100
+ url: 'Webページ',
101
+ fixture: '検証用データ',
102
+ });
103
+
104
+ const APPLICABILITY_STATE_LABELS = Object.freeze({
105
+ unknown: '未確認',
106
+ conditional: '条件付き',
107
+ always: '常時適用',
108
+ unconditional: '常時適用',
109
+ applicable: '適用',
110
+ not_applicable: '対象外',
111
+ });
112
+
113
+ /**
114
+ * Generic labels for the knowledge workspace. Hosts may override these
115
+ * labels, but the shared UI remains usable without organization-specific
116
+ * wording.
117
+ */
118
+ export const KNOWLEDGE_UI_LABELS = Object.freeze({
119
+ discovery: '探す',
120
+ capture: '登録案',
121
+ preview: '公開前テスト',
122
+ source: '保存先',
123
+ inspector: '選択中の知識',
124
+ selected: '選択中',
125
+ unselected: '未選択',
126
+ emptyInspectorTitle: '一覧から知識・判断を選択',
127
+ emptyInspectorDescription: '本文・出典・使える範囲・これまでの変更を確認できます。',
128
+ });
129
+
75
130
  function getDocument() {
76
131
  if (typeof document === 'undefined' || !document?.createElement) throw new Error('document_unavailable');
77
132
  return document;
@@ -102,6 +157,11 @@ function clear(parent) {
102
157
  return parent;
103
158
  }
104
159
 
160
+ function focusKnowledgeDetail() {
161
+ const detail = globalThis.document?.getElementById?.('knowledge-selected-detail');
162
+ detail?.focus?.({ preventScroll: true });
163
+ }
164
+
105
165
  function text(value, fallback = UNKNOWN) {
106
166
  if (value === null || value === undefined) return fallback;
107
167
  if (typeof value === 'string') return value.trim() || fallback;
@@ -114,6 +174,14 @@ function optionalText(value) {
114
174
  return typeof value === 'string' ? value : String(value);
115
175
  }
116
176
 
177
+ function resolveKnowledgeUiLabels(value) {
178
+ const source = value && typeof value === 'object' ? value : {};
179
+ return Object.fromEntries(Object.entries(KNOWLEDGE_UI_LABELS).map(([key, fallback]) => {
180
+ const candidate = optionalText(source[key]).trim();
181
+ return [key, candidate || fallback];
182
+ }));
183
+ }
184
+
117
185
  function asArray(value) {
118
186
  return Array.isArray(value) ? value : null;
119
187
  }
@@ -123,6 +191,26 @@ function typeLabel(value) {
123
191
  return TYPE_LABELS[normalized] ?? text(value);
124
192
  }
125
193
 
194
+ function scopeLabel(value) {
195
+ const normalized = optionalText(value).trim().toLowerCase();
196
+ return SCOPE_LABELS[normalized] ?? text(value);
197
+ }
198
+
199
+ function lifecycleStatusLabel(value) {
200
+ const normalized = optionalText(value).trim().toLowerCase();
201
+ return LIFECYCLE_STATUS_LABELS[normalized] ?? text(value);
202
+ }
203
+
204
+ function sourceKindLabel(value) {
205
+ const normalized = optionalText(value).trim().toLowerCase();
206
+ return SOURCE_KIND_LABELS[normalized] ?? text(value);
207
+ }
208
+
209
+ function applicabilityStateLabel(value) {
210
+ const normalized = optionalText(value).trim().toLowerCase();
211
+ return APPLICABILITY_STATE_LABELS[normalized] ?? text(value);
212
+ }
213
+
126
214
  function stateLabel(value) {
127
215
  return STATE_LABELS[value] ?? (value ? text(value) : UNKNOWN);
128
216
  }
@@ -472,7 +560,7 @@ function renderSource(parent, source) {
472
560
  ? makeElement('a', { className: 'knowledge-source-link', text: source.pointer, attrs: { href: pointer, target: '_blank', rel: 'noopener noreferrer' } })
473
561
  : makeElement('span', { className: 'knowledge-source-link', text: source?.pointer || UNKNOWN });
474
562
  append(section, renderDefinitionList(makeElement('div'), [
475
- ['種別', text(source?.kind)],
563
+ ['種別', sourceKindLabel(source?.kind)],
476
564
  ['本文', source?.state === 'fetched' ? '取得済み' : source?.state === 'pointer_only' ? '参照先のみ(本文未取得)' : '本文取得状態未確認'],
477
565
  ]));
478
566
  const pointerRow = makeElement('p', { className: 'knowledge-source-pointer' });
@@ -494,11 +582,11 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
494
582
  const queryLabel = makeElement('label', { text: 'キーワード' });
495
583
  const query = makeElement('input', { className: 'knowledge-control', value: filters.q ?? filters.query ?? '', attrs: { type: 'search', name: 'q', maxlength: '200', placeholder: 'タイトル・要旨を検索' } });
496
584
  queryLabel.append(query);
497
- const scopeLabel = makeElement('label', { text: '範囲' });
585
+ const scopeFieldLabel = makeElement('label', { text: '範囲' });
498
586
  const scope = makeElement('select', { className: 'knowledge-control', attrs: { name: 'scope' } });
499
- for (const [value, label] of [['project', 'このプロジェクト'], ['organization', '組織から継承'], ['all', '両方']]) append(scope, makeElement('option', { text: label, value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
587
+ for (const value of ['project', 'organization', 'all']) append(scope, makeElement('option', { text: scopeLabel(value), value, attrs: { value, ...(filters.scope === value ? { selected: 'selected' } : {}) } }));
500
588
  scope.value = filters.scope || 'all';
501
- scopeLabel.append(scope);
589
+ scopeFieldLabel.append(scope);
502
590
  const statusLabel = makeElement('label', { text: '状態' });
503
591
  const status = makeElement('select', { className: 'knowledge-control', attrs: { name: 'status' } });
504
592
  for (const [value, label] of [['active', '有効'], ['inactive', '失効を含む'], ['all', 'すべて']]) append(status, makeElement('option', { text: label, value, attrs: { value, ...(filters.status === value ? { selected: 'selected' } : {}) } }));
@@ -506,7 +594,7 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
506
594
  statusLabel.append(status);
507
595
  const submit = makeElement('button', { className: 'knowledge-button primary', text: '探す', attrs: { type: 'submit' } });
508
596
  const reset = makeElement('button', { className: 'knowledge-button secondary', text: '条件をクリア', attrs: { type: 'button' } });
509
- append(form, queryLabel, scopeLabel, statusLabel, makeElement('div', { className: 'knowledge-filter-actions' }));
597
+ append(form, queryLabel, scopeFieldLabel, statusLabel, makeElement('div', { className: 'knowledge-filter-actions' }));
510
598
  form.lastChild.append(submit, reset);
511
599
  form.addEventListener('submit', (event) => {
512
600
  event.preventDefault();
@@ -525,10 +613,13 @@ export function renderDiscoveryFilters(root, filters = {}, callbacks = {}) {
525
613
  export function renderKnowledgeList(root, model = {}, callbacks = {}) {
526
614
  clear(root);
527
615
  const collection = model.records === undefined && (model.items || model.results || model.state) ? normalizeKnowledgeCollection(model) : model;
616
+ const searchedScope = Array.isArray(collection.searched_scope) && collection.searched_scope.length
617
+ ? collection.searched_scope.map(scopeLabel).join(' / ')
618
+ : null;
528
619
  const section = makeElement('section', { className: 'knowledge-list-panel', attrs: { 'aria-labelledby': 'knowledge-list-title' } });
529
620
  const header = makeElement('div', { className: 'knowledge-panel-head' });
530
621
  append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: Array.isArray(collection.records) ? `${collection.records.length}件` : UNKNOWN }));
531
- header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: collection.searched_scope ? `検索範囲: ${collection.searched_scope.join(' / ')}` : '検索範囲: 未確認' }));
622
+ header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認' }));
532
623
  section.append(header);
533
624
  const state = normaliseStatus(collection.state);
534
625
  if (state === 'loading') {
@@ -545,18 +636,37 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
545
636
  section.append(statusNotice({ state: 'unknown', detail: '結果の有無を確認できません。空の一覧とは扱いません。' }, { retry: true, onRetry: callbacks.onRetry }));
546
637
  } else {
547
638
  const rows = makeElement('div', { className: 'knowledge-list-rows' });
639
+ const hasSelectedId = callbacks.selectedId !== undefined && callbacks.selectedId !== null;
548
640
  if (!collection.records.length) rows.append(makeElement('div', { className: 'knowledge-empty', text: '検索結果を確認できません。空の一覧とは扱いません。' }));
549
641
  for (const item of collection.records) {
550
- const row = makeElement('button', { className: 'knowledge-list-row', attrs: { type: 'button', 'aria-pressed': String(item.id === callbacks.selectedId) } });
551
642
  const title = item.title ? text(item.title) : 'タイトル未確認';
552
- append(row,
553
- makeElement('span', { className: 'knowledge-row-title', text: title }),
643
+ const row = makeElement('button', { className: 'knowledge-list-row', attrs: { type: 'button', 'aria-pressed': String(item.id === callbacks.selectedId), 'aria-controls': 'knowledge-selected-detail', title } });
644
+ const main = makeElement('span', { className: 'knowledge-row-main' });
645
+ const meta = makeElement('span', { className: 'knowledge-row-meta' });
646
+ append(meta,
554
647
  makeElement('span', { className: 'knowledge-row-type', text: item.type ? typeLabel(item.type) : UNKNOWN }),
555
- makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? text(item.scope) : UNKNOWN }),
556
- makeElement('span', { className: 'knowledge-row-state', text: item.lifecycle.status !== UNKNOWN ? text(item.lifecycle.status) : UNKNOWN }),
648
+ makeElement('span', { className: 'knowledge-row-scope', text: item.scope ? scopeLabel(item.scope) : UNKNOWN }),
649
+ makeElement('span', { className: 'knowledge-row-state', text: item.lifecycle.status !== UNKNOWN ? lifecycleStatusLabel(item.lifecycle.status) : UNKNOWN }),
650
+ );
651
+ append(main,
652
+ makeElement('span', { className: 'knowledge-row-title', text: title, attrs: { title } }),
653
+ meta,
654
+ );
655
+ append(row,
656
+ main,
657
+ makeElement('span', { className: 'knowledge-row-chevron', text: '›', attrs: { 'aria-hidden': 'true' } }),
557
658
  );
558
659
  row.addEventListener('click', () => callbacks.onSelect?.(item));
559
660
  rows.append(row);
661
+ if (hasSelectedId && item.id === callbacks.selectedId) {
662
+ const jump = makeElement('a', {
663
+ className: 'knowledge-detail-jump',
664
+ text: '選択した知識の詳細へ',
665
+ attrs: { href: '#knowledge-selected-detail', 'aria-controls': 'knowledge-selected-detail' },
666
+ });
667
+ jump.addEventListener('click', () => focusKnowledgeDetail());
668
+ rows.append(jump);
669
+ }
560
670
  }
561
671
  section.append(rows);
562
672
  }
@@ -566,9 +676,41 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
566
676
 
567
677
  export function renderKnowledgeDetail(root, item, callbacks = {}) {
568
678
  clear(root);
569
- const section = makeElement('section', { className: 'knowledge-detail-panel', attrs: { 'aria-labelledby': 'knowledge-detail-title' } });
679
+ const labels = resolveKnowledgeUiLabels(callbacks.labels);
680
+ const showSelectionGuide = callbacks.showSelectionGuide !== false;
681
+ const detailState = callbacks.detailState === undefined
682
+ ? (item ? 'ready' : 'idle')
683
+ : normaliseStatus(callbacks.detailState?.state ?? callbacks.detailState);
684
+ const hasDetailHeading = (detailState === 'ready' && Boolean(item))
685
+ || (!item && showSelectionGuide);
686
+ const section = makeElement('section', {
687
+ className: `knowledge-detail-panel${!item && showSelectionGuide ? ' knowledge-detail-panel-empty' : ''}`,
688
+ attrs: hasDetailHeading ? { 'aria-labelledby': 'knowledge-detail-title' } : { 'aria-label': '知識の詳細' },
689
+ });
690
+ if (detailState === 'loading') {
691
+ section.append(makeElement('p', { className: 'knowledge-inline-status', text: '正本の詳細と履歴を読み込み中です。', attrs: { role: 'status' } }));
692
+ root.append(section);
693
+ return section;
694
+ }
695
+ if (['error', 'error_retryable', 'permission_denied', 'unknown'].includes(detailState)) {
696
+ section.append(statusNotice(callbacks.detailState, { retry: Boolean(callbacks.onRetry), onRetry: callbacks.onRetry }));
697
+ section.append(makeElement('p', { className: 'knowledge-warning-copy', text: '正本の詳細を確認できないため、版の変更や失効は実行できません。' }));
698
+ root.append(section);
699
+ return section;
700
+ }
570
701
  if (!item) {
571
- append(section, makeElement('h3', { text: '詳細', attrs: { id: 'knowledge-detail-title' } }), makeElement('p', { className: 'knowledge-detail-empty', text: '一覧から知識を選ぶと、由来・適用範囲・履歴を確認できます。' }));
702
+ if (showSelectionGuide) {
703
+ const guide = makeElement('section', { className: 'knowledge-selection-guide', attrs: { 'aria-labelledby': 'knowledge-selection-guide-title' } });
704
+ append(guide,
705
+ makeElement('h3', { text: labels.emptyInspectorTitle, attrs: { id: 'knowledge-detail-title' } }),
706
+ makeElement('p', { className: 'knowledge-detail-empty', text: labels.emptyInspectorDescription }),
707
+ makeElement('h4', { text: '選択すると確認できること', attrs: { id: 'knowledge-selection-guide-title' } }),
708
+ );
709
+ const guideItems = makeElement('ul', { className: 'knowledge-selection-guide-list' });
710
+ for (const label of ['内容を読む', '出典と使える範囲を確かめる', '変更の履歴を見る']) guideItems.append(makeElement('li', { text: label }));
711
+ guide.append(guideItems);
712
+ section.append(guide);
713
+ }
572
714
  root.append(section);
573
715
  return section;
574
716
  }
@@ -581,10 +723,10 @@ export function renderKnowledgeDetail(root, item, callbacks = {}) {
581
723
  renderDefinitionList(section, [
582
724
  ['正本ID', item.id ? text(item.id) : UNKNOWN],
583
725
  ['版', item.version !== null && item.version !== undefined && String(item.version).trim() !== '' ? String(item.version) : UNKNOWN],
584
- ['範囲', item.scope ? text(item.scope) : UNKNOWN],
726
+ ['範囲', item.scope ? scopeLabel(item.scope) : UNKNOWN],
585
727
  ['責任者', ownerValue(item.owner)],
586
- ['有効状態', item.lifecycle.status],
587
- ['適用', item.applicability.state ? text(item.applicability.state) : UNKNOWN],
728
+ ['有効状態', lifecycleStatusLabel(item.lifecycle.status)],
729
+ ['適用', item.applicability.state ? applicabilityStateLabel(item.applicability.state) : UNKNOWN],
588
730
  ['発効', displayDateTime(item.lifecycle.effective_at)],
589
731
  ['失効', displayDateTime(item.lifecycle.expires_at)],
590
732
  ['更新', displayDateTime(item.updated_at)],
@@ -822,7 +964,7 @@ export function renderLifecycle(root, item, callbacks = {}) {
822
964
  const section = makeElement('section', { className: 'knowledge-lifecycle-panel', attrs: { 'aria-labelledby': 'knowledge-lifecycle-title' } });
823
965
  append(section, makeElement('h3', { text: '版と失効', attrs: { id: 'knowledge-lifecycle-title' } }), makeElement('p', { text: '改訂・置換・失効は履歴を残し、正本のreadback確認後に状態を更新します。' }));
824
966
  if (!item) { section.append(makeElement('p', { text: '対象の判断を選択してください。' })); root.append(section); return section; }
825
- renderDefinitionList(section, [['現在の版', item.version !== null && item.version !== undefined ? String(item.version) : UNKNOWN], ['状態', item.lifecycle?.status ?? UNKNOWN], ['正本ID', item.id ? text(item.id) : UNKNOWN]]);
967
+ renderDefinitionList(section, [['現在の版', item.version !== null && item.version !== undefined ? String(item.version) : UNKNOWN], ['状態', lifecycleStatusLabel(item.lifecycle?.status)], ['正本ID', item.id ? text(item.id) : UNKNOWN]]);
826
968
  const canEdit = callbacks.canEdit !== false && (typeof callbacks.canEdit !== 'function' || callbacks.canEdit(item));
827
969
  const actions = makeElement('div', { className: 'knowledge-lifecycle-actions' });
828
970
  if (canEdit && callbacks.onRevision) {
@@ -1053,11 +1195,15 @@ function defaultCandidateBody(values, context = {}) {
1053
1195
  * project code; only a `true` result shows the registration form. Without
1054
1196
  * it, the form follows `canManageKnowledge(session)`. Revision,
1055
1197
  * supersession, and retirement always follow `canManageKnowledge`.
1198
+ * @param {Partial<typeof KNOWLEDGE_UI_LABELS>} [options.labels]
1199
+ * Optional host labels for the workspace regions. Unspecified labels use
1200
+ * the generic Japanese defaults.
1056
1201
  */
1057
1202
  export function createKnowledgeOutcomeController(options = {}) {
1058
1203
  const { root, api, apiMutation, project, session, onStateChange, onCommitted, onRetired, resolveDecisionDomain, serializeDraftInput, serializeCandidate, serializeRevision, serializeRetire, serializePreview } = options;
1059
1204
  if (!root || typeof root.replaceChildren !== 'function') throw new Error('knowledge_root_unavailable');
1060
1205
  const code = projectCodeOf(project);
1206
+ const labels = resolveKnowledgeUiLabels(options.labels);
1061
1207
  const canEditDestination = () => (typeof options.canEditDestination === 'function'
1062
1208
  ? options.canEditDestination((typeof session === 'function' ? session() : session) ?? null, code ?? null) === true
1063
1209
  : canManageKnowledge(session));
@@ -1082,6 +1228,7 @@ export function createKnowledgeOutcomeController(options = {}) {
1082
1228
  authority: { state: 'loading', domains: null },
1083
1229
  destination: { state: 'unknown', registration: null, draft: {}, message: '保存先APIの契約を確認できていません。' },
1084
1230
  };
1231
+ let selectionGeneration = 0;
1085
1232
 
1086
1233
  function resumeUrl(draftId) {
1087
1234
  const location = options.location ?? globalThis.location;
@@ -1121,20 +1268,20 @@ export function createKnowledgeOutcomeController(options = {}) {
1121
1268
  const discoveryHeader = makeElement('div', { className: 'knowledge-view-heading' });
1122
1269
  append(discoveryHeader,
1123
1270
  makeElement('div', {},),
1124
- makeElement('span', { className: 'knowledge-view-kicker', text: 'Discovery' }),
1271
+ makeElement('span', { className: 'knowledge-view-kicker', text: labels.discovery }),
1125
1272
  );
1126
- discoveryHeader.firstChild.append(makeElement('h3', { id: 'knowledge-discovery-title', text: '知識・判断を探す' }), makeElement('p', { text: '検索結果を選ぶと、右側に正本とライフサイクルを表示します。' }));
1273
+ discoveryHeader.firstChild.append(makeElement('h3', { id: 'knowledge-discovery-title', text: '知識・判断を探す' }), makeElement('p', { text: '選んだ知識の本文・出典・履歴を確認できます。' }));
1127
1274
  discovery.append(discoveryHeader);
1128
1275
  const filters = makeElement('div', { className: 'knowledge-filter-region knowledge-discovery-toolbar' });
1129
1276
  renderDiscoveryFilters(filters, state.filters, { onChange: (next) => { state.filters = next; void loadItems(); }, });
1130
1277
  discovery.append(filters);
1131
1278
  const list = makeElement('div', { className: 'knowledge-discovery-list' });
1132
- renderKnowledgeList(list, state.list, { selectedId: state.selected?.id, onSelect: (item) => { state.selected = item; state.detail = { state: 'ready', item }; state.lifecycle = { state: 'ready', item }; render(); }, onRetry: () => loadItems() });
1279
+ renderKnowledgeList(list, state.list, { labels, selectedId: state.selected?.id, onSelect: (item) => { void selectItem(item); }, onRetry: () => loadItems() });
1133
1280
  discovery.append(list);
1134
1281
  primary.append(discovery);
1135
1282
  const captureWorkspace = makeElement('section', { className: 'knowledge-focused-view knowledge-capture-view', hidden: state.view !== 'capture', attrs: { 'aria-labelledby': 'knowledge-capture-workspace-title' } });
1136
1283
  const captureHeading = makeElement('div', { className: 'knowledge-view-heading' });
1137
- append(captureHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: 'Capture' }));
1284
+ append(captureHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.capture }));
1138
1285
  captureHeading.firstChild.append(makeElement('h3', { id: 'knowledge-capture-workspace-title', text: '登録案を作る' }), makeElement('p', { text: '自然文を候補へ整理し、正本へ保存する前に内容と根拠を確認します。' }));
1139
1286
  captureWorkspace.append(captureHeading);
1140
1287
  const capture = makeElement('div', { className: 'knowledge-capture-form-region' });
@@ -1154,7 +1301,7 @@ export function createKnowledgeOutcomeController(options = {}) {
1154
1301
  primary.append(captureWorkspace);
1155
1302
  const preview = makeElement('section', { className: 'knowledge-focused-view knowledge-preview-view', hidden: state.view !== 'preview', attrs: { 'aria-labelledby': 'knowledge-preview-workspace-title' } });
1156
1303
  const previewHeading = makeElement('div', { className: 'knowledge-view-heading' });
1157
- append(previewHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: 'Preview' }));
1304
+ append(previewHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.preview }));
1158
1305
  previewHeading.firstChild.append(makeElement('h3', { id: 'knowledge-preview-workspace-title', text: '公開前テスト' }), makeElement('p', { text: '隔離した質問で候補の使われ方と根拠を確認します。' }));
1159
1306
  preview.append(previewHeading);
1160
1307
  const previewContent = makeElement('div', { className: 'knowledge-preview-content' });
@@ -1163,7 +1310,7 @@ export function createKnowledgeOutcomeController(options = {}) {
1163
1310
  primary.append(preview);
1164
1311
  const destination = makeElement('section', { className: 'knowledge-focused-view knowledge-destination-view', hidden: state.view !== 'destination', attrs: { 'aria-labelledby': 'knowledge-destination-workspace-title' } });
1165
1312
  const destinationHeading = makeElement('div', { className: 'knowledge-view-heading' });
1166
- append(destinationHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: 'Source' }));
1313
+ append(destinationHeading, makeElement('div', {},), makeElement('span', { className: 'knowledge-view-kicker', text: labels.source }));
1167
1314
  destinationHeading.firstChild.append(makeElement('h3', { id: 'knowledge-destination-workspace-title', text: '知識文書の保存先' }), makeElement('p', { text: '正本ドキュメントの場所を登録し、保存後の状態を再取得して確認します。' }));
1168
1315
  destination.append(destinationHeading);
1169
1316
  const destinationContent = makeElement('div', { className: 'knowledge-destination-content' });
@@ -1175,21 +1322,40 @@ export function createKnowledgeOutcomeController(options = {}) {
1175
1322
  destination.append(destinationContent);
1176
1323
  primary.append(destination);
1177
1324
  body.append(primary);
1178
- const detail = makeElement('aside', { className: 'knowledge-outcome-detail knowledge-inspector', attrs: { 'aria-label': '選択した知識の詳細' } });
1325
+ const detail = makeElement('aside', {
1326
+ className: 'knowledge-outcome-detail knowledge-inspector',
1327
+ attrs: {
1328
+ 'aria-label': labels.inspector,
1329
+ id: 'knowledge-selected-detail',
1330
+ tabindex: '-1',
1331
+ },
1332
+ });
1179
1333
  const inspectorHeading = makeElement('div', { className: 'knowledge-inspector-heading' });
1180
- append(inspectorHeading, makeElement('span', { className: 'knowledge-view-kicker', text: 'Inspector' }), makeElement('span', { className: 'knowledge-inspector-state', text: state.selected ? '選択中' : '未選択' }));
1334
+ append(inspectorHeading, makeElement('span', { className: 'knowledge-view-kicker', text: labels.inspector }), makeElement('span', { className: 'knowledge-inspector-state', text: state.selected ? labels.selected : labels.unselected }));
1181
1335
  detail.append(inspectorHeading);
1182
1336
  const detailContent = makeElement('div', { className: 'knowledge-inspector-detail' });
1183
- renderKnowledgeDetail(detailContent, state.selected, { onPreview: (item) => { state.preview = { state: 'idle', question: '', context: '', item_id: item.id, candidate_version: item.version, item_version: item.version }; state.view = 'preview'; render(); } });
1337
+ const hasSelectableKnowledge = normaliseStatus(state.list.state) === 'ready' && Array.isArray(state.list.records) && state.list.records.length > 0;
1338
+ renderKnowledgeDetail(detailContent, state.selected, {
1339
+ labels,
1340
+ detailState: state.detail,
1341
+ showSelectionGuide: Boolean(state.selected) || hasSelectableKnowledge,
1342
+ onRetry: state.selected ? () => void selectItem(state.selected.id) : null,
1343
+ onPreview: (item) => { state.preview = { state: 'idle', question: '', context: '', item_id: item.id, candidate_version: item.version, item_version: item.version }; state.view = 'preview'; render(); },
1344
+ });
1184
1345
  detail.append(detailContent);
1185
1346
  const lifecycle = makeElement('div', { className: 'knowledge-inspector-lifecycle' });
1186
- renderLifecycle(lifecycle, state.selected, { canEdit: canManageKnowledge(session), ownerCandidates, revisionDraft: state.lifecycle.revisionDraft, supersessionDraft: state.lifecycle.supersessionDraft, supersessionCandidates: state.list.records, onRevision: reviseItem, onSupersede: supersedeItem, onRetire: retireItem }); detail.append(lifecycle);
1347
+ const detailUnavailable = ['loading', 'error', 'error_retryable', 'permission_denied', 'unknown'].includes(state.detail.state);
1348
+ if (state.selected && !detailUnavailable) renderLifecycle(lifecycle, state.selected, { canEdit: canManageKnowledge(session), ownerCandidates, revisionDraft: state.lifecycle.revisionDraft, supersessionDraft: state.lifecycle.supersessionDraft, supersessionCandidates: state.list.records, onRevision: reviseItem, onSupersede: supersedeItem, onRetire: retireItem }); detail.append(lifecycle);
1187
1349
  if (state.commit.state !== 'idle') { const commit = makeElement('div'); renderCommitState(commit, state.commit, { onReadback: state.commit.pending_save ? retryPendingSave : readbackCommit }); detail.append(commit); }
1188
1350
  body.append(detail); shell.append(body); root.append(shell); notify();
1189
1351
  return shell;
1190
1352
  }
1191
1353
 
1192
1354
  async function loadItems() {
1355
+ selectionGeneration += 1;
1356
+ state.selected = null;
1357
+ state.detail = { state: 'idle' };
1358
+ state.lifecycle = { state: 'idle' };
1193
1359
  state.list = { ...state.list, state: 'loading' }; render();
1194
1360
  try { state.list = normalizeKnowledgeCollection(await callKnowledgeApi(api, buildKnowledgeItemsPath(code, state.filters))); }
1195
1361
  catch (error) { state.list = { state: error.code === 'permission_denied' || error.status === 403 ? 'permission_denied' : 'error_retryable', records: null, message: apiErrorText(error), error: error.code }; }
@@ -1251,17 +1417,26 @@ export function createKnowledgeOutcomeController(options = {}) {
1251
1417
  }
1252
1418
 
1253
1419
  async function selectItem(itemOrId) {
1420
+ const requestGeneration = ++selectionGeneration;
1254
1421
  const id = typeof itemOrId === 'object' ? itemOrId?.id : itemOrId;
1255
1422
  const listItem = typeof itemOrId === 'object' ? itemOrId : state.list.records?.find((item) => item.id === id);
1256
- state.selected = listItem ?? null; state.detail = { state: 'loading' }; render();
1257
- if (!id) { state.detail = { state: 'unknown', message: '正本IDを確認できません。' }; render(); return null; }
1423
+ state.selected = listItem ?? null; state.detail = { state: 'loading' }; state.lifecycle = { state: 'idle' }; render();
1424
+ if (!id) {
1425
+ if (requestGeneration !== selectionGeneration) return null;
1426
+ state.detail = { state: 'unknown', message: '正本IDを確認できません。' }; render(); return null;
1427
+ }
1258
1428
  try {
1259
1429
  const payload = await callKnowledgeApi(api, knowledgePath(code, `${KNOWLEDGE_ROUTES.items}/${encodeURIComponent(id)}`));
1430
+ if (requestGeneration !== selectionGeneration) return null;
1260
1431
  const historyPayload = await callKnowledgeApi(api, knowledgePath(code, `${KNOWLEDGE_ROUTES.items}/${encodeURIComponent(id)}/${KNOWLEDGE_ROUTES.history}`));
1432
+ if (requestGeneration !== selectionGeneration) return null;
1261
1433
  const item = normalizeKnowledgeItem(payload?.item ?? payload?.record ?? payload?.data ?? payload) ?? listItem;
1262
1434
  if (item) item.history = Array.isArray(historyPayload?.entries) ? historyPayload.entries : null;
1263
1435
  state.selected = item; state.detail = { state: 'ready', item }; state.lifecycle = { state: 'ready', item }; render(); return item;
1264
- } catch (error) { state.detail = { state: error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; render(); return null; }
1436
+ } catch (error) {
1437
+ if (requestGeneration !== selectionGeneration) return null;
1438
+ state.detail = { state: error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; render(); return null;
1439
+ }
1265
1440
  }
1266
1441
 
1267
1442
  async function createDraft(input) {
@@ -0,0 +1,31 @@
1
+ /** Compact notices are projections of the same source-read states as the full rail.
2
+ * A task's needs_check is a record gap, never an unavailable read or failed job.
3
+ */
4
+ import { readReasonText, shortTime } from './world-work-rail.js';
5
+
6
+ const READ_LABELS = Object.freeze({ tasks: '仕事', persons: '人物', decisions: '方針の決定', relations: '関係' });
7
+ const STATE_TEXT = Object.freeze({ partial: '一部だけ読めた', failed: '読込失敗', forbidden: '権限がなく読めない', not_connected: '未接続', unavailable: '読めない' });
8
+
9
+ export function workCanvasNotes(work) {
10
+ if (!work) return [];
11
+ if (work.status !== 'ok') {
12
+ const state = STATE_TEXT[work.status] ?? '読めない';
13
+ return [{ label: '区画の仕事', tone: 'warning', summary: `仕事:${state}(0件とは確認できません)`, text: `仕事は${state}。${readReasonText(work.reason) ?? '理由不明'}。仕事の件数は未確認です。0件とは確認できません。` }];
14
+ }
15
+ const notes = [];
16
+ for (const [key, label] of Object.entries(READ_LABELS)) {
17
+ const read = work.reads?.[key];
18
+ if (!read || read.state !== 'complete') {
19
+ const state = STATE_TEXT[read?.state] ?? '状態不明';
20
+ notes.push({ label: `区画の${label}`, tone: 'warning', summary: `${label}:${state}`, text: `${label}は${state}。${readReasonText(read?.reason) ?? '理由不明'}。読めていない記録は0件とは限りません。` });
21
+ }
22
+ }
23
+ const summary = work.summary;
24
+ if (summary?.state === 'complete' || summary?.state === 'partial') {
25
+ const needs = summary.needs_check?.length;
26
+ if (needs > 0) notes.push({ label: '記録の確認', tone: 'attention', summary: `記録の要確認 ${needs}件`, text: `読めた未完了の仕事のうち${needs}件に、記録から把握できていないことがあります。仕事が止まっているという意味ではありません。詳細の絞り込みから根拠を確認できます。` });
27
+ }
28
+ const at = work.reads?.tasks?.read_at;
29
+ notes.push({ label: '区画の時点', tone: 'info', text: `仕事の読み込み:${shortTime(at) ?? '時点不明'}。表示は読み込んだ時点の記録です。` });
30
+ return notes;
31
+ }