@unson/brainbase-mcp 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/local-web-fetch-bridge.js +1 -1
  2. package/dist/local-web-host.d.ts +59 -0
  3. package/dist/local-web-host.js +171 -4
  4. package/dist/meeting-minutes-http.d.ts +22 -0
  5. package/dist/meeting-minutes-http.js +252 -0
  6. package/dist/meeting-minutes-lineage-adapters.d.ts +157 -0
  7. package/dist/meeting-minutes-lineage-adapters.js +410 -0
  8. package/dist/meeting-minutes-lineage-http.d.ts +20 -0
  9. package/dist/meeting-minutes-lineage-http.js +221 -0
  10. package/dist/meeting-minutes-lineage-lifecycle.d.ts +58 -0
  11. package/dist/meeting-minutes-lineage-lifecycle.js +146 -0
  12. package/dist/meeting-minutes-lineage.d.ts +307 -0
  13. package/dist/meeting-minutes-lineage.js +1030 -0
  14. package/dist/meeting-minutes-storage-controller.d.ts +182 -0
  15. package/dist/meeting-minutes-storage-controller.js +366 -0
  16. package/dist/meeting-minutes-storage-http.d.ts +25 -0
  17. package/dist/meeting-minutes-storage-http.js +312 -0
  18. package/dist/meeting-minutes-storage.d.ts +169 -0
  19. package/dist/meeting-minutes-storage.js +402 -0
  20. package/dist/meeting-minutes.d.ts +223 -0
  21. package/dist/meeting-minutes.js +1172 -0
  22. package/dist/world-extension.js +1 -1
  23. package/package.json +46 -3
  24. package/ui/brainbase-tokens.css +35 -35
  25. package/ui/local-web-shell.css +20 -17
  26. package/ui/local-web-shell.js +34 -1
  27. package/ui/meeting-minutes-lineage-http.js +247 -0
  28. package/ui/meeting-minutes-lineage.css +177 -0
  29. package/ui/meeting-minutes-lineage.js +485 -0
  30. package/ui/meeting-minutes-storage.css +170 -0
  31. package/ui/meeting-minutes-storage.js +388 -0
  32. package/ui/meeting-minutes.css +215 -0
  33. package/ui/meeting-minutes.js +622 -0
  34. package/ui/world/world-district.js +306 -87
  35. package/ui/world/world-scenery.js +208 -0
  36. package/ui/world/world-view.css +54 -0
  37. package/ui/world/world-view.js +65 -205
@@ -0,0 +1,177 @@
1
+ .bb-mml {
2
+ display: grid;
3
+ gap: 16px;
4
+ min-width: 0;
5
+ }
6
+
7
+ .bb-mml-heading {
8
+ display: grid;
9
+ gap: 4px;
10
+ }
11
+
12
+ .bb-mml-title {
13
+ margin: 0;
14
+ color: var(--bb-color-ink, #17202a);
15
+ font-size: 18px;
16
+ }
17
+
18
+ .bb-mml-evidence,
19
+ .bb-mml-notice,
20
+ .bb-mml-empty,
21
+ .bb-mml-action-hint {
22
+ margin: 0;
23
+ color: var(--bb-color-muted, #68727d);
24
+ font-size: 12px;
25
+ line-height: 1.55;
26
+ }
27
+
28
+ .bb-mml-notice {
29
+ padding: 10px 12px;
30
+ border: 1px solid var(--bb-color-border, #d8dde3);
31
+ border-radius: 8px;
32
+ background: var(--bb-color-panel-subtle, #f7f8fa);
33
+ }
34
+
35
+ .bb-mml-candidates {
36
+ display: grid;
37
+ gap: 10px;
38
+ }
39
+
40
+ .bb-mml-candidate-form {
41
+ display: grid;
42
+ gap: 10px;
43
+ padding: 14px;
44
+ border: 1px solid var(--bb-color-border, #d8dde3);
45
+ border-radius: 10px;
46
+ background: var(--bb-color-panel-subtle, #f7f8fa);
47
+ }
48
+
49
+ .bb-mml-form-title,
50
+ .bb-mml-form-subtitle {
51
+ margin: 0;
52
+ color: var(--bb-color-ink, #17202a);
53
+ font-size: 14px;
54
+ }
55
+
56
+ .bb-mml-form-subtitle {
57
+ font-size: 12px;
58
+ }
59
+
60
+ .bb-mml-form-help,
61
+ .bb-mml-form-status {
62
+ margin: 0;
63
+ color: var(--bb-color-muted, #68727d);
64
+ font-size: 12px;
65
+ line-height: 1.55;
66
+ }
67
+
68
+ .bb-mml-form-group {
69
+ display: grid;
70
+ gap: 8px;
71
+ padding-top: 4px;
72
+ }
73
+
74
+ .bb-mml-field {
75
+ display: grid;
76
+ gap: 4px;
77
+ color: var(--bb-color-muted, #68727d);
78
+ font-size: 12px;
79
+ }
80
+
81
+ .bb-mml-field-label {
82
+ font-weight: 600;
83
+ }
84
+
85
+ .bb-mml-input {
86
+ box-sizing: border-box;
87
+ width: 100%;
88
+ min-height: 32px;
89
+ padding: 6px 8px;
90
+ border: 1px solid var(--bb-color-border-strong, #b8c0c9);
91
+ border-radius: 6px;
92
+ background: var(--bb-color-panel, #fff);
93
+ color: var(--bb-color-ink, #17202a);
94
+ font: inherit;
95
+ font-size: 12px;
96
+ }
97
+
98
+ textarea.bb-mml-input {
99
+ min-height: 56px;
100
+ resize: vertical;
101
+ }
102
+
103
+ .bb-mml-candidate {
104
+ display: grid;
105
+ gap: 10px;
106
+ padding: 14px;
107
+ border: 1px solid var(--bb-color-border, #d8dde3);
108
+ border-radius: 10px;
109
+ background: var(--bb-color-panel, #fff);
110
+ }
111
+
112
+ .bb-mml-candidate-title {
113
+ margin: 0;
114
+ color: var(--bb-color-ink, #17202a);
115
+ font-size: 14px;
116
+ overflow-wrap: anywhere;
117
+ }
118
+
119
+ .bb-mml-statuses,
120
+ .bb-mml-candidate-actions {
121
+ display: flex;
122
+ flex-wrap: wrap;
123
+ align-items: center;
124
+ gap: 6px;
125
+ }
126
+
127
+ .bb-mml-status {
128
+ display: inline-flex;
129
+ align-items: center;
130
+ min-height: 24px;
131
+ padding: 3px 8px;
132
+ border: 1px solid var(--bb-color-border, #d8dde3);
133
+ border-radius: 999px;
134
+ color: var(--bb-color-muted, #68727d);
135
+ font-size: 11px;
136
+ }
137
+
138
+ .bb-mml-status-confirmed,
139
+ .bb-mml-status-adopted,
140
+ .bb-mml-status-accepted,
141
+ .bb-mml-status-actual {
142
+ border-color: var(--bb-color-accent, #84a98c);
143
+ color: var(--bb-color-accent-strong, #2f6f44);
144
+ }
145
+
146
+ .bb-mml-status-review_required {
147
+ border-color: var(--bb-color-warning, #d89b35);
148
+ color: var(--bb-color-warning-strong, #8a5a00);
149
+ }
150
+
151
+ .bb-mml-action {
152
+ min-height: 32px;
153
+ padding: 6px 10px;
154
+ border: 1px solid var(--bb-color-border-strong, #b8c0c9);
155
+ border-radius: 6px;
156
+ background: var(--bb-color-panel, #fff);
157
+ color: var(--bb-color-ink, #17202a);
158
+ cursor: pointer;
159
+ font: inherit;
160
+ font-size: 12px;
161
+ }
162
+
163
+ .bb-mml-action:hover,
164
+ .bb-mml-action:focus-visible {
165
+ border-color: var(--bb-color-accent, #84a98c);
166
+ outline: 2px solid var(--bb-color-accent-soft, #dfeee2);
167
+ outline-offset: 1px;
168
+ }
169
+
170
+ .bb-mml-corrections {
171
+ color: var(--bb-color-muted, #68727d);
172
+ font-size: 12px;
173
+ }
174
+
175
+ .bb-mml-corrections p {
176
+ margin: 8px 0 0;
177
+ }
@@ -0,0 +1,485 @@
1
+ /*
2
+ * Meeting-minutes lineage is rendered as a projection of the core ledger.
3
+ * The host supplies the actions; this module never creates judgment, Task, or
4
+ * execution records by itself. That keeps the screen usable with any
5
+ * minutes provider while preserving the existing adoption and Task owners.
6
+ */
7
+
8
+ export const MEETING_MINUTES_LINEAGE_UI_CONTRACT_VERSION = 'brainbase.meeting-minutes-lineage-ui.v1';
9
+
10
+ const STATUS_LABELS = Object.freeze({
11
+ unconfirmed: '未確認',
12
+ confirmed: '確認済み',
13
+ not_adopted: '未採用',
14
+ adopted: '採用済み',
15
+ unrecorded: '未記録',
16
+ actual: '実行記録あり',
17
+ accepted: '結果確認済み',
18
+ clear: '修正なし',
19
+ review_required: '修正後の再確認が必要',
20
+ });
21
+
22
+ function isRecord(value) {
23
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+
26
+ function text(value) {
27
+ return typeof value === 'string' ? value.trim() : '';
28
+ }
29
+
30
+ function textLines(value) {
31
+ if (Array.isArray(value)) return value.map((line) => text(line)).filter(Boolean).join('\n');
32
+ return text(value);
33
+ }
34
+
35
+ function status(value, allowed, fallback) {
36
+ return allowed.includes(value) ? value : fallback;
37
+ }
38
+
39
+ function exactEvidence(value) {
40
+ if (!isRecord(value)) return null;
41
+ const meetingId = text(value.meetingId);
42
+ const minutesId = text(value.minutesId);
43
+ const versionId = text(value.versionId);
44
+ const provenance = isRecord(value.provenance) ? value.provenance : null;
45
+ if (!meetingId || !minutesId || !versionId || !provenance || !text(provenance.providerKind) || !text(provenance.providerId)) return null;
46
+ return {
47
+ meetingId,
48
+ minutesId,
49
+ versionId,
50
+ contentDigest: value.contentDigest === null ? null : text(value.contentDigest) || null,
51
+ locator: value.locator ?? null,
52
+ provenance: {
53
+ providerKind: text(provenance.providerKind),
54
+ providerId: text(provenance.providerId),
55
+ ...(text(provenance.revision) ? { revision: text(provenance.revision) } : {}),
56
+ ...(text(provenance.digest) ? { digest: text(provenance.digest) } : {}),
57
+ },
58
+ };
59
+ }
60
+
61
+ function candidateView(value) {
62
+ if (!isRecord(value) || !isRecord(value.candidate)) return null;
63
+ const candidate = value.candidate;
64
+ const id = text(candidate.id);
65
+ const kind = candidate.kind === 'judgment' || candidate.kind === 'task' ? candidate.kind : '';
66
+ if (!id || !kind) return null;
67
+ return {
68
+ ...value,
69
+ candidate: {
70
+ ...candidate,
71
+ id,
72
+ kind,
73
+ proposal: candidate.proposal ?? null,
74
+ evidence: exactEvidence(candidate.evidence),
75
+ },
76
+ confirmationStatus: status(value.confirmationStatus, ['unconfirmed', 'confirmed'], 'unconfirmed'),
77
+ adoptionStatus: status(value.adoptionStatus, ['not_adopted', 'adopted'], 'not_adopted'),
78
+ executionStatus: status(value.executionStatus, ['unrecorded', 'actual'], 'unrecorded'),
79
+ resultStatus: status(value.resultStatus, ['unrecorded', 'accepted'], 'unrecorded'),
80
+ reviewStatus: status(value.reviewStatus, ['clear', 'review_required'], 'clear'),
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Validate the core read model while preserving a non-success state.
86
+ * `[]` is a valid list only after the evidence and response shape are known.
87
+ */
88
+ export function normalizeMeetingMinutesLineageView(payload) {
89
+ if (!isRecord(payload)) return { status: 'invalid', reason: 'response_not_object' };
90
+ if (payload.status === 'unavailable') {
91
+ return { status: 'unavailable', reason: text(payload.reason) || 'lineage_unavailable' };
92
+ }
93
+ const evidence = exactEvidence(payload.evidence);
94
+ if (!evidence || !Array.isArray(payload.candidates) || !Array.isArray(payload.corrections)) {
95
+ return { status: 'invalid', reason: 'lineage_shape_invalid' };
96
+ }
97
+ const candidates = payload.candidates.map(candidateView).filter(Boolean);
98
+ const invalidCount = payload.candidates.length - candidates.length;
99
+ const corrections = payload.corrections.filter(isRecord).map((correction) => ({
100
+ id: text(correction.id) || null,
101
+ reason: text(correction.reason) || '議事録版が修正されました',
102
+ affectedCandidateIds: Array.isArray(correction.affectedCandidateIds)
103
+ ? correction.affectedCandidateIds.filter((id) => text(id)).map((id) => text(id))
104
+ : [],
105
+ }));
106
+ return { status: 'available', evidence, candidates, corrections, invalidCount };
107
+ }
108
+
109
+ function makeElement(doc, tag, className, content) {
110
+ const element = doc.createElement(tag);
111
+ if (className) element.className = className;
112
+ if (content !== undefined) element.textContent = content;
113
+ return element;
114
+ }
115
+
116
+ function append(parent, ...children) {
117
+ for (const child of children) if (child) parent.appendChild(child);
118
+ return parent;
119
+ }
120
+
121
+ function evidenceLabel(evidence) {
122
+ return `${evidence.provenance.providerKind}:${evidence.provenance.providerId} / ${evidence.versionId}`;
123
+ }
124
+
125
+ function proposalLabel(proposal) {
126
+ if (typeof proposal === 'string') return proposal;
127
+ if (isRecord(proposal)) {
128
+ const task = isRecord(proposal.task) ? proposal.task : (isRecord(proposal.taskData) ? proposal.taskData : null);
129
+ if (task) {
130
+ const title = text(task.title);
131
+ const description = text(task.description);
132
+ if (title || description) return [title, description].filter(Boolean).join(' — ');
133
+ }
134
+ const learning = isRecord(proposal.learningCandidate) ? proposal.learningCandidate : (isRecord(proposal.candidate) ? proposal.candidate : null);
135
+ if (learning) {
136
+ const proposedChange = proposalValueLabel(learning.proposedChange);
137
+ const grounds = proposalLinesLabel(learning.grounds);
138
+ if (proposedChange || grounds) return [proposedChange, grounds ? `根拠: ${grounds}` : ''].filter(Boolean).join(' / ');
139
+ }
140
+ for (const key of ['title', 'judgment', 'summary', 'description', 'text']) {
141
+ if (text(proposal[key])) return text(proposal[key]);
142
+ }
143
+ }
144
+ return '候補の内容を開く';
145
+ }
146
+
147
+ function proposalValueLabel(value) {
148
+ if (typeof value === 'string') return text(value);
149
+ if (!isRecord(value)) return '';
150
+ for (const key of ['title', 'label', 'judgment', 'summary', 'description', 'text']) {
151
+ if (text(value[key])) return text(value[key]);
152
+ }
153
+ return '';
154
+ }
155
+
156
+ function proposalLinesLabel(value) {
157
+ if (Array.isArray(value)) return value.map((line) => proposalValueLabel(line)).filter(Boolean).join(' / ');
158
+ return proposalValueLabel(value);
159
+ }
160
+
161
+ function statusBadge(doc, value) {
162
+ const label = STATUS_LABELS[value] || value;
163
+ const badge = makeElement(doc, 'span', `bb-mml-status bb-mml-status-${value}`, label);
164
+ badge.setAttribute('data-status', value);
165
+ return badge;
166
+ }
167
+
168
+ function actionButton(doc, label, action, candidate) {
169
+ if (typeof action !== 'function') return null;
170
+ const button = makeElement(doc, 'button', 'bb-mml-action', label);
171
+ button.type = 'button';
172
+ button.addEventListener('click', () => action(candidate));
173
+ return button;
174
+ }
175
+
176
+ function candidateActions(doc, item, actions) {
177
+ const actionsNode = makeElement(doc, 'div', 'bb-mml-candidate-actions');
178
+ const candidate = item.candidate;
179
+ if (item.reviewStatus === 'review_required') {
180
+ append(actionsNode, actionButton(doc, '修正を確認', actions.reviewCandidate, candidate));
181
+ } else if (item.confirmationStatus === 'unconfirmed') {
182
+ append(actionsNode, actionButton(doc, '確認する', actions.confirmCandidate, candidate));
183
+ } else if (item.adoptionStatus === 'not_adopted') {
184
+ append(actionsNode, actionButton(doc, candidate.kind === 'judgment' ? '判断として採用' : 'Taskとして採用', candidate.kind === 'judgment' ? actions.adoptJudgment : actions.adoptTask, candidate));
185
+ }
186
+ if (!actionsNode.children?.length) actionsNode.appendChild(makeElement(doc, 'span', 'bb-mml-action-hint', '操作は接続済みの正本から行います'));
187
+ return actionsNode;
188
+ }
189
+
190
+ function actionErrorMessage(error) {
191
+ if (error instanceof Error && error.message) return error.message;
192
+ if (isRecord(error) && text(error.message)) return text(error.message);
193
+ return '操作に失敗しました。再試行してください。';
194
+ }
195
+
196
+ function field(doc, label, { type = 'text', value = '', required = false, placeholder = '' } = {}) {
197
+ const wrapper = makeElement(doc, 'label', 'bb-mml-field');
198
+ wrapper.appendChild(makeElement(doc, 'span', 'bb-mml-field-label', label));
199
+ const input = makeElement(doc, type === 'textarea' ? 'textarea' : 'input', 'bb-mml-input');
200
+ if (type !== 'textarea') input.type = type;
201
+ input.value = value ?? '';
202
+ if (required) input.setAttribute('required', '');
203
+ if (placeholder) input.setAttribute('placeholder', placeholder);
204
+ wrapper.appendChild(input);
205
+ return { wrapper, input };
206
+ }
207
+
208
+ function selectField(doc, label, options, value) {
209
+ const wrapper = makeElement(doc, 'label', 'bb-mml-field');
210
+ wrapper.appendChild(makeElement(doc, 'span', 'bb-mml-field-label', label));
211
+ const input = makeElement(doc, 'select', 'bb-mml-input');
212
+ for (const option of options) {
213
+ const item = makeElement(doc, 'option', '', option.label);
214
+ item.value = option.value;
215
+ if (option.value === value) item.selected = true;
216
+ input.appendChild(item);
217
+ }
218
+ input.value = value ?? options[0]?.value ?? '';
219
+ wrapper.appendChild(input);
220
+ return { wrapper, input };
221
+ }
222
+
223
+ function valueOf(input) {
224
+ return typeof input?.value === 'string' ? input.value.trim() : '';
225
+ }
226
+
227
+ function linesOf(input, fallback = '') {
228
+ const value = valueOf(input);
229
+ const lines = value.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
230
+ return lines.length ? lines : (fallback ? [fallback] : []);
231
+ }
232
+
233
+ function setFieldsDisabled(fields, disabled) {
234
+ for (const fieldRef of fields) fieldRef.input.disabled = disabled;
235
+ }
236
+
237
+ function record(value) {
238
+ return isRecord(value) ? value : {};
239
+ }
240
+
241
+ function candidateForm(doc, actions, candidateDefaults, onCreated) {
242
+ if (typeof actions.createCandidate !== 'function') return null;
243
+ const defaults = record(candidateDefaults);
244
+ const taskDefaults = record(defaults.task);
245
+ const judgmentDefaults = record(defaults.judgment);
246
+ const sourceDefault = record(judgmentDefaults.sourceEvaluationRef ?? defaults.sourceEvaluationRef);
247
+ const targetDefault = record(judgmentDefaults.target ?? defaults.target);
248
+ const applicabilityDefault = record(judgmentDefaults.applicability ?? defaults.applicability);
249
+ const form = makeElement(doc, 'form', 'bb-mml-candidate-form');
250
+ form.appendChild(makeElement(doc, 'h3', 'bb-mml-form-title', 'この版から候補を作る'));
251
+ form.appendChild(makeElement(doc, 'p', 'bb-mml-form-help', '候補を作成した後、内容を確認してから正本へ採用します。'));
252
+
253
+ const kind = selectField(doc, '候補の種類', [
254
+ { value: 'task', label: 'Task' },
255
+ { value: 'judgment', label: '判断' },
256
+ ], text(defaults.kind) === 'judgment' ? 'judgment' : 'task');
257
+ form.appendChild(kind.wrapper);
258
+
259
+ const task = makeElement(doc, 'div', 'bb-mml-form-group');
260
+ task.appendChild(makeElement(doc, 'h4', 'bb-mml-form-subtitle', 'Taskの内容'));
261
+ const taskTitle = field(doc, 'Task名', { value: text(taskDefaults.title), required: true, placeholder: '例:次回会議の資料を確認する' });
262
+ const taskDescription = field(doc, '説明', { type: 'textarea', value: text(taskDefaults.description), placeholder: '必要な作業や完了条件' });
263
+ const taskPriority = field(doc, '優先度', { value: text(taskDefaults.priority), placeholder: '任意' });
264
+ const taskAssignee = field(doc, '担当者', { value: text(taskDefaults.assignee_person_id), placeholder: '任意' });
265
+ const taskDue = field(doc, '期限', { type: 'datetime-local', value: text(taskDefaults.due_at) });
266
+ for (const item of [taskTitle, taskDescription, taskPriority, taskAssignee, taskDue]) task.appendChild(item.wrapper);
267
+ form.appendChild(task);
268
+
269
+ const judgment = makeElement(doc, 'div', 'bb-mml-form-group');
270
+ judgment.appendChild(makeElement(doc, 'h4', 'bb-mml-form-subtitle', '判断の内容'));
271
+ const sourceId = field(doc, '評価の識別子', { value: text(sourceDefault.id), required: true, placeholder: '既存の評価ID' });
272
+ const sourceDigest = field(doc, '評価のダイジェスト', { value: text(sourceDefault.digest), required: true, placeholder: 'sha256:...' });
273
+ const targetKind = selectField(doc, '対象の種類', [
274
+ { value: 'world_model', label: 'World Model' },
275
+ { value: 'judgment_method', label: '判断方法' },
276
+ { value: 'execution_method', label: '実行方法' },
277
+ { value: 'objective', label: '目的' },
278
+ ], text(targetDefault.kind) || 'world_model');
279
+ const targetId = field(doc, '対象の識別子', { value: text(targetDefault.id), required: true, placeholder: '対象ID' });
280
+ const targetRevision = field(doc, '対象の版', { value: text(targetDefault.revision), required: true, placeholder: '1' });
281
+ const targetDigest = field(doc, '対象のダイジェスト', { value: text(targetDefault.digest), required: true, placeholder: 'sha256:...' });
282
+ const proposedChange = field(doc, '判断の提案', { type: 'textarea', value: text(judgmentDefaults.proposedChange ?? defaults.proposedChange), required: true, placeholder: '何をどう判断・変更するか' });
283
+ const grounds = field(doc, '根拠(1行1件)', { type: 'textarea', value: textLines(judgmentDefaults.grounds ?? defaults.grounds), required: true, placeholder: '議事録から確認できる根拠' });
284
+ const counterexamples = field(doc, '反例・留保(1行1件)', { type: 'textarea', value: textLines(judgmentDefaults.counterexamples ?? defaults.counterexamples), required: true, placeholder: '分からない点も含めて記入' });
285
+ const uncertainty = field(doc, '不確実性(1行1件)', { type: 'textarea', value: textLines(judgmentDefaults.uncertainty ?? defaults.uncertainty), required: true, placeholder: '判断を見直す条件' });
286
+ const subjectIds = field(doc, '対象範囲(1行1件)', { type: 'textarea', value: textLines(applicabilityDefault.subjectIds ?? defaults.subjectIds ?? 'self'), required: true, placeholder: 'self' });
287
+ const validFrom = field(doc, '適用開始', { type: 'datetime-local', value: text(applicabilityDefault.validFrom ?? defaults.validFrom) });
288
+ const validUntil = field(doc, '適用終了', { type: 'datetime-local', value: text(applicabilityDefault.validUntil ?? defaults.validUntil) });
289
+ for (const item of [sourceId, sourceDigest, targetKind, targetId, targetRevision, targetDigest, proposedChange, grounds, counterexamples, uncertainty, subjectIds, validFrom, validUntil]) judgment.appendChild(item.wrapper);
290
+ form.appendChild(judgment);
291
+
292
+ const status = makeElement(doc, 'p', 'bb-mml-form-status', '');
293
+ const submit = makeElement(doc, 'button', 'bb-mml-action bb-mml-submit', '候補を作成');
294
+ submit.type = 'submit';
295
+ form.appendChild(submit);
296
+ form.appendChild(status);
297
+
298
+ const setMode = () => {
299
+ const isJudgment = valueOf(kind.input) === 'judgment';
300
+ task.hidden = isJudgment;
301
+ judgment.hidden = !isJudgment;
302
+ setFieldsDisabled([taskTitle, taskDescription, taskPriority, taskAssignee, taskDue], isJudgment);
303
+ setFieldsDisabled([sourceId, sourceDigest, targetKind, targetId, targetRevision, targetDigest, proposedChange, grounds, counterexamples, uncertainty, subjectIds, validFrom, validUntil], !isJudgment);
304
+ task.setAttribute('aria-hidden', isJudgment ? 'true' : 'false');
305
+ judgment.setAttribute('aria-hidden', isJudgment ? 'false' : 'true');
306
+ };
307
+ kind.input.addEventListener('change', setMode);
308
+ setMode();
309
+
310
+ form.addEventListener('submit', async (event) => {
311
+ event?.preventDefault?.();
312
+ status.textContent = '';
313
+ submit.disabled = true;
314
+ const kindValue = valueOf(kind.input) === 'judgment' ? 'judgment' : 'task';
315
+ const proposal = kindValue === 'task'
316
+ ? {
317
+ task: {
318
+ title: valueOf(taskTitle.input),
319
+ ...(valueOf(taskDescription.input) ? { description: valueOf(taskDescription.input) } : {}),
320
+ ...(valueOf(taskPriority.input) ? { priority: valueOf(taskPriority.input) } : {}),
321
+ ...(valueOf(taskAssignee.input) ? { assignee_person_id: valueOf(taskAssignee.input) } : {}),
322
+ ...(valueOf(taskDue.input) ? { due_at: valueOf(taskDue.input) } : {}),
323
+ },
324
+ }
325
+ : {
326
+ learningCandidate: {
327
+ sourceEvaluationRef: { id: valueOf(sourceId.input), digest: valueOf(sourceDigest.input) },
328
+ target: {
329
+ kind: valueOf(targetKind.input),
330
+ id: valueOf(targetId.input),
331
+ revision: valueOf(targetRevision.input),
332
+ digest: valueOf(targetDigest.input),
333
+ },
334
+ proposedChange: valueOf(proposedChange.input),
335
+ grounds: linesOf(grounds.input),
336
+ counterexamples: linesOf(counterexamples.input),
337
+ uncertainty: linesOf(uncertainty.input),
338
+ applicability: {
339
+ subjectIds: linesOf(subjectIds.input, 'self'),
340
+ validFrom: valueOf(validFrom.input) || new Date().toISOString(),
341
+ ...(valueOf(validUntil.input) ? { validUntil: valueOf(validUntil.input) } : {}),
342
+ },
343
+ },
344
+ learningValidation: {
345
+ findings: [],
346
+ conclusion: 'indeterminate',
347
+ modelDisposition: 'indeterminate',
348
+ basis: '議事録版から作成した候補。確認後に採用します。',
349
+ validatedAt: new Date().toISOString(),
350
+ },
351
+ };
352
+ try {
353
+ const created = await actions.createCandidate({ kind: kindValue, proposal, epistemicStatus: 'inferred' });
354
+ status.textContent = `候補を作成しました(${created?.id ?? 'ID確認中'})。内容を確認して採用できます。`;
355
+ await onCreated?.(created);
356
+ } catch (error) {
357
+ status.textContent = error instanceof Error ? error.message : '候補を作成できませんでした。';
358
+ submit.disabled = false;
359
+ }
360
+ });
361
+ return form;
362
+ }
363
+
364
+ function renderUnavailable(root, normalized, doc) {
365
+ const notice = makeElement(doc, 'p', 'bb-mml-notice', normalized.status === 'unavailable'
366
+ ? '議事録の判断履歴を読み取れません。権限または保存先を確認してください。'
367
+ : '議事録の判断履歴の形式を確認できません。');
368
+ notice.setAttribute('data-lineage-status', normalized.status);
369
+ root.appendChild(notice);
370
+ return root;
371
+ }
372
+
373
+ /** Render one lineage projection into a host-owned slot. */
374
+ export function renderMeetingMinutesLineage(root, payload, { actions = {}, candidateDefaults = null, onCandidateCreated, actionError = null, documentRef = globalThis.document } = {}) {
375
+ if (!root || !documentRef || typeof documentRef.createElement !== 'function') throw new TypeError('root and documentRef are required');
376
+ const normalized = normalizeMeetingMinutesLineageView(payload);
377
+ while (root.firstChild) root.removeChild(root.firstChild);
378
+ root.className = `${root.className || ''} bb-mml`.trim();
379
+ if (normalized.status !== 'available') return renderUnavailable(root, normalized, documentRef);
380
+
381
+ const heading = makeElement(documentRef, 'div', 'bb-mml-heading');
382
+ append(heading,
383
+ makeElement(documentRef, 'h2', 'bb-mml-title', '議事録からの判断・Task'),
384
+ makeElement(documentRef, 'p', 'bb-mml-evidence', `根拠: ${evidenceLabel(normalized.evidence)}`));
385
+ root.appendChild(heading);
386
+
387
+ if (actionError) {
388
+ const notice = makeElement(documentRef, 'p', 'bb-mml-notice', `操作に失敗しました。${actionErrorMessage(actionError)} 再試行できます。`);
389
+ notice.setAttribute('data-lineage-action-status', 'error');
390
+ root.appendChild(notice);
391
+ }
392
+
393
+ if (normalized.invalidCount > 0) {
394
+ root.appendChild(makeElement(documentRef, 'p', 'bb-mml-notice', `${normalized.invalidCount}件の候補は形式を確認できないため表示していません。`));
395
+ }
396
+ const form = candidateForm(documentRef, actions, candidateDefaults, onCandidateCreated);
397
+ if (form) root.appendChild(form);
398
+ const list = makeElement(documentRef, 'div', 'bb-mml-candidates');
399
+ if (!normalized.candidates.length) {
400
+ list.appendChild(makeElement(documentRef, 'p', 'bb-mml-empty', 'この議事録版から作られた候補はありません。'));
401
+ }
402
+ for (const item of normalized.candidates) {
403
+ const row = makeElement(documentRef, 'article', 'bb-mml-candidate');
404
+ row.setAttribute('data-candidate-id', item.candidate.id);
405
+ const title = makeElement(documentRef, 'h3', 'bb-mml-candidate-title', `${item.candidate.kind === 'judgment' ? '判断' : 'Task'}: ${proposalLabel(item.candidate.proposal)}`);
406
+ const statuses = makeElement(documentRef, 'div', 'bb-mml-statuses');
407
+ append(statuses,
408
+ statusBadge(documentRef, item.confirmationStatus),
409
+ statusBadge(documentRef, item.adoptionStatus),
410
+ statusBadge(documentRef, item.executionStatus),
411
+ statusBadge(documentRef, item.resultStatus),
412
+ statusBadge(documentRef, item.reviewStatus));
413
+ append(row, title, statuses, candidateActions(documentRef, item, actions));
414
+ list.appendChild(row);
415
+ }
416
+ root.appendChild(list);
417
+ if (normalized.corrections.length) {
418
+ const corrections = makeElement(documentRef, 'details', 'bb-mml-corrections');
419
+ corrections.appendChild(makeElement(documentRef, 'summary', '', `修正履歴 (${normalized.corrections.length})`));
420
+ for (const correction of normalized.corrections) corrections.appendChild(makeElement(documentRef, 'p', '', correction.reason));
421
+ root.appendChild(corrections);
422
+ }
423
+ return root;
424
+ }
425
+
426
+ /** Host adapter: the core owns loading and action implementations. */
427
+ export function createMeetingMinutesLineageView({ root, load, actions = {}, candidateDefaults = null, documentRef = globalThis.document } = {}) {
428
+ let state = { status: 'unavailable', reason: 'not_loaded' };
429
+ let actionError = null;
430
+ let wrappedActions;
431
+ const renderCurrent = () => renderMeetingMinutesLineage(root, state, {
432
+ actions: wrappedActions,
433
+ candidateDefaults,
434
+ onCandidateCreated: () => controller.refresh(),
435
+ actionError,
436
+ documentRef,
437
+ });
438
+ const runAction = async (action, candidate) => {
439
+ try {
440
+ await action(candidate);
441
+ actionError = null;
442
+ await controller.refresh();
443
+ } catch (error) {
444
+ actionError = error;
445
+ renderCurrent();
446
+ }
447
+ };
448
+ wrappedActions = { ...actions };
449
+ for (const name of ['confirmCandidate', 'adoptJudgment', 'adoptTask', 'reviewCandidate']) {
450
+ if (typeof actions[name] === 'function') {
451
+ wrappedActions[name] = (candidate) => runAction(actions[name], candidate);
452
+ }
453
+ }
454
+ const controller = {
455
+ get state() { return state; },
456
+ render(payload = state) {
457
+ actionError = null;
458
+ state = normalizeMeetingMinutesLineageView(payload);
459
+ renderCurrent();
460
+ return state;
461
+ },
462
+ async refresh() {
463
+ if (typeof load !== 'function') {
464
+ state = { status: 'unavailable', reason: 'loader_not_connected' };
465
+ renderCurrent();
466
+ return state;
467
+ }
468
+ try {
469
+ const payload = await load();
470
+ return controller.render(payload);
471
+ } catch (error) {
472
+ state = {
473
+ status: 'unavailable',
474
+ reason: error && typeof error.code === 'string' ? error.code : 'lineage_unavailable',
475
+ };
476
+ renderCurrent();
477
+ return state;
478
+ }
479
+ },
480
+ };
481
+ controller.render(state);
482
+ return controller;
483
+ }
484
+
485
+ export default renderMeetingMinutesLineage;