@unson/brainbase-mcp 0.41.10 → 0.41.13

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.
@@ -14,8 +14,7 @@ import {
14
14
  workspaceDialog,
15
15
  workspaceFilterPopover,
16
16
  workspaceErrorCard,
17
- workspaceLoadingCard,
18
- workspaceDismissNotification,
17
+ workspaceCombobox,
19
18
  workspaceDefinition,
20
19
  workspaceDetailEmpty,
21
20
  workspaceLedger,
@@ -32,18 +31,63 @@ const PERIODS = Object.freeze({ week: 'week', past30days: 'past30days', all: 'al
32
31
  const PERIOD_LABELS = Object.freeze({ week: '今週', past30days: '過去30日', all: '全期間' });
33
32
  const SECTION_KEYS = Object.freeze(['needs_human', 'blocked', 'continued', 'other']);
34
33
  const NORMAL_ENTRYPOINTS = Object.freeze(['codex', 'claude_code', 'mana', 'company_os', 'unknown']);
34
+ // Where the judgment was made. "company_os" is the organization server's own
35
+ // runs, so it is named after what the person sees rather than the module.
35
36
  const NORMAL_ENTRYPOINT_LABELS = Object.freeze({
36
37
  codex: 'Codex',
37
38
  claude_code: 'Claude Code',
38
39
  mana: 'Mana',
39
- company_os: 'Company OS',
40
- unknown: '入口不明',
40
+ company_os: '組織のBrainbase',
41
+ unknown: '記録元が不明',
42
+ });
43
+ // The filter names the organization source a little more fully than a row.
44
+ const NORMAL_ENTRYPOINT_FILTER_LABELS = Object.freeze({
45
+ company_os: '組織のBrainbase(自動の判断)',
41
46
  });
42
47
  const FEEDBACK_KINDS = Object.freeze(['feedback', 'result', 'correction']);
43
48
  const FEEDBACK_KIND_LABELS = Object.freeze({
44
- feedback: 'フィードバック',
45
- result: '結果',
46
- correction: '訂正',
49
+ feedback: 'メモ',
50
+ result: '良かった',
51
+ correction: '直すところ',
52
+ });
53
+ // The one question a person answers about a judgment. Each answer maps to
54
+ // the existing append-only event kinds; only answers that say the result was
55
+ // looked at carry an explicit outcome.
56
+ const FEEDBACK_ANSWERS = Object.freeze([
57
+ { value: 'good', kind: 'result', outcomeStatus: 'confirmed', label: '良かった', placeholder: '気づいたことがあれば書いてください(任意)', fallbackSummary: '良かった' },
58
+ { value: 'fix', kind: 'correction', outcomeStatus: 'confirmed', label: '直すところがある', placeholder: 'どこを、どう直すかを書いてください', fallbackSummary: null },
59
+ { value: 'unsure', kind: 'feedback', outcomeStatus: null, label: 'まだ分からない', placeholder: '分かったら確かめたいことを書いてください(任意)', fallbackSummary: 'まだ結果を確かめていない' },
60
+ ]);
61
+ const REFERENCE_KIND_LABELS = Object.freeze({
62
+ philosophy: '哲学',
63
+ objective: '目的',
64
+ model: '世界モデル',
65
+ problem_snapshot: '問題の記録',
66
+ evidence: '根拠の記録',
67
+ });
68
+ // Graph ids carry their type as a fixed prefix. This is read only when the
69
+ // record left `kind` empty; an unknown prefix stays "その他".
70
+ const REFERENCE_ID_PREFIXES = Object.freeze([
71
+ ['fnd_philosophy_', 'philosophy'],
72
+ ['fnd_unson_philosophy_', 'philosophy'],
73
+ ['phi_', 'philosophy'],
74
+ ['fnd_objective_', 'objective'],
75
+ ['fnd_unson_objective_', 'objective'],
76
+ ['obj_', 'objective'],
77
+ ['fnd_model_', 'model'],
78
+ ['fnd_unson_model_', 'model'],
79
+ ]);
80
+ const MISSING_FIELD_LABELS = Object.freeze({
81
+ entrypoint: '記録元',
82
+ project_code: 'プロジェクト',
83
+ 'judgment.status': '判断の状態',
84
+ 'judgment.summary': '判断の内容',
85
+ 'judgment.reason': '判断の理由',
86
+ 'judgment.selected_references': '使った参照',
87
+ 'judgment.alternatives': '比べた案',
88
+ 'execution.status': '実行したか',
89
+ 'execution.result_summary': '実行の結果',
90
+ 'execution.outcome_status': '結果を確かめたか',
47
91
  });
48
92
 
49
93
  function isRecord(value) {
@@ -121,6 +165,11 @@ function findFirst(node, predicate) {
121
165
  return null;
122
166
  }
123
167
 
168
+ // The page search box; the filter dropdowns carry their own text inputs.
169
+ function isSearchInput(node) {
170
+ return node?.tagName === 'INPUT' && attributeValue(node, 'type') === 'search';
171
+ }
172
+
124
173
  function replaceNode(nextNode, previousNode) {
125
174
  const parent = nextNode?.parentNode;
126
175
  if (!parent || !previousNode) return false;
@@ -594,7 +643,18 @@ function normalRecordSearchText(record) {
594
643
  ].filter(Boolean).join(' ').toLocaleLowerCase('ja-JP');
595
644
  }
596
645
 
597
- function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIODS.past30days, query = '' } = {}) {
646
+ /**
647
+ * A result counts as looked at when the record says so, or when a person
648
+ * appended an answer that explicitly confirmed it. Feedback events are
649
+ * append-only and never rewrite `execution.outcome_status`, so both are read.
650
+ */
651
+ function outcomeConfirmed(record) {
652
+ if (text(record?.execution?.outcome_status).toLowerCase() === 'confirmed') return true;
653
+ const events = Array.isArray(record?.feedbackEvents) ? record.feedbackEvents : [];
654
+ return events.some((event) => text(event?.content?.outcome_status).toLowerCase() === 'confirmed');
655
+ }
656
+
657
+ function aggregateNormalJudgmentHistory(home,{ now = new Date(), period = PERIODS.past30days, query = '' } = {}) {
598
658
  const empty = {
599
659
  mode: 'normal',
600
660
  rows: [],
@@ -645,6 +705,7 @@ function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIO
645
705
  missingFields: record.missingFields,
646
706
  alternatives: record.alternatives,
647
707
  references: record.selectedReferences,
708
+ feedbackEvents: record.feedbackEvents,
648
709
  searchText,
649
710
  });
650
711
  }
@@ -672,7 +733,7 @@ function aggregateNormalJudgmentHistory(home, { now = new Date(), period = PERIO
672
733
  const referenceCount = metricsUnconfirmed || referencesUnconfirmed ? null : references.size;
673
734
  const confirmedOutcomes = metricsUnconfirmed
674
735
  ? null
675
- : rows.filter((row) => text(row.execution?.outcome_status).toLowerCase() === 'confirmed').length;
736
+ : rows.filter(outcomeConfirmed).length;
676
737
  return {
677
738
  ...empty,
678
739
  rows,
@@ -817,82 +878,298 @@ function referenceLabel(ref) {
817
878
  return label;
818
879
  }
819
880
 
820
- function makeReferenceSummary(doc, references) {
821
- if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
822
- if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
823
- const labeled = [];
824
- let unlabeled = 0;
881
+ /** The kind of a reference in words, from its recorded kind, usage or id prefix. */
882
+ function referenceKindLabel(ref) {
883
+ for (const key of [text(ref?.kind).toLowerCase(), text(ref?.usage).toLowerCase(), text(ref?.layer).toLowerCase()]) {
884
+ if (REFERENCE_KIND_LABELS[key]) return REFERENCE_KIND_LABELS[key];
885
+ }
886
+ const id = text(ref?.ref ?? ref?.id);
887
+ const prefix = REFERENCE_ID_PREFIXES.find(([candidate]) => id.startsWith(candidate));
888
+ return prefix ? REFERENCE_KIND_LABELS[prefix[1]] : 'その他の参照';
889
+ }
890
+
891
+ /** "哲学 2・目的 1" for references that only have an id. */
892
+ function referenceKindCounts(references) {
893
+ const counts = new Map();
825
894
  for (const ref of references) {
826
- const label = referenceLabel(ref);
827
- if (label) labeled.push(label);
828
- else unlabeled += 1;
895
+ const kind = referenceKindLabel(ref);
896
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
897
+ }
898
+ return [...counts].map(([kind, count]) => `${kind} ${count}`).join('・');
899
+ }
900
+
901
+ function makeReferenceSummary(doc, references, labels = null) {
902
+ if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は記録されていません' });
903
+ // Personal KG records are summarized by the owner badge only, never by name.
904
+ const shown = references.filter((ref) => judgmentReferenceKind(ref) !== PERSONAL_KNOWLEDGE_KIND);
905
+ if (!shown.length) {
906
+ return makeWorkspaceElement(doc, 'span', {
907
+ className: 'bb-jh-unrecorded',
908
+ text: references.length ? '判断の枠組みは記録されていません' : '参照はありません',
909
+ });
910
+ }
911
+ const labeled = [];
912
+ const unlabeled = [];
913
+ for (const ref of shown) {
914
+ const name = referenceDisplayName(ref, labels);
915
+ if (name.known) labeled.push(name.text);
916
+ else unlabeled.push(ref);
829
917
  }
830
918
  const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-summary' });
831
919
  if (labeled.length) {
832
920
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: labeled.slice(0, 2).join('、') }));
833
921
  if (labeled.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${labeled.length - 2}件` }));
834
922
  }
835
- if (unlabeled > 0) wrap.append(makeWorkspaceElement(doc, 'strong', { text: `名前のない参照 ${unlabeled}件` }));
923
+ if (unlabeled.length) {
924
+ wrap.append(makeWorkspaceElement(doc, labeled.length ? 'small' : 'strong', { text: referenceKindCounts(unlabeled) }));
925
+ }
836
926
  return wrap;
837
927
  }
838
928
 
929
+ // Only a state that asks the person for something is shown in the list.
930
+ // "resolved" and an unrecorded state are kept in the record details.
839
931
  function judgmentStatusLabel(status) {
932
+ const key = text(status);
933
+ if (key === 'needs_clarification' || key === 'needs_classification' || key === 'needs_policy_resolution') return 'あなたの確認待ち';
934
+ return '';
935
+ }
936
+
937
+ function judgmentStatusDetail(status) {
840
938
  const labels = {
841
- resolved: '解決済み',
842
- needs_clarification: '確認が必要',
843
- needs_classification: '分類待ち',
844
- needs_policy_resolution: '方針の確認待ち',
845
- unknown: '状態は未記録',
939
+ resolved: '進め方が決まった',
940
+ needs_clarification: 'あなたの確認待ち',
941
+ needs_classification: 'あなたの確認待ち',
942
+ needs_policy_resolution: 'あなたの確認待ち',
846
943
  };
847
- const key = text(status);
848
- if (!key) return '状態は未記録';
849
- return labels[key] ?? '状態は未記録';
944
+ return labels[text(status)] ?? null;
850
945
  }
851
946
 
852
947
  function makeDecisionSummary(doc, row) {
853
948
  if (row.mode === 'normal') {
854
949
  const summary = text(row.judgment?.summary);
855
950
  const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
856
- wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
951
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断の内容は記録されていません' }));
952
+ wrap.append(makeOwnerJudgmentBadge(doc, row.references));
857
953
  return wrap;
858
954
  }
859
955
  const summary = text(row.proof?.decision?.summary);
860
956
  const wrap = makeWorkspaceElement(doc, 'span', { className: `bb-jh-decision${row.delegated ? ' is-delegated' : ''}` });
861
- wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || (row.proof?.interruption?.resolution === 'continued_without_human' ? '判断内容未記録' : '本人に確認') }));
957
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || (row.proof?.interruption?.resolution === 'continued_without_human' ? '判断の内容は記録されていません' : '本人に確認') }));
862
958
  if (row.delegated) wrap.append(makeWorkspaceElement(doc, 'small', { text: 'Brainbaseが代わりに判断' }));
863
959
  else if (row.proof?.interruption?.resolution !== 'continued_without_human') wrap.append(makeWorkspaceElement(doc, 'small', { text: '本人に戻した判断' }));
864
960
  return wrap;
865
961
  }
866
962
 
963
+ // docs/specs/judgment-history-frame-basis.md: the shared kind/usage values.
964
+ const FRAME_KINDS = Object.freeze(['philosophy', 'objective', 'model']);
965
+ const FRAME_KIND_LABELS = Object.freeze({ philosophy: '哲学', objective: '目的', model: '世界モデル' });
966
+ const FRAME_ROLE_LABELS = Object.freeze({
967
+ constraint: '守る制約として使った',
968
+ criterion: '照らした基準として使った',
969
+ prediction: '置いた予測として使った',
970
+ });
971
+ const FRAME_ROLE_DETAIL_LABELS = Object.freeze({
972
+ constraint: { satisfied: '守れた', violated: '反した', tension: 'ぶつかった' },
973
+ criterion: { satisfied: '満たした', violated: '満たさない', tension: '一部ぶつかる' },
974
+ prediction: { held: 'データで当たった', failed: 'データで外れた', unchecked: 'まだデータで確かめていない' },
975
+ });
976
+ const PERSONAL_KNOWLEDGE_KIND = 'personal_knowledge';
977
+
978
+ /** Kind of a selected reference: the recorded kind, or the Graph id prefix for older records. */
979
+ export function judgmentReferenceKind(reference) {
980
+ const kind = text(reference?.kind);
981
+ if (kind) return kind;
982
+ const ref = text(reference?.ref ?? reference?.id);
983
+ return REFERENCE_ID_PREFIXES.find(([prefix]) => ref.startsWith(prefix))?.[1] ?? null;
984
+ }
985
+
986
+ /** Parse the shared usage value; values from other writers stay unparsed. */
987
+ export function parseJudgmentFrameUsage(usage) {
988
+ const [role, detail = null, ...rest] = text(usage).split(':');
989
+ if (rest.length || !FRAME_ROLE_LABELS[role]) return null;
990
+ if (detail !== null && !FRAME_ROLE_DETAIL_LABELS[role][detail]) return null;
991
+ if (role === 'constraint' && detail === null) return null;
992
+ return { role, detail };
993
+ }
994
+
995
+ /**
996
+ * Group one judgment's references into the frame it used and whether the
997
+ * owner's Personal KG entered it. "Not used" is claimed only for a writer
998
+ * that records usage; anything else stays "unrecorded" (P9).
999
+ */
1000
+ export function describeJudgmentBasis(references) {
1001
+ const groups = { philosophy: [], objective: [], model: [] };
1002
+ const others = [];
1003
+ let personal = 0;
1004
+ let usageRecorded = false;
1005
+ for (const reference of Array.isArray(references) ? references : []) {
1006
+ const kind = judgmentReferenceKind(reference);
1007
+ if (kind === PERSONAL_KNOWLEDGE_KIND) {
1008
+ personal += 1;
1009
+ continue;
1010
+ }
1011
+ const usage = parseJudgmentFrameUsage(reference?.usage);
1012
+ if (FRAME_KINDS.includes(kind)) {
1013
+ if (usage) usageRecorded = true;
1014
+ groups[kind].push({ reference, usage });
1015
+ } else {
1016
+ others.push(reference);
1017
+ }
1018
+ }
1019
+ const owner = personal > 0
1020
+ ? { state: 'applied', count: personal }
1021
+ : usageRecorded ? { state: 'not_applied', count: 0 } : { state: 'unrecorded', count: 0 };
1022
+ return { groups, others, owner, recorded: Array.isArray(references) };
1023
+ }
1024
+
1025
+ const OWNER_JUDGMENT_BADGES = Object.freeze({
1026
+ applied: '本人の考えあり',
1027
+ not_applied: '本人の考えなし',
1028
+ unrecorded: '本人の考え:記録なし',
1029
+ });
1030
+
1031
+ // "recorded" is the normal case and "unknown" means nobody checked; neither
1032
+ // needs the person's attention, so only the two blocking states are shown.
867
1033
  const REFERENCE_AVAILABILITY_LABELS = Object.freeze({
868
- recorded: '今の権限で開けます',
869
1034
  permission_denied: '開く権限がありません',
870
- unavailable: '本文はもうありません',
871
- unknown: '未確認',
1035
+ unavailable: '本文はもう残っていません',
1036
+ });
1037
+ const REFERENCE_AVAILABILITY_DETAIL = Object.freeze({
1038
+ recorded: '開ける',
1039
+ permission_denied: '開く権限がない',
1040
+ unavailable: '本文がない',
1041
+ unknown: '開けるか確かめていない',
872
1042
  });
873
1043
 
874
1044
  function appendReferenceList(doc, references) {
875
1045
  if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
876
1046
  if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
1047
+ const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-references' });
877
1048
  const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
1049
+ const bare = [];
878
1050
  for (const ref of references) {
879
- const item = makeWorkspaceElement(doc, 'li');
880
1051
  const label = referenceLabel(ref);
881
- item.append(makeWorkspaceElement(doc, 'strong', { text: label || '名前のない参照' }));
882
- const recordId = text(ref.ref ?? ref.id);
883
- if (recordId) {
884
- item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-record-id', text: `記録ID:${recordId}(コピー)` }));
1052
+ const application = ref.application && ref.application !== ref.label && ref.application !== label ? ref.application : '';
1053
+ const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)];
1054
+ if (!label && !application && !ref.why && !ref.usage && !availability) {
1055
+ bare.push(ref);
1056
+ continue;
885
1057
  }
886
- if (ref.application && ref.application !== ref.label && ref.application !== label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
887
- const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)] ?? REFERENCE_AVAILABILITY_LABELS.unknown;
888
- item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
889
- if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
890
- if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
891
- const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
1058
+ const item = makeWorkspaceElement(doc, 'li');
1059
+ item.append(makeWorkspaceElement(doc, 'strong', { text: label || referenceKindLabel(ref) }));
1060
+ if (label) item.append(makeWorkspaceElement(doc, 'small', { text: referenceKindLabel(ref) }));
1061
+ if (application) item.append(makeWorkspaceElement(doc, 'span', { text: application }));
1062
+ if (availability) item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
1063
+ if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `選んだ理由: ${ref.why}` }));
1064
+ if (ref.usage && !REFERENCE_KIND_LABELS[text(ref.usage)]) item.append(makeWorkspaceElement(doc, 'span', { text: `使い方: ${ref.usage}` }));
1065
+ const alternateLabels = (ref.labels ?? []).filter((value) => value && value !== ref.label);
892
1066
  if (alternateLabels.length) item.append(makeWorkspaceElement(doc, 'span', { text: `別名: ${alternateLabels.join('、')}` }));
893
1067
  list.append(item);
894
1068
  }
895
- return list;
1069
+ if (list.children?.length) wrap.append(list);
1070
+ if (bare.length) {
1071
+ wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-reference-kinds', text: referenceKindCounts(bare) }));
1072
+ wrap.append(makeWorkspaceElement(doc, 'p', {
1073
+ className: 'bb-jh-reference-note',
1074
+ text: 'この記録には参照の名前が残っていません。どの記録かは「記録の詳細」のIDで確かめられます。',
1075
+ }));
1076
+ }
1077
+ return wrap;
1078
+ }
1079
+
1080
+ /** Name for a frame reference, resolved by the host under the viewer's current permission. */
1081
+ function referenceDisplayName(reference, labels) {
1082
+ const recorded = referenceLabel(reference);
1083
+ if (recorded) return { text: recorded, known: true };
1084
+ const entry = labels?.get?.(text(reference?.ref ?? reference?.id));
1085
+ if (entry?.status === 'found' && text(entry.label)) return { text: text(entry.label), known: true };
1086
+ if (entry?.status === 'loading') return { text: '名前を確認中', known: false };
1087
+ return { text: '名前を確かめられません', known: false };
1088
+ }
1089
+
1090
+ function frameUsageText(usage) {
1091
+ if (!usage) return '使い方の記録なし';
1092
+ const detail = usage.detail ? FRAME_ROLE_DETAIL_LABELS[usage.role][usage.detail] : '';
1093
+ return detail ? `${FRAME_ROLE_LABELS[usage.role]}・${detail}` : FRAME_ROLE_LABELS[usage.role];
1094
+ }
1095
+
1096
+ function referenceWhy(doc, reference) {
1097
+ const why = text(reference?.why);
1098
+ return why ? makeWorkspaceElement(doc, 'span', { className: 'bb-jh-basis-why', text: `選んだ理由:${why}` }) : null;
1099
+ }
1100
+
1101
+ function appendJudgmentBasis(doc, references, labels) {
1102
+ const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-basis' });
1103
+ if (references === null || references === undefined) {
1104
+ wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '判断の枠組み(哲学・目的・世界モデル)の記録はありません。使っていないとは限りません。' }));
1105
+ return wrap;
1106
+ }
1107
+ const basis = describeJudgmentBasis(references);
1108
+ for (const kind of FRAME_KINDS) {
1109
+ const section = makeWorkspaceElement(doc, 'section', { className: `bb-jh-basis-group is-${kind}` });
1110
+ section.append(makeWorkspaceElement(doc, 'h4', { text: FRAME_KIND_LABELS[kind] }));
1111
+ const entries = basis.groups[kind];
1112
+ if (!entries.length) {
1113
+ section.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: 'この判断では記録されていません。' }));
1114
+ } else {
1115
+ const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-basis-list' });
1116
+ for (const { reference, usage } of entries) {
1117
+ const item = makeWorkspaceElement(doc, 'li');
1118
+ const name = referenceDisplayName(reference, labels);
1119
+ item.append(makeWorkspaceElement(doc, 'strong', { className: name.known ? '' : 'bb-jh-unrecorded', text: name.text }));
1120
+ item.append(makeWorkspaceElement(doc, 'span', {
1121
+ className: `bb-jh-basis-usage${usage ? ` is-${usage.role}${usage.detail ? ` is-${usage.detail}` : ''}` : ' is-unrecorded'}`,
1122
+ text: frameUsageText(usage),
1123
+ }));
1124
+ const why = referenceWhy(doc, reference);
1125
+ if (why) item.append(why);
1126
+ list.append(item);
1127
+ }
1128
+ section.append(list);
1129
+ }
1130
+ wrap.append(section);
1131
+ }
1132
+ if (basis.others.length) {
1133
+ const section = makeWorkspaceElement(doc, 'section', { className: 'bb-jh-basis-group is-other' });
1134
+ section.append(makeWorkspaceElement(doc, 'h4', { text: 'そのほかの参照' }));
1135
+ const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-basis-list' });
1136
+ for (const reference of basis.others) {
1137
+ const item = makeWorkspaceElement(doc, 'li');
1138
+ const name = referenceDisplayName(reference, labels);
1139
+ item.append(makeWorkspaceElement(doc, 'strong', { className: name.known ? '' : 'bb-jh-unrecorded', text: name.text }));
1140
+ item.append(makeWorkspaceElement(doc, 'small', { text: referenceKindLabel(reference) }));
1141
+ const why = referenceWhy(doc, reference);
1142
+ if (why) item.append(why);
1143
+ list.append(item);
1144
+ }
1145
+ section.append(list);
1146
+ wrap.append(section);
1147
+ }
1148
+ return wrap;
1149
+ }
1150
+
1151
+ function appendOwnerJudgment(doc, references) {
1152
+ const { owner } = describeJudgmentBasis(references);
1153
+ const wrap = makeWorkspaceElement(doc, 'div', { className: `bb-jh-owner is-${owner.state}` });
1154
+ if (owner.state === 'applied') {
1155
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: `あなたの判断の材料(個人KG)を${owner.count}件使いました。` }));
1156
+ wrap.append(makeWorkspaceElement(doc, 'p', { text: 'この判断には、あなたの考えが入っています。個人KGの中身はここには出しません。' }));
1157
+ } else if (owner.state === 'not_applied') {
1158
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: 'あなたの判断の材料(個人KG)は使っていません。' }));
1159
+ wrap.append(makeWorkspaceElement(doc, 'p', { text: 'Graphの哲学・目的・世界モデルと、AIの判断で進めました。' }));
1160
+ } else {
1161
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: '個人KGを使ったかどうかは記録されていません。' }));
1162
+ wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: 'この記録の書き手は使い方を残していません。使っていないとは限りません。' }));
1163
+ }
1164
+ return wrap;
1165
+ }
1166
+
1167
+ function makeOwnerJudgmentBadge(doc, references) {
1168
+ const { owner } = describeJudgmentBasis(references);
1169
+ return makeWorkspaceElement(doc, 'span', {
1170
+ className: `bb-jh-owner-badge is-${owner.state}`,
1171
+ text: OWNER_JUDGMENT_BADGES[owner.state],
1172
+ });
896
1173
  }
897
1174
 
898
1175
  function evidenceIdentifier(entry) {
@@ -927,16 +1204,16 @@ function appendNormalAlternatives(doc, alternatives) {
927
1204
  if (alternatives === null || alternatives === undefined) {
928
1205
  return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '案は記録されていません。' });
929
1206
  }
930
- if (!alternatives.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '保存された案はありません。' });
1207
+ if (!alternatives.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '比べた案はありません。' });
931
1208
  const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-alternative-list' });
932
1209
  for (const alternative of alternatives) {
933
1210
  const item = makeWorkspaceElement(doc, 'li');
934
- item.append(makeWorkspaceElement(doc, 'strong', { text: alternative.label || '案の名称は未記録' }));
1211
+ item.append(makeWorkspaceElement(doc, 'strong', { text: alternative.label || '案の名前は記録されていません' }));
935
1212
  if (alternative.evaluation) item.append(makeWorkspaceElement(doc, 'span', { text: `評価: ${alternative.evaluation}` }));
936
- if (alternative.adoption) item.append(makeWorkspaceElement(doc, 'span', { text: `採用状態: ${alternative.adoption}` }));
937
- else if (alternative.adopted === true) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 採用' }));
938
- else if (alternative.adopted === false) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 不採用' }));
939
- else if (Object.prototype.hasOwnProperty.call(alternative, 'adopted')) item.append(makeWorkspaceElement(doc, 'span', { text: '採用状態: 記録なし' }));
1213
+ if (alternative.adoption) item.append(makeWorkspaceElement(doc, 'span', { text: alternative.adoption }));
1214
+ else if (alternative.adopted === true) item.append(makeWorkspaceElement(doc, 'span', { text: '採用した案' }));
1215
+ else if (alternative.adopted === false) item.append(makeWorkspaceElement(doc, 'span', { text: '採用しなかった案' }));
1216
+ else if (Object.prototype.hasOwnProperty.call(alternative, 'adopted')) item.append(makeWorkspaceElement(doc, 'span', { text: '採用したかは記録されていません' }));
940
1217
  list.append(item);
941
1218
  }
942
1219
  return list;
@@ -944,26 +1221,26 @@ function appendNormalAlternatives(doc, alternatives) {
944
1221
 
945
1222
  function normalExecutionLabel(execution) {
946
1223
  const status = text(execution?.status).toLowerCase();
947
- const labels = { pending: '実行待ち', completed: '実行済み', failed: '実行失敗', held: '保留', cancelled: '取消', unknown: '実行状態未確認' };
948
- return labels[status] ?? '実行状態未確認';
1224
+ const labels = { pending: 'まだ実行していません', completed: '実行しました', failed: '実行に失敗しました', held: '保留しています', cancelled: '取り消しました' };
1225
+ return labels[status] ?? '実行したかは記録されていません';
949
1226
  }
950
1227
 
951
- function normalOutcomeLabel(execution) {
952
- const status = text(execution?.outcome_status).toLowerCase();
953
- const labels = { confirmed: '結果確認済み', unconfirmed: '結果未確認', unknown: '結果未確認' };
954
- return labels[status] ?? '結果未確認';
1228
+ function normalOutcomeLabel(record) {
1229
+ return outcomeConfirmed(record)
1230
+ ? '結果を確かめました'
1231
+ : 'まだ確かめていません。下の「この判断は良かったか」で記録できます。';
955
1232
  }
956
1233
 
957
- function appendNormalExecution(doc, execution) {
1234
+ function appendNormalExecution(doc, record) {
958
1235
  return workspaceDefinition(doc, [
959
- ['実行', [normalExecutionLabel(execution), text(execution?.result_summary)]],
960
- ['結果', normalOutcomeLabel(execution)],
1236
+ ['実行', [normalExecutionLabel(record.execution), text(record.execution?.result_summary)]],
1237
+ ['結果', normalOutcomeLabel(record)],
961
1238
  ]);
962
1239
  }
963
1240
 
964
- function appendNormalMissingFields(doc, missingFields) {
965
- if (!Array.isArray(missingFields) || missingFields.length === 0) return null;
966
- return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-missing-fields', text: `未記録: ${missingFields.join('、')}` });
1241
+ function missingFieldLabels(missingFields) {
1242
+ if (!Array.isArray(missingFields)) return [];
1243
+ return [...new Set(missingFields.map((field) => MISSING_FIELD_LABELS[field] ?? field))];
967
1244
  }
968
1245
 
969
1246
  function appendTextParts(doc, values) {
@@ -975,10 +1252,8 @@ function appendTextParts(doc, values) {
975
1252
  }
976
1253
 
977
1254
  function normalFeedbackContentSummary(content) {
978
- if (!isRecord(content)) return '内容は未記録';
979
- return text(content.summary ?? content.result_summary ?? content.message ?? content.reason)
980
- || canonicalJson(content)
981
- || '内容は未記録';
1255
+ if (!isRecord(content)) return '内容は記録されていません';
1256
+ return text(content.summary ?? content.result_summary ?? content.message ?? content.reason) || '内容は記録されていません';
982
1257
  }
983
1258
 
984
1259
  function normalReferencePayload(record, event, applicabilityReason = '', eventRef = null) {
@@ -1001,130 +1276,169 @@ function normalReferencePayloadText(record, event, applicabilityReason = '', eve
1001
1276
 
1002
1277
  function appendNormalReferenceExport(doc, record, event, { onCopyReference } = {}) {
1003
1278
  const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-reference-export' });
1004
- const refLabel = event
1005
- ? `${record.recordId} / ${event.eventId}`
1006
- : `${record.recordId} / イベントIDは未記録`;
1007
- wrap.append(makeWorkspaceElement(doc, 'code', { text: refLabel }));
1008
- if (event) wrap.append(makeWorkspaceElement(doc, 'p', { text: `種別: ${FEEDBACK_KIND_LABELS[event.kind] ?? event.kind}・${normalFeedbackContentSummary(event.content)}` }));
1279
+ wrap.append(makeWorkspaceElement(doc, 'p', {
1280
+ text: event ? `${FEEDBACK_KIND_LABELS[event.kind] ?? event.kind}:${normalFeedbackContentSummary(event.content)}` : 'この判断そのもの',
1281
+ }));
1009
1282
  const reason = makeWorkspaceElement(doc, 'textarea', {
1010
1283
  className: 'bb-jh-applicability-reason',
1011
1284
  attrs: {
1012
1285
  rows: 2,
1013
- 'aria-label': `適用理由${event ? `(${event.eventId})` : ''}`,
1014
- placeholder: '次の判断で使う理由を記入',
1286
+ 'aria-label': `引き継ぐ理由${event ? `(${event.eventId})` : ''}`,
1287
+ placeholder: '次の判断でこれを使う理由(任意)',
1015
1288
  },
1016
1289
  });
1017
1290
  reason.value = '';
1018
- wrap.append(makeWorkspaceElement(doc, 'label', { text: '適用理由' }), reason);
1291
+ wrap.append(makeWorkspaceElement(doc, 'label', { text: '引き継ぐ理由' }), reason);
1019
1292
  const preview = makeWorkspaceElement(doc, 'pre', {
1020
1293
  className: 'bb-jh-reference-payload',
1021
1294
  text: normalReferencePayloadText(record, event),
1022
1295
  });
1023
- const status = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-copy-status', text: 'この情報を次の判断へ渡せます。' });
1296
+ const status = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-copy-status', text: '' });
1024
1297
  wrap.append(preview, workspaceButton(doc, {
1025
- text: '参照情報をコピー',
1298
+ text: 'AIへ渡す参照をコピー',
1026
1299
  variant: 'quiet',
1027
1300
  attrs: { 'data-action': 'copy-judgment-reference' },
1028
1301
  onClick: () => {
1029
1302
  if (typeof onCopyReference === 'function') onCopyReference({ record, event, reason, preview, status });
1030
- else status.textContent = '参照情報を選択してコピーできます。';
1303
+ else status.textContent = '上の内容を選んでコピーできます。';
1031
1304
  },
1032
1305
  }), status);
1033
1306
  return wrap;
1034
1307
  }
1035
1308
 
1036
- function appendNormalFeedbackEvents(doc, record, { onCopyReference } = {}) {
1309
+ function appendNormalFeedbackEvents(doc, record) {
1037
1310
  const events = Array.isArray(record.feedbackEvents) ? record.feedbackEvents : [];
1038
- const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-events' });
1039
- if (!events.length) {
1040
- wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '追記された結果・訂正・フィードバックはありません。' }));
1041
- wrap.append(appendNormalReferenceExport(doc, record, null, { onCopyReference }));
1042
- return wrap;
1043
- }
1311
+ if (!events.length) return null;
1312
+ const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-feedback-events' });
1044
1313
  for (const event of events) {
1045
- const item = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-event' });
1314
+ const item = makeWorkspaceElement(doc, 'li', { className: `bb-jh-feedback-event is-${event.kind}` });
1046
1315
  item.append(makeWorkspaceElement(doc, 'strong', { text: FEEDBACK_KIND_LABELS[event.kind] ?? event.kind }));
1047
- item.append(makeWorkspaceElement(doc, 'code', { text: `record_id=${event.recordId} event_id=${event.eventId}` }));
1048
1316
  item.append(makeWorkspaceElement(doc, 'p', { text: normalFeedbackContentSummary(event.content) }));
1049
- item.append(appendNormalReferenceExport(doc, record, event, { onCopyReference }));
1050
- wrap.append(item);
1317
+ list.append(item);
1051
1318
  }
1052
- return wrap;
1319
+ return list;
1053
1320
  }
1054
1321
 
1055
1322
  function feedbackStatusText(draft) {
1056
1323
  if (!draft) return '';
1057
- if (draft.phase === 'submitting') return '送信中です。保存状態はまだ未確認です。';
1058
- if (draft.phase === 'readback') return '正本を読み戻して保存状態を確認しています。';
1059
- if (draft.phase === 'saved') return '保存済み(正本で確認しました)。';
1060
- if (draft.phase === 'unavailable') return `利用できません。${draft.status || '正本に接続できないため、保存状態を確認できません。'}`;
1061
- if (draft.phase === 'unconfirmed') return `未確認。${draft.status || 'POST後の正本読戻しで一致を確認できませんでした。'}`;
1062
- if (draft.phase === 'error') return `送信できません。保存状態は未確認です。${draft.status ? ` ${draft.status}` : ''}`;
1063
- return draft.status || '送信前です。';
1324
+ if (draft.phase === 'submitting') return '記録しています…';
1325
+ if (draft.phase === 'readback') return '記録できたか確かめています…';
1326
+ if (draft.phase === 'saved') return '記録しました。';
1327
+ if (draft.phase === 'unavailable') return `記録できませんでした。${draft.status || '記録先に接続できません。'}`;
1328
+ if (draft.phase === 'unconfirmed') return `送りましたが、記録できたかを確かめられませんでした。${draft.status || ''}再読み込みして確かめてください。`;
1329
+ if (draft.phase === 'error') return draft.status || '記録できませんでした。';
1330
+ return draft.status || '';
1331
+ }
1332
+
1333
+ function feedbackAnswer(value) {
1334
+ return FEEDBACK_ANSWERS.find((answer) => answer.value === value) ?? null;
1064
1335
  }
1065
1336
 
1066
1337
  function appendNormalFeedbackComposer(doc, record, {
1067
1338
  detailReady = false,
1068
1339
  detailPhase = 'idle',
1069
1340
  draft,
1070
- onKindChange,
1341
+ onAnswerChange,
1071
1342
  onSummaryChange,
1072
- onOutcomeStatusChange,
1073
1343
  onSubmitFeedback,
1074
1344
  } = {}) {
1075
1345
  const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-composer' });
1076
1346
  if (!detailReady) {
1077
1347
  const message = detailPhase === 'loading'
1078
- ? '正本の詳細を確認しています。'
1079
- : '正本の詳細を確認できないため、追記は利用できません。';
1348
+ ? 'この判断の記録を読み込んでいます…'
1349
+ : 'この判断の記録を読み込めないため、いまは記録できません。再読み込みしてください。';
1080
1350
  wrap.append(makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: message }));
1081
1351
  return wrap;
1082
1352
  }
1083
- const kindLabel = makeWorkspaceElement(doc, 'label', { text: '追記の種別' });
1084
- const kind = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '追記の種別' } });
1085
- for (const value of FEEDBACK_KINDS) {
1086
- kind.append(makeWorkspaceElement(doc, 'option', {
1087
- text: FEEDBACK_KIND_LABELS[value],
1088
- attrs: { value },
1353
+ const phase = draft?.phase ?? 'idle';
1354
+ const busy = phase === 'submitting' || phase === 'readback';
1355
+ const saved = phase === 'saved';
1356
+ const selected = feedbackAnswer(draft?.answer);
1357
+ const answers = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback-answers', attrs: { role: 'group', 'aria-label': 'この判断は良かったか' } });
1358
+ for (const answer of FEEDBACK_ANSWERS) {
1359
+ const active = answer.value === selected?.value;
1360
+ answers.append(workspaceButton(doc, {
1361
+ text: answer.label,
1362
+ variant: active ? 'primary' : 'quiet',
1363
+ disabled: busy || saved,
1364
+ attrs: { 'data-answer': answer.value, 'aria-pressed': active ? 'true' : 'false' },
1365
+ onClick: () => onAnswerChange?.(answer.value),
1089
1366
  }));
1090
1367
  }
1091
- kind.value = draft?.kind ?? 'feedback';
1092
- kind.addEventListener('change', (event) => onKindChange?.(event?.target?.value ?? kind.value));
1093
- const summaryLabel = makeWorkspaceElement(doc, 'label', { text: '内容(保存公開フィールド)' });
1094
1368
  const summary = makeWorkspaceElement(doc, 'textarea', {
1095
1369
  className: 'bb-jh-feedback-content',
1096
- attrs: { rows: 3, 'aria-label': '追記の内容', placeholder: '結果・訂正・フィードバックを記入' },
1370
+ attrs: {
1371
+ rows: 3,
1372
+ 'aria-label': '判断へのひとこと',
1373
+ placeholder: selected?.placeholder ?? '気づいたことを書いてください(任意)',
1374
+ },
1097
1375
  });
1098
1376
  summary.value = draft?.summary ?? '';
1377
+ if (busy || saved) summary.setAttribute('disabled', '');
1099
1378
  summary.addEventListener('input', (event) => onSummaryChange?.(event?.target?.value ?? summary.value));
1100
- const outcomeLabel = makeWorkspaceElement(doc, 'label', { text: '結果の確認状態(明示する場合)' });
1101
- const outcomeStatus = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': '結果の確認状態' } });
1102
- outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: '記録しない', attrs: { value: '' } }));
1103
- for (const value of ['unknown', 'unconfirmed', 'confirmed']) {
1104
- const label = { unknown: '未確認', unconfirmed: '結果未確認', confirmed: '結果確認済み' }[value];
1105
- outcomeStatus.append(makeWorkspaceElement(doc, 'option', { text: label, attrs: { value } }));
1106
- }
1107
- outcomeStatus.value = draft?.outcomeStatus ?? '';
1108
- outcomeStatus.addEventListener('change', (event) => onOutcomeStatusChange?.(event?.target?.value ?? outcomeStatus.value));
1109
- const eventId = makeWorkspaceElement(doc, 'code', { text: `event_id=${draft?.eventId ?? '未発行'}` });
1110
- const phase = draft?.phase ?? 'idle';
1111
- const busy = phase === 'submitting' || phase === 'readback';
1112
1379
  const button = workspaceButton(doc, {
1113
- text: phase === 'saved' ? '保存済み' : '結果・訂正・フィードバックを追記',
1380
+ text: saved ? '記録しました' : '記録する',
1114
1381
  variant: 'primary',
1115
- disabled: busy || phase === 'saved',
1382
+ disabled: busy || saved,
1116
1383
  attrs: { 'data-action': 'submit-judgment-feedback' },
1117
- onClick: () => onSubmitFeedback?.({ kind, summary, outcomeStatus, eventId: draft?.eventId }),
1384
+ onClick: () => onSubmitFeedback?.({ summary }),
1118
1385
  });
1119
1386
  const status = makeWorkspaceElement(doc, 'p', {
1120
1387
  className: `bb-jh-feedback-status is-${phase}`,
1121
1388
  text: feedbackStatusText(draft),
1122
1389
  attrs: { role: phase === 'error' || phase === 'unavailable' ? 'alert' : 'status' },
1123
1390
  });
1124
- wrap.append(kindLabel, kind, summaryLabel, summary, outcomeLabel, outcomeStatus, eventId, button, status);
1391
+ wrap.append(answers, summary, button, status);
1125
1392
  return wrap;
1126
1393
  }
1127
1394
 
1395
+ /** Ids of the references, with kind and whether they can be opened, for the record details. */
1396
+ function referenceIdList(doc, references) {
1397
+ if (!Array.isArray(references) || !references.length) return null;
1398
+ const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-id-list' });
1399
+ for (const ref of references) {
1400
+ const id = text(ref.ref ?? ref.id);
1401
+ // Personal KG records are shown only as "used" and a count, never by id.
1402
+ if (!id || judgmentReferenceKind(ref) === PERSONAL_KNOWLEDGE_KIND) continue;
1403
+ const availability = REFERENCE_AVAILABILITY_DETAIL[text(ref.availability)] ?? REFERENCE_AVAILABILITY_DETAIL.unknown;
1404
+ const version = text(ref.version) ? `・版 ${text(ref.version)}` : '';
1405
+ list.append(makeWorkspaceElement(doc, 'li', { text: `${id}(${referenceKindLabel(ref)}${version}・${availability})` }));
1406
+ }
1407
+ return list.children?.length ? list : null;
1408
+ }
1409
+
1410
+ function appendNormalRecordDetails(doc, record, { onCopyReference, open = false, onToggle } = {}) {
1411
+ const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-record-details' });
1412
+ details.open = open === true;
1413
+ if (typeof onToggle === 'function') details.addEventListener('toggle', () => onToggle(details.open));
1414
+ details.append(makeWorkspaceElement(doc, 'summary', { text: '記録の詳細' }));
1415
+ const referenceIds = referenceIdList(doc, record.selectedReferences);
1416
+ const events = Array.isArray(record.feedbackEvents) ? record.feedbackEvents : [];
1417
+ const eventIds = events.length ? makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-id-list' }) : null;
1418
+ for (const event of events) {
1419
+ eventIds.append(makeWorkspaceElement(doc, 'li', { text: `${FEEDBACK_KIND_LABELS[event.kind] ?? event.kind}: ${event.eventId}` }));
1420
+ }
1421
+ const missing = missingFieldLabels(record.missingFields);
1422
+ details.append(workspaceDefinition(doc, [
1423
+ ['記録ID', record.recordId],
1424
+ ['会話のID', record.turnRef || '記録されていません'],
1425
+ ['判断の状態', judgmentStatusDetail(record.judgment?.status) ?? '記録されていません'],
1426
+ ['参照のID', referenceIds ?? 'ありません'],
1427
+ ['追記のID', eventIds ?? 'まだありません'],
1428
+ ['記録されていない項目', missing.length ? `${missing.join('、')}(判断したAIが書き残していない項目です)` : 'ありません'],
1429
+ ]));
1430
+ const handoff = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-handoff' });
1431
+ handoff.append(makeWorkspaceElement(doc, 'h4', { text: '次の判断に引き継ぐ' }));
1432
+ handoff.append(makeWorkspaceElement(doc, 'p', {
1433
+ className: 'bb-jh-reference-note',
1434
+ text: 'この判断を別の判断の根拠として使うときに、AIへ渡す参照です。コピーしてAIとの会話に貼ります。',
1435
+ }));
1436
+ handoff.append(appendNormalReferenceExport(doc, record, null, { onCopyReference }));
1437
+ for (const event of events) handoff.append(appendNormalReferenceExport(doc, record, event, { onCopyReference }));
1438
+ details.append(handoff);
1439
+ return details;
1440
+ }
1441
+
1128
1442
  function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
1129
1443
  if (!rail) return;
1130
1444
  rail.replaceChildren();
@@ -1134,8 +1448,8 @@ function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
1134
1448
  const homeRecord = row?.mode === 'normal' ? normalizeNormalRecord(row.record) : null;
1135
1449
  const record = detail ?? homeRecord ?? row;
1136
1450
  rail.append(workspaceRailHead(doc, {
1137
- kicker: `${NORMAL_ENTRYPOINT_LABELS[record.entrypoint] ?? '入口不明'}の判断`,
1138
- title: text(record.judgment?.summary) || '判断内容は未記録',
1451
+ kicker: `${NORMAL_ENTRYPOINT_LABELS[record.entrypoint] ?? NORMAL_ENTRYPOINT_LABELS.unknown}の判断`,
1452
+ title: text(record.judgment?.summary) || '判断の内容は記録されていません',
1139
1453
  sub: formatDate(record.recordedAt),
1140
1454
  }));
1141
1455
  if (typeof onClose === 'function') {
@@ -1147,37 +1461,41 @@ function renderNormalRail(doc, rail, row, detail, onClose, options = {}) {
1147
1461
  }));
1148
1462
  }
1149
1463
  rail.append(workspaceRailBlock(doc, {
1150
- title: '使った参照(当時の記録)',
1151
- content: appendReferenceList(doc, record.selectedReferences),
1464
+ title: '判断の根拠',
1465
+ className: 'bb-jh-basis-block',
1466
+ content: appendJudgmentBasis(doc, record.selectedReferences, options.labels),
1467
+ }));
1468
+ rail.append(workspaceRailBlock(doc, {
1469
+ title: '本人の考え',
1470
+ content: appendOwnerJudgment(doc, record.selectedReferences),
1152
1471
  }));
1153
1472
  rail.append(workspaceRailBlock(doc, {
1154
1473
  title: '判断の理由',
1155
1474
  content: appendTextParts(doc, [text(record.judgment?.reason) || '理由は記録されていません。']),
1156
1475
  }));
1157
1476
  rail.append(workspaceRailBlock(doc, {
1158
- title: '保存された案',
1477
+ title: '比べた案',
1159
1478
  content: appendNormalAlternatives(doc, record.alternatives),
1160
1479
  }));
1161
1480
  rail.append(workspaceRailBlock(doc, {
1162
1481
  title: '実行と結果',
1163
- content: appendNormalExecution(doc, record.execution),
1482
+ content: appendNormalExecution(doc, record),
1164
1483
  }));
1165
- rail.append(workspaceRailBlock(doc, {
1166
- title: '結果・訂正・フィードバック',
1167
- content: appendNormalFeedbackEvents(doc, record, { onCopyReference: options.onCopyReference }),
1168
- }));
1169
- rail.append(workspaceRailBlock(doc, {
1170
- title: '次の判断に渡す参照',
1171
- content: appendNormalFeedbackComposer(doc, record, options),
1172
- }));
1173
- const metadata = workspaceDefinition(doc, [
1174
- ['記録ID', record.recordId],
1484
+ const feedback = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-feedback' });
1485
+ const events = appendNormalFeedbackEvents(doc, record);
1486
+ if (events) feedback.append(events);
1487
+ feedback.append(appendNormalFeedbackComposer(doc, record, options));
1488
+ rail.append(workspaceRailBlock(doc, { title: 'この判断は良かったか', content: feedback }));
1489
+ const scope = workspaceDefinition(doc, [
1175
1490
  ['プロジェクト', record.projectCode],
1176
- ['Turn', record.turnRef],
1177
1491
  ]);
1178
- const missing = appendNormalMissingFields(doc, record.missingFields);
1179
- if (missing) metadata.append(missing);
1180
- rail.append(workspaceRailBlock(doc, { title: '記録の範囲', content: metadata }));
1492
+ const range = makeWorkspaceElement(doc, 'div');
1493
+ range.append(scope, appendNormalRecordDetails(doc, record, {
1494
+ onCopyReference: options.onCopyReference,
1495
+ open: options.recordDetailsOpen,
1496
+ onToggle: options.onRecordDetailsToggle,
1497
+ }));
1498
+ rail.append(workspaceRailBlock(doc, { title: '記録の範囲', content: range }));
1181
1499
  }
1182
1500
 
1183
1501
  function renderRail(doc, rail, row, detail, onClose, options = {}) {
@@ -1211,10 +1529,15 @@ function renderRail(doc, rail, row, detail, onClose, options = {}) {
1211
1529
  attrs: { 'aria-label': '判断の詳細を閉じる' },
1212
1530
  }));
1213
1531
  }
1214
- rail.append(workspaceRailBlock(doc, {
1215
- title: '使った参照',
1216
- content: appendReferenceList(doc, row.references),
1217
- }));
1532
+ const references = makeWorkspaceElement(doc, 'div');
1533
+ references.append(appendReferenceList(doc, row.references));
1534
+ const ids = referenceIdList(doc, row.references);
1535
+ if (ids) {
1536
+ const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-record-details' });
1537
+ details.append(makeWorkspaceElement(doc, 'summary', { text: '記録の詳細' }), workspaceDefinition(doc, [['参照のID', ids]]));
1538
+ references.append(details);
1539
+ }
1540
+ rail.append(workspaceRailBlock(doc, { title: '使った参照', content: references }));
1218
1541
  rail.append(workspaceRailBlock(doc, {
1219
1542
  title: '行った判断',
1220
1543
  content: appendTextParts(doc, [
@@ -1321,9 +1644,7 @@ function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow
1321
1644
  reload,
1322
1645
  });
1323
1646
  }
1324
- if (loading) {
1325
- return workspaceLoadingCard(doc, { title: 'ロード中' });
1326
- }
1647
+ if (loading) return null;
1327
1648
  if (home?.status === 'partial') {
1328
1649
  return historyBanner(doc, {
1329
1650
  label: '一部の記録だけを表示中',
@@ -1400,7 +1721,7 @@ function normalSourceStatusLabel(status) {
1400
1721
  }
1401
1722
 
1402
1723
  function sourceChipText(source) {
1403
- const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
1724
+ const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? NORMAL_ENTRYPOINT_LABELS.unknown;
1404
1725
  if (source?.status === 'pending' || source?.status === 'loading') return `${name}:確認中…`;
1405
1726
  if (Number.isInteger(source?.retrieved)) return `${name}:${source.retrieved}件まで届きました`;
1406
1727
  const statusLabel = normalSourceStatusLabel(source.status);
@@ -1409,21 +1730,21 @@ function sourceChipText(source) {
1409
1730
  return `${name}:${statusLabel}${reason}`;
1410
1731
  }
1411
1732
 
1412
- function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
1413
- const wrap = makeWorkspaceElement(doc, 'label', { className: `bb-jh-filter${className ? ` ${className}` : ''}` });
1733
+ function createFilterCombobox(doc, { key, label, value, options, onChange, searchPlaceholder }) {
1734
+ const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-filter' });
1414
1735
  wrap.append(makeWorkspaceElement(doc, 'span', { text: label }));
1415
- const select = makeWorkspaceElement(doc, 'select', { attrs: { 'aria-label': label } });
1416
- const current = value ?? '';
1417
- const choices = [{ value: '', label: 'すべて' }, ...options];
1418
- for (const option of choices) {
1419
- const optionElement = makeWorkspaceElement(doc, 'option', { text: option.label, attrs: { value: option.value, selected: option.value === current ? 'selected' : undefined } });
1420
- optionElement.value = option.value;
1421
- select.append(optionElement);
1422
- }
1423
- select.value = current;
1424
- select.addEventListener('change', (event) => onChange(event?.target?.value ?? select.value ?? ''));
1425
- wrap.append(select);
1426
- return wrap;
1736
+ const combobox = workspaceCombobox(doc, {
1737
+ label,
1738
+ value: value ?? '',
1739
+ options: [{ value: '', label: 'すべて' }, ...options],
1740
+ searchPlaceholder,
1741
+ onChange: (next) => onChange(next ?? ''),
1742
+ });
1743
+ // A stable mark lets a redraw return focus to the same filter, whose
1744
+ // accessible name changes with the chosen value.
1745
+ combobox.trigger.setAttribute('data-filter', key);
1746
+ wrap.append(combobox.root);
1747
+ return { root: wrap, combobox };
1427
1748
  }
1428
1749
 
1429
1750
  function normalFilterControls(doc, home, state, onChange) {
@@ -1433,6 +1754,8 @@ function normalFilterControls(doc, home, state, onChange) {
1433
1754
  for (const project of knownProjects) {
1434
1755
  if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
1435
1756
  }
1757
+ const sources = Array.isArray(home?.coverage?.sources) ? home.coverage.sources : [];
1758
+ const unconnected = new Set(sources.filter((source) => source.status === 'unavailable').map((source) => normalEntrypoint(source.entrypoint)));
1436
1759
  const entrypoints = [...(filters.entrypoints ?? [])];
1437
1760
  const knownEntrypoints = (home?.records ?? []).map((record) => record.entrypoint).filter(Boolean);
1438
1761
  for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
@@ -1440,20 +1763,29 @@ function normalFilterControls(doc, home, state, onChange) {
1440
1763
  entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
1441
1764
  }
1442
1765
  }
1766
+ const entrypointOptions = entrypoints.map((option) => ({
1767
+ ...option,
1768
+ label: NORMAL_ENTRYPOINT_FILTER_LABELS[option.value] ?? NORMAL_ENTRYPOINT_LABELS[option.value] ?? option.label,
1769
+ hint: unconnected.has(option.value) ? 'まだつながっていません' : '',
1770
+ }));
1443
1771
  const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-normal-filters', attrs: { 'aria-label': '絞り込み' } });
1444
- wrap.append(createFilterSelect(doc, {
1772
+ const project = createFilterCombobox(doc, {
1773
+ key: 'project',
1445
1774
  label: 'プロジェクト',
1446
1775
  value: state.project,
1447
1776
  options: projects,
1448
- onChange: (project) => onChange({ project: project || null }),
1449
- }));
1450
- wrap.append(createFilterSelect(doc, {
1451
- label: '入口',
1777
+ searchPlaceholder: 'プロジェクトを探す',
1778
+ onChange: (next) => onChange({ project: next || null }),
1779
+ });
1780
+ const entrypoint = createFilterCombobox(doc, {
1781
+ key: 'entrypoint',
1782
+ label: '判断したAI',
1452
1783
  value: state.entrypoint,
1453
- options: entrypoints,
1454
- onChange: (entrypoint) => onChange({ entrypoint: entrypoint || null }),
1455
- }));
1456
- return wrap;
1784
+ options: entrypointOptions,
1785
+ onChange: (next) => onChange({ entrypoint: next || null }),
1786
+ });
1787
+ wrap.append(project.root, entrypoint.root);
1788
+ return { root: wrap, comboboxes: [project.combobox, entrypoint.combobox] };
1457
1789
  }
1458
1790
 
1459
1791
  function paginationControls(doc, aggregate, onNext) {
@@ -1476,8 +1808,9 @@ function makeEntrypointStatus(doc, row) {
1476
1808
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: row.delegated ? 'Brainbase' : '本人' }));
1477
1809
  return wrap;
1478
1810
  }
1479
- wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明' }));
1480
- wrap.append(makeWorkspaceElement(doc, 'small', { text: judgmentStatusLabel(row.judgment?.status) }));
1811
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? NORMAL_ENTRYPOINT_LABELS.unknown }));
1812
+ const status = judgmentStatusLabel(row.judgment?.status);
1813
+ if (status) wrap.append(makeWorkspaceElement(doc, 'small', { className: 'bb-jh-waiting', text: status }));
1481
1814
  return wrap;
1482
1815
  }
1483
1816
 
@@ -1494,26 +1827,37 @@ function skeletonRows(doc, count = 5) {
1494
1827
  }));
1495
1828
  }
1496
1829
 
1830
+ function loadingLine(doc) {
1831
+ return makeWorkspaceElement(doc, 'p', {
1832
+ className: 'bb-jh-summary bb-jh-loading',
1833
+ text: '判断の履歴を読み込んでいます…',
1834
+ attrs: { role: 'status', 'aria-live': 'polite' },
1835
+ });
1836
+ }
1837
+
1497
1838
  function summaryLine(doc, aggregate, { loading = false } = {}) {
1498
- const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
1499
1839
  const retrieved = Number.isInteger(aggregate?.retrievedCount) ? aggregate.retrievedCount : (aggregate?.rows?.length ?? 0);
1500
1840
  const complete = aggregate?.countComplete === true && Number.isInteger(aggregate?.totalCount);
1501
1841
  const showCount = complete || retrieved > 0;
1502
- if (showCount) {
1503
- line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
1504
- line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1505
- } else {
1506
- line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1507
- line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1508
- }
1509
- const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
1510
- const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
1842
+ if (loading && !showCount) return loadingLine(doc);
1843
+ const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
1844
+ line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
1845
+ line.append(makeWorkspaceElement(doc, 'span', { text: loading ? ' 届きました(続きを読み込んでいます…)' : ' 取得済み' }));
1846
+ if (loading) return line;
1847
+ const references = aggregate?.stats?.referenceCount ?? aggregate?.stats?.references;
1848
+ const confirmed = aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes;
1511
1849
  line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
1512
- if (Number.isInteger(references)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${references}件` }));
1513
- else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1514
- line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確認済み ' }));
1515
- if (Number.isInteger(confirmed)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${confirmed}件` }));
1516
- else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1850
+ line.append(makeWorkspaceElement(doc, Number.isInteger(references) ? 'strong' : 'span', { text: Number.isInteger(references) ? `${references}件` : '数えられません' }));
1851
+ if (aggregate?.mode !== 'normal') return line;
1852
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確かめた判断 ' }));
1853
+ line.append(makeWorkspaceElement(doc, Number.isInteger(confirmed) ? 'strong' : 'span', { text: Number.isInteger(confirmed) ? `${confirmed}件` : '数えられません' }));
1854
+ // Results are never recorded automatically; say how a person adds one.
1855
+ if (Number.isInteger(confirmed) && confirmed < retrieved) {
1856
+ line.append(makeWorkspaceElement(doc, 'span', {
1857
+ className: 'bb-jh-summary-note',
1858
+ text: ' 結果は自動では記録されません。判断を開いて「良かったか」を記録すると数えられます。',
1859
+ }));
1860
+ }
1517
1861
  return line;
1518
1862
  }
1519
1863
 
@@ -1529,7 +1873,7 @@ function formatCheckedAt(value) {
1529
1873
  }
1530
1874
  }
1531
1875
 
1532
- function makeRows(doc, aggregate, selectedKey, onSelect) {
1876
+ function makeRows(doc, aggregate, selectedKey, onSelect, labels = null) {
1533
1877
  return aggregate.rows.map((row) => ({
1534
1878
  key: row.key,
1535
1879
  label: `${formatDate(row.recordedAt)} ${row.mode === 'normal' ? text(row.judgment?.summary) || '判断履歴' : text(row.proof?.decision?.summary) || '判断履歴'}`,
@@ -1539,7 +1883,7 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
1539
1883
  cells: [
1540
1884
  makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
1541
1885
  makeDecisionSummary(doc, row),
1542
- makeReferenceSummary(doc, row.references),
1886
+ makeReferenceSummary(doc, row.references, row.mode === 'normal' ? labels : null),
1543
1887
  makeEntrypointStatus(doc, row),
1544
1888
  ],
1545
1889
  }));
@@ -1560,6 +1904,9 @@ export function createJudgmentHistoryUI({
1560
1904
  now = new Date(),
1561
1905
  skeletonDelayMs = 300,
1562
1906
  slowDelayMs = 10000,
1907
+ // Optional host port: (references: {ref, kind}[]) => Promise<Map|object of ref -> name>.
1908
+ // The host resolves names under the viewer's current permission.
1909
+ resolveReferenceLabels = null,
1563
1910
  } = {}) {
1564
1911
  if (!root) throw new TypeError('root is required');
1565
1912
  const doc = safeDocument(explicitDocument);
@@ -1598,9 +1945,13 @@ export function createJudgmentHistoryUI({
1598
1945
  checkedAt: null,
1599
1946
  skeletonTimer: null,
1600
1947
  slowTimer: null,
1948
+ labels: new Map(),
1601
1949
  };
1602
1950
 
1603
1951
  let filterPopover = null;
1952
+ let filterComboboxes = [];
1953
+ // The record details stay open across redraws of the same screen.
1954
+ let recordDetailsOpen = false;
1604
1955
  let filterExpanded = false;
1605
1956
  let drawer = null;
1606
1957
  let drawerContent = null;
@@ -1619,19 +1970,13 @@ export function createJudgmentHistoryUI({
1619
1970
  className: 'bb-ws-error-stack bb-jh-notifications', attrs: { 'aria-label': '判断履歴の通知' },
1620
1971
  });
1621
1972
  if (notificationRoot) notificationRoot.append(notifications);
1622
- let loadingCard = null;
1623
- let dismissLoading = null;
1624
1973
  let statusCard = null;
1625
1974
  let statusKey = null;
1626
1975
 
1976
+ // Loading is shown in the page (status line and placeholder rows), the same
1977
+ // way the other workspace screens do; the floating stack keeps only the
1978
+ // states that remain after loading, such as a partial or failed read.
1627
1979
  function updateNotifications(loading) {
1628
- if (loading) {
1629
- dismissLoading?.(); dismissLoading = null;
1630
- if (!loadingCard) loadingCard = workspaceLoadingCard(doc, { title: 'ロード中' });
1631
- if (loadingCard.parentNode !== notifications) notifications.append(loadingCard);
1632
- } else if (loadingCard && !dismissLoading) {
1633
- dismissLoading = workspaceDismissNotification(loadingCard);
1634
- }
1635
1980
  const home = state.home;
1636
1981
  const key = loading ? null : JSON.stringify([state.phase, state.error, home?.status, home?.coverage]);
1637
1982
  if (statusKey !== key) {
@@ -1725,9 +2070,8 @@ export function createJudgmentHistoryUI({
1725
2070
  draft = {
1726
2071
  recordId,
1727
2072
  eventId: newFeedbackEventId(),
1728
- kind: 'feedback',
2073
+ answer: null,
1729
2074
  summary: '',
1730
- outcomeStatus: '',
1731
2075
  phase: 'idle',
1732
2076
  status: '',
1733
2077
  };
@@ -1796,34 +2140,50 @@ export function createJudgmentHistoryUI({
1796
2140
  const clipboard = globalThis.navigator?.clipboard;
1797
2141
  if (!clipboard || typeof clipboard.writeText !== 'function') throw new Error('clipboard_unavailable');
1798
2142
  await clipboard.writeText(serialized);
1799
- status.textContent = '参照情報をコピーしました。';
2143
+ status.textContent = 'コピーしました。';
1800
2144
  return true;
1801
2145
  } catch {
1802
- status.textContent = '参照情報を表示しました。選択してコピーできます。';
2146
+ status.textContent = 'コピーできませんでした。上の内容を選んでコピーしてください。';
1803
2147
  return false;
1804
2148
  }
1805
2149
  }
1806
2150
 
2151
+ // Keep the list's "結果を確かめた判断" count in step with an answer that
2152
+ // was read back from the canonical record, without reloading the page.
2153
+ function applyFeedbackToHome(recordId, feedbackEvents) {
2154
+ if (state.home?.mode !== 'normal' || !Array.isArray(state.home.records)) return;
2155
+ const index = state.home.records.findIndex((record) => record.recordId === recordId);
2156
+ if (index < 0) return;
2157
+ const records = [...state.home.records];
2158
+ records[index] = { ...records[index], feedbackEvents: Array.isArray(feedbackEvents) ? feedbackEvents : [] };
2159
+ state.home = { ...state.home, records };
2160
+ state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
2161
+ }
2162
+
1807
2163
  async function submitFeedback(row, controls) {
1808
2164
  if (!row || row.mode !== 'normal' || !controls || !request || state.disposed) return false;
1809
2165
  const current = feedbackDraft(row.recordId);
1810
- const kind = feedbackKind(controls.kind?.value ?? current?.kind) ?? 'feedback';
1811
- const summary = text(controls.summary?.value ?? current?.summary);
1812
- const outcomeStatus = text(controls.outcomeStatus?.value ?? current?.outcomeStatus).toLowerCase();
2166
+ const answer = feedbackAnswer(current?.answer);
2167
+ const written = text(controls.summary?.value ?? current?.summary);
2168
+ if (!answer) {
2169
+ updateFeedbackDraft(row.recordId, { summary: written, phase: 'error', status: '「良かった」「直すところがある」「まだ分からない」から1つ選んでください。' });
2170
+ render();
2171
+ return false;
2172
+ }
2173
+ const summary = written || answer.fallbackSummary;
1813
2174
  if (!summary) {
1814
- updateFeedbackDraft(row.recordId, { kind, summary, outcomeStatus: '', phase: 'error', status: '内容を入力してください。' });
2175
+ updateFeedbackDraft(row.recordId, { summary: written, phase: 'error', status: 'どこを、どう直すかを書いてください。' });
1815
2176
  render();
1816
2177
  return false;
1817
2178
  }
1818
- const acceptedOutcomeStatuses = new Set(['unknown', 'unconfirmed', 'confirmed']);
2179
+ const { kind } = answer;
1819
2180
  const content = { summary };
1820
- if (acceptedOutcomeStatuses.has(outcomeStatus)) content.outcome_status = outcomeStatus;
1821
- const displayedEventId = text(controls.eventId?.textContent);
1822
- const eventId = displayedEventId.startsWith('event_id=')
1823
- ? displayedEventId.slice('event_id='.length)
1824
- : displayedEventId || current?.eventId || newFeedbackEventId();
2181
+ if (answer.outcomeStatus) content.outcome_status = answer.outcomeStatus;
2182
+ // The draft keeps one event id per record, so a retry after an
2183
+ // unconfirmed save appends the same event instead of a second one.
2184
+ const eventId = current?.eventId || newFeedbackEventId();
1825
2185
  const expected = { recordId: row.recordId, eventId, kind, content };
1826
- updateFeedbackDraft(row.recordId, { eventId, kind, summary, outcomeStatus, phase: 'submitting', status: '' });
2186
+ updateFeedbackDraft(row.recordId, { eventId, summary: written, phase: 'submitting', status: '' });
1827
2187
  render();
1828
2188
  try {
1829
2189
  const response = await request(`${state.activeBasePath}/feedback`, {
@@ -1839,7 +2199,7 @@ export function createJudgmentHistoryUI({
1839
2199
  const unavailable = [401, 403, 404, 405, 501, 503].includes(statusCode);
1840
2200
  updateFeedbackDraft(row.recordId, {
1841
2201
  phase: unavailable ? 'unavailable' : 'unconfirmed',
1842
- status: unavailable ? `正本へ接続できません(HTTP ${statusCode})。` : `受付応答はHTTP ${statusCode}でした。`,
2202
+ status: unavailable ? '記録先に接続できません。' : '',
1843
2203
  });
1844
2204
  render();
1845
2205
  return false;
@@ -1855,18 +2215,16 @@ export function createJudgmentHistoryUI({
1855
2215
  const responseReadback = responseFeedbackReadback(responsePayload, expected);
1856
2216
  if (detailReadback || responseReadback) {
1857
2217
  updateFeedbackDraft(row.recordId, { phase: 'saved', status: '' });
2218
+ if (detailReadback) applyFeedbackToHome(row.recordId, detail.feedbackEvents);
1858
2219
  } else if (state.detailPhase === 'error') {
1859
- updateFeedbackDraft(row.recordId, { phase: 'unavailable', status: '正本を読み戻せませんでした。' });
2220
+ updateFeedbackDraft(row.recordId, { phase: 'unavailable', status: '記録を読み直せませんでした。' });
1860
2221
  } else {
1861
- updateFeedbackDraft(row.recordId, { phase: 'unconfirmed', status: '同じevent_idの正本記録を確認できませんでした。' });
2222
+ updateFeedbackDraft(row.recordId, { phase: 'unconfirmed', status: '' });
1862
2223
  }
1863
2224
  render();
1864
2225
  return detailReadback || responseReadback;
1865
- } catch (error) {
1866
- updateFeedbackDraft(row.recordId, {
1867
- phase: 'unavailable',
1868
- status: `正本へ接続できません。${error instanceof Error ? error.message : String(error)}`,
1869
- });
2226
+ } catch {
2227
+ updateFeedbackDraft(row.recordId, { phase: 'unavailable', status: '記録先に接続できません。' });
1870
2228
  render();
1871
2229
  return false;
1872
2230
  }
@@ -1896,6 +2254,35 @@ export function createJudgmentHistoryUI({
1896
2254
  void load({ append: false });
1897
2255
  }
1898
2256
 
2257
+ function requestReferenceLabels(rows) {
2258
+ const pending = [];
2259
+ for (const row of rows) {
2260
+ if (row.mode !== 'normal' || !Array.isArray(row.references)) continue;
2261
+ for (const reference of row.references) {
2262
+ const ref = text(reference?.ref);
2263
+ const kind = judgmentReferenceKind(reference);
2264
+ if (!ref || kind === PERSONAL_KNOWLEDGE_KIND || referenceLabel(reference) || state.labels.has(ref)) continue;
2265
+ state.labels.set(ref, { status: typeof resolveReferenceLabels === 'function' ? 'loading' : 'missing' });
2266
+ pending.push({ ref, kind });
2267
+ }
2268
+ }
2269
+ if (!pending.length || typeof resolveReferenceLabels !== 'function') return;
2270
+ const settle = (resolved) => {
2271
+ if (state.disposed) return;
2272
+ const lookup = resolved instanceof Map
2273
+ ? (ref) => resolved.get(ref)
2274
+ : isRecord(resolved) ? (ref) => resolved[ref] : () => null;
2275
+ for (const { ref } of pending) {
2276
+ const label = text(lookup(ref));
2277
+ state.labels.set(ref, label ? { status: 'found', label } : { status: 'missing' });
2278
+ }
2279
+ render();
2280
+ };
2281
+ Promise.resolve()
2282
+ .then(() => resolveReferenceLabels(pending.map((entry) => ({ ...entry }))))
2283
+ .then(settle, () => settle(null));
2284
+ }
2285
+
1899
2286
  async function loadDetail(row) {
1900
2287
  if (!row || row.mode !== 'normal' || !request || state.disposed) return null;
1901
2288
  const generation = ++state.detailGeneration;
@@ -1936,17 +2323,24 @@ export function createJudgmentHistoryUI({
1936
2323
  if (state.disposed) return;
1937
2324
  updateNotifications(state.phase === 'loading');
1938
2325
  if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) {
1939
- if (!notificationRoot && notifications.parentNode !== root) root.append(notifications);
2326
+ // Before the placeholder rows appear, say in the page what is happening.
2327
+ const pending = makeWorkspaceElement(doc, 'div', { className: 'bb-jh', attrs: { 'aria-busy': 'true' } });
2328
+ if (!notificationRoot) pending.append(notifications);
2329
+ pending.append(loadingLine(doc));
2330
+ root.replaceChildren(pending);
1940
2331
  return;
1941
2332
  }
1942
2333
  const focusedFilter = filterExpanded && filterPopover
1943
2334
  ? findFirst(filterPopover.panel, node => node === doc.activeElement) : null;
1944
2335
  const filterFocus = focusedFilter && {
2336
+ filter: attributeValue(focusedFilter, 'data-filter'),
1945
2337
  label: attributeValue(focusedFilter, 'aria-label'), period: attributeValue(focusedFilter, 'data-period'),
1946
2338
  };
1947
2339
  filterPopover?.destroy();
1948
2340
  filterPopover = null;
1949
- const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
2341
+ for (const combobox of filterComboboxes) combobox.destroy();
2342
+ filterComboboxes = [];
2343
+ const previousSearchInput = findFirst(root, isSearchInput);
1950
2344
  const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
1951
2345
  ? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
1952
2346
  : null;
@@ -1995,7 +2389,11 @@ export function createJudgmentHistoryUI({
1995
2389
  }
1996
2390
  }));
1997
2391
  const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
1998
- if (showNormalFilters) filterFields.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
2392
+ if (showNormalFilters) {
2393
+ const controlsForFilters = normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange);
2394
+ filterComboboxes = controlsForFilters.comboboxes;
2395
+ filterFields.append(controlsForFilters.root);
2396
+ }
1999
2397
  controls.append(createSearch(doc, state.query, (query) => {
2000
2398
  state.query = String(query);
2001
2399
  state.focusSearch = true;
@@ -2041,6 +2439,7 @@ export function createJudgmentHistoryUI({
2041
2439
  const emptyText = loading ? '' : state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
2042
2440
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
2043
2441
  : 'この期間の判断履歴はありません。';
2442
+ requestReferenceLabels(aggregate.rows);
2044
2443
  const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
2045
2444
  state.lastFocusedKey = key;
2046
2445
  state.selectedKey = key;
@@ -2049,11 +2448,11 @@ export function createJudgmentHistoryUI({
2049
2448
  render();
2050
2449
  const selectedRow = aggregate.rows.find((row) => row.key === key);
2051
2450
  if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
2052
- });
2451
+ }, state.labels);
2053
2452
  wrapper.append(workspaceLedger(doc, {
2054
2453
  className: 'bb-jh-ledger',
2055
2454
  ariaLabel: '判断履歴',
2056
- columns: ['日時', '行った判断', '使った参照', '入口・状態'],
2455
+ columns: ['日時', '行った判断', '使った参照', '判断したAI'],
2057
2456
  rows: loading ? [...dataRows, ...skeletonRows(doc)] : dataRows,
2058
2457
  empty: emptyText,
2059
2458
  }));
@@ -2064,20 +2463,22 @@ export function createJudgmentHistoryUI({
2064
2463
  }
2065
2464
  });
2066
2465
  if (pagination) wrapper.append(pagination);
2067
- const nextSearchInput = findFirst(wrapper, (node) => node.tagName === 'INPUT');
2466
+ const nextSearchInput = findFirst(wrapper, isSearchInput);
2068
2467
  if (previousSearchInput && nextSearchInput) {
2069
2468
  previousSearchInput.value = state.query;
2070
2469
  replaceNode(nextSearchInput, previousSearchInput);
2071
2470
  }
2072
2471
  root.replaceChildren(wrapper);
2073
2472
  if (filterExpanded && filterFocus) {
2074
- const field = findFirst(filterPopover.panel, node => filterFocus.label
2075
- ? attributeValue(node, 'aria-label') === filterFocus.label
2076
- : filterFocus.period && attributeValue(node, 'data-period') === filterFocus.period);
2473
+ const field = findFirst(filterPopover.panel, node => (filterFocus.filter
2474
+ ? attributeValue(node, 'data-filter') === filterFocus.filter
2475
+ : filterFocus.label
2476
+ ? attributeValue(node, 'aria-label') === filterFocus.label
2477
+ : filterFocus.period && attributeValue(node, 'data-period') === filterFocus.period));
2077
2478
  field?.focus?.();
2078
2479
  }
2079
2480
  if (state.focusSearch || previousSearchFocused) {
2080
- const input = findFirst(root, (node) => node.tagName === 'INPUT');
2481
+ const input = findFirst(root, isSearchInput);
2081
2482
  input?.focus?.();
2082
2483
  const start = previousSearchSelection?.start ?? state.query.length;
2083
2484
  const end = previousSearchSelection?.end ?? start;
@@ -2091,14 +2492,21 @@ export function createJudgmentHistoryUI({
2091
2492
  detailReady: state.detailPhase === 'ready' && Boolean(selectedDetail),
2092
2493
  detailPhase: state.detailPhase,
2093
2494
  draft: feedbackDraft(selected.recordId),
2094
- onKindChange: (kind) => {
2095
- updateFeedbackDraft(selected.recordId, { kind: feedbackKind(kind) ?? 'feedback' });
2495
+ onAnswerChange: (answer) => {
2496
+ const current = feedbackDraft(selected.recordId);
2497
+ updateFeedbackDraft(selected.recordId, {
2498
+ answer: feedbackAnswer(answer)?.value ?? null,
2499
+ phase: current?.phase === 'error' ? 'idle' : current?.phase,
2500
+ status: current?.phase === 'error' ? '' : current?.status,
2501
+ });
2096
2502
  render();
2097
2503
  },
2098
2504
  onSummaryChange: (summary) => updateFeedbackDraft(selected.recordId, { summary: String(summary ?? '') }),
2099
- onOutcomeStatusChange: (outcomeStatus) => updateFeedbackDraft(selected.recordId, { outcomeStatus: String(outcomeStatus ?? '') }),
2505
+ recordDetailsOpen,
2506
+ onRecordDetailsToggle: (open) => { recordDetailsOpen = open; },
2100
2507
  onSubmitFeedback: (controls) => void submitFeedback(selected, controls),
2101
2508
  onCopyReference: (payload) => void copyReference(payload),
2509
+ labels: state.labels,
2102
2510
  }
2103
2511
  : {};
2104
2512
  if (nativeDrawer) {
@@ -2253,7 +2661,7 @@ export function createJudgmentHistoryUI({
2253
2661
  state.generation += 1;
2254
2662
  clearLoadTimers();
2255
2663
  filterPopover?.destroy();
2256
- dismissLoading?.();
2664
+ for (const combobox of filterComboboxes) combobox.destroy();
2257
2665
  notifications.remove?.();
2258
2666
  disposeDrawer();
2259
2667
  root.removeEventListener?.('keydown', handleKeyDown);