@unson/brainbase-mcp 0.41.7 → 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.
@@ -78,9 +78,13 @@ export interface FoundationObjectiveDetails {
78
78
  readonly evaluator_note?: unknown;
79
79
  readonly current_state?: unknown;
80
80
  }
81
- /** Runtime shape used by the overview contract for Objective records. */
81
+ /** The words a World Model record gives for where it applies. */
82
+ export interface FoundationModelDetails {
83
+ readonly applicability_text?: unknown;
84
+ }
85
+ /** Runtime shape used by the overview contract for Objective and World Model records. */
82
86
  export interface FoundationOverviewObjectiveRecord extends FoundationCatalogRecord {
83
- readonly details?: FoundationObjectiveDetails;
87
+ readonly details?: FoundationObjectiveDetails | FoundationModelDetails;
84
88
  }
85
89
  export interface GraphFoundationReaders {
86
90
  readonly store: Pick<FoundationRevisionStore, 'read' | 'readLatest' | 'list'>;
@@ -525,12 +525,10 @@ function validateFoundationRecord(row, reference, context, selectedProjectCode,
525
525
  definition: cloneFoundationDefinition(history.definition),
526
526
  digest: digestFoundationDefinition(history.definition)
527
527
  };
528
- if (reference.type === 'objective') {
529
- const details = projectObjectiveDetails(history.payload);
530
- if (details !== undefined)
531
- return { ...record, details };
532
- }
533
- return record;
528
+ const details = reference.type === 'objective'
529
+ ? projectDetails(history.payload, OBJECTIVE_DETAIL_KEYS)
530
+ : reference.type === 'model' ? projectDetails(history.payload, MODEL_DETAIL_KEYS) : undefined;
531
+ return details === undefined ? record : { ...record, details };
534
532
  }
535
533
  const OBJECTIVE_DETAIL_KEYS = [
536
534
  'title',
@@ -540,15 +538,18 @@ const OBJECTIVE_DETAIL_KEYS = [
540
538
  'evaluator_note',
541
539
  'current_state'
542
540
  ];
543
- function projectObjectiveDetails(payload) {
541
+ // A model's foundation scope carries ids and a registration time; its own words for where it
542
+ // applies live beside the definition.
543
+ const MODEL_DETAIL_KEYS = ['applicability_text'];
544
+ function projectDetails(payload, keys) {
544
545
  const nestedDetails = isPlainRecord(payload.details) ? payload.details : undefined;
545
546
  const foundation = isPlainRecord(payload.foundation) ? payload.foundation : undefined;
546
547
  // Graphify payloads historically carried these descriptive fields at the
547
548
  // payload root. Keep the two known nested forms compatible while exposing
548
- // only the six fields in the overview contract.
549
+ // only the listed fields in the overview contract.
549
550
  const candidates = [payload, nestedDetails, foundation].filter((candidate) => candidate !== undefined);
550
551
  const projected = {};
551
- for (const key of OBJECTIVE_DETAIL_KEYS) {
552
+ for (const key of keys) {
552
553
  const source = candidates.find((candidate) => Object.prototype.hasOwnProperty.call(candidate, key));
553
554
  if (source !== undefined && source[key] !== undefined)
554
555
  projected[key] = source[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.7",
3
+ "version": "0.41.8",
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",
@@ -483,5 +483,5 @@
483
483
  "engines": {
484
484
  "node": ">=20"
485
485
  },
486
- "gitHead": "218515bb85c16915c1e509f608f83aa06af3a5d1"
486
+ "gitHead": "b061e1b0a7ccb22155c6e22459ad735dc5e7adb1"
487
487
  }
@@ -151,14 +151,6 @@
151
151
  line-height: 1.55;
152
152
  }
153
153
 
154
- .bb-fov-record-id {
155
- display: block;
156
- margin-top: 7px;
157
- color: var(--bb-color-quiet);
158
- font-family: var(--bb-font-mono);
159
- font-size: 10px;
160
- overflow-wrap: anywhere;
161
- }
162
154
 
163
155
  .bb-fov-ref-list {
164
156
  display: grid;
@@ -254,4 +246,5 @@
254
246
  @media (max-width: 760px) { .bb-fov-objective-ledger { overflow-x: auto; } .bb-fov-objective-ledger .bb-ws-ledger-row { min-width: 760px; } }
255
247
  @media (max-width: 480px) { .bb-fov-reading .bb-ws-definition { grid-template-columns: 1fr; gap: 4px; } .bb-fov-reading dd { margin-bottom: 12px; } }
256
248
  .bb-fov-philosophy-card .bb-fov-original { border-top: 0; padding-top: 0; }
249
+ .bb-fov-model-card .bb-fov-original { margin-top: 12px; padding-top: 10px; overflow-wrap: anywhere; }
257
250
  .bb-fov-objective-ledger .bb-ws-ledger-row:focus-visible { outline: 2px solid var(--bb-color-focus); outline-offset: 2px; }
@@ -55,6 +55,16 @@ const PHILOSOPHY_STATUS_LABELS = Object.freeze({
55
55
  archived: '終了',
56
56
  });
57
57
  const OPERATOR_LABELS = Object.freeze({ at_least: '以上', at_most: '以下', equals: '一致' });
58
+ const SOURCE_KIND_LABELS = Object.freeze({
59
+ candidate: '候補からの登録',
60
+ document: '文書',
61
+ observation: '観察',
62
+ decision: '決定',
63
+ import: '取り込み',
64
+ });
65
+ // A record without a name is said to have none; its id stays in the record details (P9).
66
+ const UNNAMED_RECORD = '名前の記録なし';
67
+ const UNNAMED_VARIABLE = '名前の記録がない変数';
58
68
 
59
69
  export class FoundationOverviewError extends Error {
60
70
  constructor(code, message) {
@@ -225,12 +235,52 @@ const SCOPE_LABELS = Object.freeze({
225
235
  company: '組織',
226
236
  });
227
237
 
238
+ function formatValidity(from, until) {
239
+ if (!from && !until) return null;
240
+ if (!from) return `${formatDate(until)} まで`;
241
+ return `${formatDate(from)} から ${until ? `${formatDate(until)} まで` : '(終了日の記録なし)'}`;
242
+ }
243
+
244
+ /**
245
+ * A scope in words: `{ type, id }` names the project or organization, and a validity scope
246
+ * (`subjectIds`, `validFrom`, `validUntil`) becomes its period and the number of subjects. The
247
+ * subject ids themselves belong to the record details.
248
+ */
228
249
  function formatScope(value) {
229
250
  if (!isObject(value)) return formatStructuredValue(value);
230
251
  const type = formatAny(value.type);
231
252
  const id = formatAny(value.id);
232
253
  if (type && id) return `${SCOPE_LABELS[type] ?? type}: ${id}`;
233
- return formatStructuredValue(value);
254
+ const subjects = firstValue(value, 'subjectIds', 'subject_ids');
255
+ const period = formatValidity(firstValue(value, 'validFrom', 'valid_from'), firstValue(value, 'validUntil', 'valid_until'));
256
+ const parts = [
257
+ Array.isArray(subjects) && subjects.length > 0 ? `対象 ${subjects.length}件` : null,
258
+ period ? `適用期間: ${period}` : null,
259
+ ].filter(Boolean);
260
+ if (parts.length > 0) return parts.join(' / ');
261
+ return formatAny(firstValue(value, 'text', 'description', 'statement', 'label', 'name'))
262
+ ?? '読める形の記録がありません(記録の詳細を参照)';
263
+ }
264
+
265
+ /** Where a definition came from, as kinds of source; the source and evidence ids stay in the details. */
266
+ function formatProvenance(provenance) {
267
+ if (!Array.isArray(provenance) || provenance.length === 0) return null;
268
+ return provenance.map((item) => {
269
+ const kind = SOURCE_KIND_LABELS[item?.sourceKind] ?? '種類の記録なし';
270
+ const evidence = Array.isArray(item?.evidenceIds) ? item.evidenceIds.length : 0;
271
+ return evidence > 0 ? `${kind}(証拠${evidence}件)` : kind;
272
+ }).join('、');
273
+ }
274
+
275
+ /** Ids, revisions and digests of one record, for the "記録の詳細" disclosure only. */
276
+ function recordDetailsElement(doc, rows, summary = '記録の詳細') {
277
+ const details = makeElement(doc, 'details', { className: 'bb-fov-original' });
278
+ details.append(makeElement(doc, 'summary', { text: summary }), workspaceDefinition(doc, rows));
279
+ return details;
280
+ }
281
+
282
+ function referenceLabels(refs) {
283
+ return Array.isArray(refs) && refs.length > 0 ? refs.map(referenceLabel).join('、') : null;
234
284
  }
235
285
 
236
286
  function recordDefinition(record) { return isObject(record?.definition) ? record.definition : {}; }
@@ -265,6 +315,15 @@ function statusElement(doc, definition, className = '') {
265
315
  return makeElement(doc, 'p', { className: `bb-fov-status${className ? ` ${className}` : ''}`, text: statusText(definition) });
266
316
  }
267
317
 
318
+ /**
319
+ * The one state a reader needs in front of a model: whether it is held as verified or as a
320
+ * hypothesis. Adoption, validation and authorized uses stay in the record details.
321
+ */
322
+ function certaintyText(definition) {
323
+ const label = EPISTEMIC_LABELS[definition.epistemicState] ?? VALIDATION_LABELS[definition.validationState];
324
+ return `確からしさ: ${label ?? '記録なし'}`;
325
+ }
326
+
268
327
  function detailsFor(record) {
269
328
  return isObject(record?.details) ? record.details : {};
270
329
  }
@@ -310,9 +369,7 @@ function criteriaList(doc, definition, variableMap) {
310
369
  const ref = criterion?.variableRef;
311
370
  const exact = variableMap.get(foundationKey(ref));
312
371
  const exactDefinition = exact ? recordDefinition(exact) : null;
313
- const name = exactDefinition
314
- ? (formatAny(exactDefinition.meaning) ?? referenceLabel(ref))
315
- : referenceLabel(ref);
372
+ const name = (exactDefinition ? formatAny(exactDefinition.meaning) : null) ?? UNNAMED_VARIABLE;
316
373
  const variableDetails = exactDefinition
317
374
  ? [
318
375
  ['定義', exactDefinition.definition],
@@ -328,7 +385,7 @@ function criteriaList(doc, definition, variableMap) {
328
385
  const status = exact ? '参照確認済み' : '参照未確認(基準変数の版が一致しません)';
329
386
  list.append(makeElement(doc, 'li', {
330
387
  className: exact ? '' : 'is-unrecorded',
331
- text: `${name}${variableDetails.length > 0 ? `(${variableDetails.join(' / ')})` : ''}(${referenceLabel(ref)}) ${operator}${target === null ? '' : ` ${target}`} — ${status}`,
388
+ text: `${name}${variableDetails.length > 0 ? `(${variableDetails.join(' / ')})` : ''} ${operator}${target === null ? '' : ` ${target}`} — ${status}`,
332
389
  }));
333
390
  }
334
391
  return list;
@@ -395,7 +452,7 @@ function recordedStateElement(doc, record) {
395
452
  function objectiveCard(doc, record, variableMap) {
396
453
  const definition = recordDefinition(record);
397
454
  const details = detailsFor(record);
398
- const title = formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? definition.id;
455
+ const title = formatAny(firstValue(details, 'title')) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD;
399
456
  const gaps = missingObjectiveFields(record);
400
457
  const current = currentStateFor(record);
401
458
  const evaluationPeriod = formatAny(firstValue(details, 'evaluation_period_note', 'evaluationPeriodNote'))
@@ -446,11 +503,16 @@ function objectiveReading(doc, record, variableMap, recordContexts) {
446
503
  const provenance = Array.isArray(definition.provenance) ? definition.provenance : [];
447
504
  section('出典', workspaceDefinition(doc, [
448
505
  ['記録元', recordContexts?.get(record)?.label ?? formatAny(record.source) ?? '未記録'],
449
- ['根拠', provenance.length ? formatStructuredValue(provenance) : '未記録'],
506
+ ['根拠', formatProvenance(provenance) ?? '未記録'],
450
507
  ]));
508
+ const criteriaRefs = Array.isArray(definition.criteria) ? definition.criteria.map((criterion) => criterion?.variableRef) : [];
451
509
  const original = makeElement(doc, 'details', { className: 'bb-fov-original' });
452
510
  original.append(makeElement(doc, 'summary', { text: '記録の詳細・参照・原文' }), objectiveCard(doc, record, variableMap),
453
- workspaceDefinition(doc, [['目的ID', definition.id], ['版', definition.revision], ['digest', record.digest]]));
511
+ workspaceDefinition(doc, [
512
+ ['目的ID', definition.id], ['版', definition.revision], ['digest', record.digest],
513
+ ['基準変数の参照', referenceLabels(criteriaRefs)],
514
+ ['根拠の記録', provenance.length ? formatStructuredValue(provenance) : null],
515
+ ]));
454
516
  body.append(original); return body;
455
517
  }
456
518
 
@@ -477,12 +539,12 @@ function renderObjectives(doc, catalog, viewState, callbacks, recordContexts) {
477
539
  const opener = doc.activeElement;
478
540
  viewState.dialog?.close();
479
541
  viewState.dialog = workspaceDialog(doc, {
480
- title: formatAny(details.title) ?? formatAny(definition.meaning) ?? definition.id, opener,
542
+ title: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD, opener,
481
543
  content: objectiveReading(doc, record, recordContexts?.get(record)?.variableMap ?? variableMap, recordContexts),
482
544
  });
483
545
  },
484
546
  cells: [
485
- { primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? definition.id },
547
+ { primary: formatAny(details.title) ?? formatAny(definition.meaning) ?? UNNAMED_RECORD },
486
548
  current.text ?? current.status ?? '未記録',
487
549
  { text: current.missing.length ? current.missing.join('・') : missingObjectiveFields(record).join('・') || '記録なし', className: 'bb-fov-missing' },
488
550
  formatDate(current.asOf),
@@ -506,13 +568,13 @@ function philosophyApplicability(payload) {
506
568
  const period = validFrom
507
569
  ? `${formatDate(validFrom)}から${validUntil ? ` ${formatDate(validUntil)}まで` : ''}`
508
570
  : null;
509
- return [scope, period ? `適用期間: ${period}` : null].filter(Boolean).join(' / ') || formatStructuredValue(legacy);
571
+ return [scope, period ? `適用期間: ${period}` : null].filter(Boolean).join(' / ') || formatScope(legacy);
510
572
  }
511
573
 
512
574
  function philosophyCard(doc, record) {
513
575
  const payload = record.payload;
514
576
  const card = makeElement(doc, 'article', { className: 'bb-fov-philosophy-card' });
515
- const title = formatAny(payload.title) ?? record.id;
577
+ const title = formatAny(payload.title) ?? UNNAMED_RECORD;
516
578
  card.append(makeElement(doc, 'h3', { text: title }));
517
579
 
518
580
  if (formatAny(payload.statement)) card.append(makeElement(doc, 'p', { className: 'bb-fov-philosophy-statement', text: formatAny(payload.statement) }));
@@ -569,10 +631,10 @@ function renderReferenceList(doc, refs, variableMap, label) {
569
631
  }
570
632
  for (const ref of refs) {
571
633
  const exact = variableMap.get(foundationKey(ref));
572
- const name = exact ? formatAny(recordDefinition(exact).meaning) ?? referenceLabel(ref) : referenceLabel(ref);
634
+ const name = (exact ? formatAny(recordDefinition(exact).meaning) : null) ?? UNNAMED_VARIABLE;
573
635
  list.append(makeElement(doc, 'li', {
574
636
  className: exact ? '' : 'is-unrecorded',
575
- text: exact ? `${name}(${referenceLabel(ref)})` : `${referenceLabel(ref)} — 未接続(変数の版が一致しません)`,
637
+ text: exact ? name : `${name} — 未接続(変数の版が一致しません)`,
576
638
  }));
577
639
  }
578
640
  return list;
@@ -581,16 +643,32 @@ function renderReferenceList(doc, refs, variableMap, label) {
581
643
  function modelCard(doc, record, variableMap) {
582
644
  const definition = recordDefinition(record);
583
645
  const card = makeElement(doc, 'article', { className: 'bb-fov-model-card' });
584
- card.append(makeElement(doc, 'h3', { text: formatAny(definition.meaning) ?? definition.id }));
585
- card.append(makeElement(doc, 'code', { className: 'bb-fov-record-id', text: `${definition.id}@${definition.revision}` }));
586
- card.append(statusElement(doc, definition));
646
+ // The stored scope is ids and a registration time, not the reader's "where it applies"; the
647
+ // record's own words go in front, and the stored scope waits in the details.
648
+ const applicability = definition.applicability ?? definition.scope;
649
+ const registered = isObject(applicability) ? applicability : {};
650
+ const subjects = firstValue(registered, 'subjectIds', 'subject_ids');
651
+ const applicabilityText = formatAny(firstValue(detailsFor(record), 'applicability_text', 'applicabilityText'))
652
+ ?? (isObject(applicability) ? null : formatAny(applicability));
653
+ card.append(makeElement(doc, 'h3', { text: formatAny(definition.meaning) ?? UNNAMED_RECORD }));
654
+ card.append(makeElement(doc, 'p', { className: 'bb-fov-status', text: certaintyText(definition) }));
587
655
  card.append(workspaceDefinition(doc, [
588
656
  ['関係', formatAny(definition.relationship)],
589
- ['適用範囲', formatAny(definition.applicability ?? definition.scope)],
657
+ ['適用範囲', applicabilityText ?? { text: '範囲の説明は記録されていません', className: 'is-unrecorded' }],
590
658
  ['不確かさ', formatAny(definition.uncertainty)],
591
659
  ['入力変数', renderReferenceList(doc, definition.inputVariableRefs, variableMap, '入力変数')],
592
660
  ['出力変数', renderReferenceList(doc, definition.outputVariableRefs, variableMap, '出力変数')],
593
661
  ]));
662
+ card.append(recordDetailsElement(doc, [
663
+ ['状態の内訳', statusText(definition)],
664
+ ['記録ID', definition.id],
665
+ ['版', definition.revision],
666
+ ['digest', record.digest],
667
+ ['登録上の対象', Array.isArray(subjects) && subjects.length > 0 ? subjects.join('、') : null],
668
+ ['登録上の開始日時', formatValidity(firstValue(registered, 'validFrom', 'valid_from'), firstValue(registered, 'validUntil', 'valid_until'))],
669
+ ['入力変数の参照', referenceLabels(definition.inputVariableRefs)],
670
+ ['出力変数の参照', referenceLabels(definition.outputVariableRefs)],
671
+ ]));
594
672
  return card;
595
673
  }
596
674
 
@@ -712,7 +790,7 @@ export function renderFoundationOverview(root, state, {
712
790
  surface.append(workspacePageHeader(doc, {
713
791
  crumbs: ['Brainbase', '目的と現状'],
714
792
  title: '目的と現状',
715
- lead: '選択したプロジェクトの目的、哲学、世界モデルを、登録状態と未確認の点を分けて読みます。',
793
+ lead: '選択したプロジェクトの目的、哲学、世界モデルを、記録された内容と未確認の点を分けて読みます。',
716
794
  source: `正本: ${scopeId}`,
717
795
  }));
718
796
  }
@@ -734,7 +812,7 @@ export function renderFoundationOverview(root, state, {
734
812
  return workspaceDisclosure(doc, { title, help, content, open: viewState[key] ?? initialOpen, onToggle: open => { viewState[key] = open; } });
735
813
  };
736
814
  surface.append(fold('哲学', '判断の前提となる考え方です。登録された本文と、判断への適用範囲を分けて表示します。', renderPhilosophies(doc, catalog, recordContexts), true));
737
- surface.append(fold('世界モデル', '何をすると結果が変わるか、という仮説。未検証・許可用途を分けて確認します。', renderModels(doc, catalog, viewState, callbacks, recordContexts), false));
815
+ surface.append(fold('世界モデル', '何をすると結果が変わるか、という仮説。検証済みか仮説かを先に示し、ほかの状態や記録の番号は各モデルの「記録の詳細」で確認できます。', renderModels(doc, catalog, viewState, callbacks, recordContexts), false));
738
816
  root.append(surface);
739
817
  renderLoadingState(doc, root, viewState, false, onLoading);
740
818
  return surface;
@@ -76,12 +76,25 @@ export function graphOwnShareSource(option, { fetcher } = {}) {
76
76
  });
77
77
  }
78
78
 
79
- /** What the owner sent: the snapshot ID, where it is kept and when it was sent. */
79
+ /** What the owner sent, in words: when it was sent. The snapshot ID and digest are in ownShareHandoverRows. */
80
80
  export function ownShareHandoverText(handover) {
81
81
  if (!isRecord(handover) || !textOrNull(handover.graphId)) return null;
82
- const where = textOrNull(handover.projectCode);
83
- const digest = textOrNull(handover.digest) ?? '未確認';
84
- return `束 ${handover.graphId}(${where ? `置き場所 ${where}、` : ''}digest ${digest}、${formatDateTime(handover.createdAt)} に送信)`;
82
+ return `${formatDateTime(handover.createdAt)} に手元のOSSから送った束`;
83
+ }
84
+
85
+ /** The snapshot ID, where it is kept and its digest, for the record details only. */
86
+ export function ownShareHandoverRows(handover) {
87
+ if (!isRecord(handover) || !textOrNull(handover.graphId)) return null;
88
+ return [
89
+ ['束ID', handover.graphId],
90
+ ['置き場所', textOrNull(handover.projectCode)],
91
+ ['digest', textOrNull(handover.digest) ?? '未確認'],
92
+ ];
93
+ }
94
+
95
+ function handoverDetails(doc, handover) {
96
+ const rows = ownShareHandoverRows(handover);
97
+ return rows ? recordDetails(doc, rows) : null;
85
98
  }
86
99
 
87
100
  /**
@@ -113,6 +126,8 @@ export function ownShareSection(doc, { label, lead = OWN_LEAD, handover, childre
113
126
  const sent = ownShareHandoverText(handover);
114
127
  if (sent) facts.append(makeElement(doc, 'span', { text: sent }));
115
128
  section.append(facts);
129
+ const sentDetails = handoverDetails(doc, handover);
130
+ if (sentDetails) section.append(sentDetails);
116
131
  for (const child of children) if (child) section.append(child);
117
132
  return section;
118
133
  }
@@ -250,7 +265,7 @@ function personText(person) {
250
265
  }
251
266
 
252
267
  function criterionText(criterion) {
253
- const variable = textOrNull(criterion.variable?.meaning) ?? criterion.variable?.id ?? '変数';
268
+ const variable = textOrNull(criterion.variable?.meaning) ?? '名前の記録がない変数';
254
269
  if (criterion.target === null || criterion.target === undefined) return `${variable}(目標値なし)`;
255
270
  const value = `${String(criterion.target)}${textOrNull(criterion.variable?.unit) ? ` ${criterion.variable.unit}` : ''}`;
256
271
  if (criterion.operator === 'at_least') return `${variable}: ${value}以上`;
@@ -295,7 +310,7 @@ export function createOwnObjectivesView({
295
310
  function detail(objective) {
296
311
  const wrap = makeElement(doc, 'div', { className: 'bb-graph-own-detail', attrs: { 'aria-label': '選んだ目的(引き継いだ自分の分)' } });
297
312
  wrap.append(
298
- workspaceRailHead(doc, { kicker: '目的(引き継いだ自分の分)', title: objective.meaning, sub: objective.id }),
313
+ workspaceRailHead(doc, { kicker: '目的(引き継いだ自分の分)', title: objective.meaning }),
299
314
  closeButton(doc, () => controller.select(null)),
300
315
  );
301
316
  const criteria = makeElement(doc, 'ul', { className: 'bb-graph-plain-list' });
@@ -312,7 +327,11 @@ export function createOwnObjectivesView({
312
327
  ['状態', ownLabel(ADOPTION_LABELS, objective.adoptionState) ?? textOrNull(objective.adoptionState)],
313
328
  ['公開範囲(手元での設定)', ownLabel(VISIBILITY_LABELS, objective.visibility) ?? textOrNull(objective.visibility)],
314
329
  ]),
315
- recordDetails(doc, [['目的ID', objective.id], ['版', objective.revision]]),
330
+ recordDetails(doc, [
331
+ ['目的ID', objective.id],
332
+ ['版', objective.revision],
333
+ ['基準変数のID', objective.criteria.map((criterion) => textOrNull(criterion.variable?.id)).filter(Boolean).join('、') || null],
334
+ ]),
316
335
  ],
317
336
  }));
318
337
  return wrap;
@@ -344,7 +363,7 @@ export function createOwnObjectivesView({
344
363
  selected: objective.id === state.selectedId,
345
364
  onSelect: (id) => controller.select(id),
346
365
  cells: [
347
- { primary: objective.meaning, secondary: objective.id },
366
+ { primary: objective.meaning },
348
367
  ownLabel(ADOPTION_LABELS, objective.adoptionState) ?? String(objective.adoptionState ?? ''),
349
368
  periodText(objective.evaluationPeriod),
350
369
  personText(objective.accountable) ?? { text: '未登録', className: 'is-unresolved' },
@@ -365,7 +384,7 @@ export function createOwnObjectivesView({
365
384
  if (compact) {
366
385
  const facts = makeElement(doc, 'p', { className: 'bb-graph-own-facts', text: '読み取りのみ' });
367
386
  const refs = makeElement(doc, 'details');
368
- refs.append(makeElement(doc, 'summary', { text: '出典' }), makeElement(doc, 'p', { text: ownShareHandoverText(handover) ?? (state.list.state === 'not_handed_over' ? 'まだ引き継いでいません。' : '出典は確認できません。') }));
387
+ refs.append(makeElement(doc, 'summary', { text: '出典' }), makeElement(doc, 'p', { text: ownShareHandoverText(handover) ?? (state.list.state === 'not_handed_over' ? 'まだ引き継いでいません。' : '出典は確認できません。') }), handoverDetails(doc, handover));
369
388
  root.replaceChildren(facts, ...body(), refs);
370
389
  }
371
390
  else root.replaceChildren(ownShareSection(doc, { label: source.label, lead: OBJECTIVES_LEAD, handover, children: body(), className: 'bb-graph-own-objectives' }));
@@ -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