@unson/brainbase-mcp 0.5.0 → 0.7.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 (92) hide show
  1. package/README.md +8 -0
  2. package/contracts/judgment-dag/digest.json +2 -2
  3. package/dist/canonical-task-contract.d.ts +9 -0
  4. package/dist/canonical-task-contract.js +39 -0
  5. package/dist/canonical-task-principal.d.ts +16 -0
  6. package/dist/canonical-task-principal.js +36 -0
  7. package/dist/canonical-task-service.d.ts +251 -0
  8. package/dist/canonical-task-service.js +809 -0
  9. package/dist/company-os-evaluation.d.ts +193 -0
  10. package/dist/company-os-evaluation.js +1150 -0
  11. package/dist/company-os-impact-review.d.ts +248 -0
  12. package/dist/company-os-impact-review.js +663 -0
  13. package/dist/company-os-learning-adoption.d.ts +288 -0
  14. package/dist/company-os-learning-adoption.js +944 -0
  15. package/dist/company-os-objectives.d.ts +52 -0
  16. package/dist/company-os-objectives.js +197 -0
  17. package/dist/company-os-receipt-adapter.d.ts +109 -0
  18. package/dist/company-os-receipt-adapter.js +636 -0
  19. package/dist/constraint-exception-store.d.ts +23 -0
  20. package/dist/constraint-exception-store.js +108 -0
  21. package/dist/constraint-resolution.d.ts +175 -0
  22. package/dist/constraint-resolution.js +922 -0
  23. package/dist/decision-adapter.d.ts +145 -0
  24. package/dist/decision-adapter.js +586 -0
  25. package/dist/durable-wait-http.d.ts +51 -0
  26. package/dist/durable-wait-http.js +497 -0
  27. package/dist/durable-waits.d.ts +234 -0
  28. package/dist/durable-waits.js +944 -0
  29. package/dist/execution-authority-http.d.ts +28 -0
  30. package/dist/execution-authority-http.js +359 -0
  31. package/dist/execution-authority.d.ts +201 -0
  32. package/dist/execution-authority.js +634 -0
  33. package/dist/foundation-catalog.d.ts +26 -0
  34. package/dist/foundation-catalog.js +206 -0
  35. package/dist/foundation-constraint-store.d.ts +50 -0
  36. package/dist/foundation-constraint-store.js +215 -0
  37. package/dist/foundation-http.d.ts +90 -0
  38. package/dist/foundation-http.js +439 -0
  39. package/dist/foundation-store.d.ts +132 -0
  40. package/dist/foundation-store.js +654 -0
  41. package/dist/judgment-dag-composition.d.ts +224 -0
  42. package/dist/judgment-dag-composition.js +731 -0
  43. package/dist/judgment-dag.d.ts +2 -0
  44. package/dist/judgment-dag.js +1 -0
  45. package/dist/judgment-problem-snapshot.d.ts +185 -0
  46. package/dist/judgment-problem-snapshot.js +777 -0
  47. package/dist/judgment-view-http.d.ts +22 -0
  48. package/dist/judgment-view-http.js +128 -0
  49. package/dist/judgment-view.d.ts +222 -0
  50. package/dist/judgment-view.js +627 -0
  51. package/dist/knowledge-adapter.d.ts +112 -0
  52. package/dist/knowledge-adapter.js +691 -0
  53. package/dist/ontology-foundation.d.ts +209 -0
  54. package/dist/ontology-foundation.js +1000 -0
  55. package/dist/ontology.d.ts +13 -0
  56. package/dist/ontology.js +14 -0
  57. package/dist/organization-connection.d.ts +164 -0
  58. package/dist/organization-connection.js +428 -0
  59. package/dist/personal-knowledge.d.ts +120 -0
  60. package/dist/personal-knowledge.js +750 -0
  61. package/dist/problem-candidates.d.ts +149 -0
  62. package/dist/problem-candidates.js +898 -0
  63. package/dist/problem-selection.d.ts +238 -0
  64. package/dist/problem-selection.js +995 -0
  65. package/dist/resource-reservation-http.d.ts +31 -0
  66. package/dist/resource-reservation-http.js +371 -0
  67. package/dist/resource-reservations.d.ts +221 -0
  68. package/dist/resource-reservations.js +577 -0
  69. package/dist/server.d.ts +97 -0
  70. package/dist/server.js +180 -1
  71. package/dist/ssot.d.ts +6 -1
  72. package/dist/ssot.js +63 -5
  73. package/dist/streamable-http.d.ts +42 -0
  74. package/dist/streamable-http.js +288 -0
  75. package/dist/types.d.ts +20 -0
  76. package/dist/world-model.d.ts +181 -0
  77. package/dist/world-model.js +873 -0
  78. package/package.json +145 -3
  79. package/ui/icons/mana/LICENSE.tabler.txt +21 -0
  80. package/ui/icons/mana/affiliate.svg +20 -0
  81. package/ui/icons/mana/brand-github.svg +15 -0
  82. package/ui/icons/mana/brand-google-drive.svg +17 -0
  83. package/ui/icons/mana/brand-slack.svg +18 -0
  84. package/ui/icons/mana/cpu.svg +24 -0
  85. package/ui/judgment-view.css +238 -0
  86. package/ui/judgment-view.js +508 -0
  87. package/ui/objective-editor.css +180 -0
  88. package/ui/objective-editor.js +1139 -0
  89. package/ui/outcome-knowledge.css +365 -0
  90. package/ui/outcome-knowledge.js +1570 -0
  91. package/ui/outcome-mana.css +255 -0
  92. package/ui/outcome-mana.js +2325 -0
@@ -0,0 +1,1139 @@
1
+ /*
2
+ * Brainbase Objective editor.
3
+ *
4
+ * The OSS package owns this screen and its browser-side state only. A host
5
+ * supplies the ObjectiveEditorPort below; the module never fetches a BFF,
6
+ * opens a database, or decides organization membership/RACI. An organization
7
+ * application can compose the same screen by adapting its authenticated
8
+ * FoundationRevisionStore boundary to the port.
9
+ */
10
+
11
+ export const OBJECTIVE_EDITOR_CONTRACT_VERSION = 'brainbase.objective-editor.v1';
12
+
13
+ export const OBJECTIVE_EDITOR_STORY_IDS = Object.freeze([
14
+ 'story-company-os-objective-editor-v1',
15
+ ]);
16
+
17
+ export const OBJECTIVE_EDITOR_RELATION_TYPES = Object.freeze([
18
+ ['contributes_to', '目的への貢献'],
19
+ ['execution_depends_on', '実行の依存'],
20
+ ['time_condition', '時間の条件'],
21
+ ]);
22
+
23
+ const UNKNOWN = 'unknown';
24
+
25
+ const STATUS_LABELS = Object.freeze({
26
+ idle: '未取得',
27
+ loading: '読み込み中',
28
+ saving: '保存中',
29
+ ready: '取得済み',
30
+ empty: '登録なし',
31
+ draft: '下書き',
32
+ judgment_available: '判断に利用可能',
33
+ judgment_unknown: '判断利用可否 未確認',
34
+ permission_denied: '権限不足',
35
+ api_unavailable: 'API未提供',
36
+ missing: '欠損',
37
+ conflict: '版の競合',
38
+ saved_unverified: '保存済み・未確認',
39
+ verified: '正本と一致',
40
+ error_retryable: '取得失敗',
41
+ unknown: '確認できません',
42
+ });
43
+
44
+ const RELATION_LABELS = Object.freeze(Object.fromEntries(OBJECTIVE_EDITOR_RELATION_TYPES));
45
+
46
+ function getDocument() {
47
+ if (typeof document === 'undefined' || !document?.createElement) throw new Error('document_unavailable');
48
+ return document;
49
+ }
50
+
51
+ function makeElement(tag, options = {}) {
52
+ const element = getDocument().createElement(tag);
53
+ if (options.className) element.className = options.className;
54
+ if (options.text !== undefined && options.text !== null) element.textContent = String(options.text);
55
+ for (const [name, value] of Object.entries(options.attrs ?? {})) {
56
+ if (value === undefined || value === null || value === false) continue;
57
+ element.setAttribute(name, value === true ? '' : String(value));
58
+ }
59
+ if (options.value !== undefined && 'value' in element) element.value = String(options.value);
60
+ if (options.checked !== undefined && 'checked' in element) element.checked = Boolean(options.checked);
61
+ if (options.disabled !== undefined) element.disabled = Boolean(options.disabled);
62
+ if (options.hidden !== undefined) element.hidden = Boolean(options.hidden);
63
+ return element;
64
+ }
65
+
66
+ function append(parent, ...children) {
67
+ parent.append(...children.filter((child) => child !== null && child !== undefined));
68
+ return parent;
69
+ }
70
+
71
+ function clear(parent) {
72
+ parent.replaceChildren();
73
+ return parent;
74
+ }
75
+
76
+ function optionalText(value) {
77
+ if (value === null || value === undefined) return '';
78
+ return typeof value === 'string' ? value : String(value);
79
+ }
80
+
81
+ function nonEmptyText(value) {
82
+ const result = optionalText(value).trim();
83
+ return result || null;
84
+ }
85
+
86
+ function text(value, fallback = '未確認') {
87
+ const result = nonEmptyText(value);
88
+ return result ?? fallback;
89
+ }
90
+
91
+ function asArray(value) {
92
+ return Array.isArray(value) ? value : null;
93
+ }
94
+
95
+ function objectValue(value) {
96
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
97
+ }
98
+
99
+ function firstValue(source, ...keys) {
100
+ const object = objectValue(source);
101
+ if (!object) return undefined;
102
+ for (const key of keys) {
103
+ if (Object.hasOwn(object, key) && object[key] !== undefined && object[key] !== null) return object[key];
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ function payloadValue(payload, ...keys) {
109
+ const object = objectValue(payload);
110
+ if (!object) return undefined;
111
+ for (const key of keys) {
112
+ if (Object.hasOwn(object, key) && object[key] !== undefined && object[key] !== null) return object[key];
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function normalizeState(value, fallback = UNKNOWN) {
118
+ const state = optionalText(value).trim().toLowerCase();
119
+ if (!state) return fallback;
120
+ if (['forbidden', 'authorization_denied', 'permission_insufficient', 'not_allowed'].includes(state)) return 'permission_denied';
121
+ if (['unavailable', 'upstream_unavailable', 'api_not_implemented', 'not_implemented'].includes(state)) return 'api_unavailable';
122
+ if (['version_conflict', 'revision_conflict', 'cas_conflict'].includes(state)) return 'conflict';
123
+ if (['persisted', 'saved'].includes(state)) return 'saved_unverified';
124
+ if (['readback_verified', 'committed_verified'].includes(state)) return 'verified';
125
+ return state;
126
+ }
127
+
128
+ const EXPLICIT_COLLECTION_FAILURES = new Set(['permission_denied', 'api_unavailable', 'missing', 'conflict']);
129
+
130
+ function explicitCollectionFailure(state, key) {
131
+ if (!EXPLICIT_COLLECTION_FAILURES.has(state)) return null;
132
+ return { state, [key]: null, absence_confirmed: false };
133
+ }
134
+
135
+ function normalizeRevision(value) {
136
+ const revision = nonEmptyText(value);
137
+ return revision && /^[1-9]\d*$/.test(revision) ? revision : revision;
138
+ }
139
+
140
+ function normalizeReference(value, defaultType = null) {
141
+ const object = objectValue(value);
142
+ if (!object) return null;
143
+ const id = nonEmptyText(firstValue(object, 'id', 'resourceId', 'resource_id'));
144
+ const type = nonEmptyText(firstValue(object, 'type', 'resourceType', 'resource_type')) ?? defaultType;
145
+ const revision = normalizeRevision(firstValue(object, 'revision', 'version'));
146
+ if (!id || !type || !revision) return null;
147
+ return {
148
+ id,
149
+ type,
150
+ revision,
151
+ digest: nonEmptyText(firstValue(object, 'digest', 'hash')),
152
+ meaning: nonEmptyText(firstValue(object, 'meaning', 'label', 'name')),
153
+ raw: object,
154
+ };
155
+ }
156
+
157
+ function normalizeVariableRef(value) {
158
+ const reference = normalizeReference(value, 'variable');
159
+ if (!reference || reference.type !== 'variable') return null;
160
+ return { id: reference.id, type: 'variable', revision: reference.revision };
161
+ }
162
+
163
+ function normalizeCriterion(value) {
164
+ const object = objectValue(value);
165
+ if (!object) return null;
166
+ const variableRef = normalizeVariableRef(firstValue(object, 'variableRef', 'variable_ref', 'variable'));
167
+ const operator = nonEmptyText(firstValue(object, 'operator', 'comparison'));
168
+ if (!variableRef || !operator) return null;
169
+ const criterion = { variableRef, operator };
170
+ if (Object.hasOwn(object, 'target')) criterion.target = object.target;
171
+ return criterion;
172
+ }
173
+
174
+ function normalizePeriod(value) {
175
+ const object = objectValue(value);
176
+ if (!object) return null;
177
+ return {
178
+ from: nonEmptyText(firstValue(object, 'from', 'validFrom', 'valid_from', 'start')),
179
+ until: nonEmptyText(firstValue(object, 'until', 'validUntil', 'valid_until', 'end')),
180
+ };
181
+ }
182
+
183
+ function dateTimeLocalValue(value) {
184
+ const input = nonEmptyText(value);
185
+ if (!input) return '';
186
+ const date = new Date(input);
187
+ if (Number.isNaN(date.getTime())) return input;
188
+ const pad = (part) => String(part).padStart(2, '0');
189
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
190
+ }
191
+
192
+ function canonicalDateTimeValue(value) {
193
+ const input = nonEmptyText(value);
194
+ if (!input) return '';
195
+ const date = new Date(input);
196
+ return Number.isNaN(date.getTime()) ? input : date.toISOString();
197
+ }
198
+
199
+ function normalizeReadiness(value) {
200
+ const object = objectValue(value);
201
+ if (!object || typeof object.ready !== 'boolean') return { state: 'unknown', ready: null, issues: null };
202
+ const issues = asArray(object.issues);
203
+ return {
204
+ state: 'ready',
205
+ ready: object.ready,
206
+ issues: issues ? issues.map((issue) => ({
207
+ code: text(firstValue(issue, 'code'), 'UNKNOWN'),
208
+ path: text(firstValue(issue, 'path'), 'definition'),
209
+ message: text(firstValue(issue, 'message'), '利用条件を確認できません。'),
210
+ })) : [],
211
+ };
212
+ }
213
+
214
+ function recordPayload(payload) {
215
+ const object = objectValue(payload);
216
+ if (!object) return null;
217
+ const nestedRecord = objectValue(payloadValue(object, 'objective', 'record', 'data'));
218
+ if (nestedRecord) return nestedRecord;
219
+ // A canonical FoundationCatalogRecord carries metadata (digest/readiness)
220
+ // beside `definition`; keep the record wrapper so that metadata is not
221
+ // mistaken for an absent readback.
222
+ if (objectValue(object.definition)) return object;
223
+ return objectValue(payloadValue(object, 'definition')) ?? object;
224
+ }
225
+
226
+ /**
227
+ * Converts a canonical FoundationCatalogRecord (or a host projection of it)
228
+ * without filling missing fields with guessed values.
229
+ */
230
+ export function normalizeObjectiveRecord(payload) {
231
+ const source = recordPayload(payload);
232
+ if (!source) return null;
233
+ const definition = objectValue(source.definition) ?? source;
234
+ const type = nonEmptyText(firstValue(definition, 'type')) ?? nonEmptyText(firstValue(source, 'type'));
235
+ const id = nonEmptyText(firstValue(definition, 'id')) ?? nonEmptyText(firstValue(source, 'id'));
236
+ const revision = normalizeRevision(firstValue(definition, 'revision', 'version')) ?? normalizeRevision(firstValue(source, 'revision', 'version'));
237
+ if (!id || !revision) return null;
238
+ const criteriaValue = firstValue(definition, 'criteria');
239
+ const criteria = asArray(criteriaValue);
240
+ // A malformed criterion must invalidate the whole Objective readback. It
241
+ // is unsafe to drop a broken Variable reference and present the remaining
242
+ // criteria as a usable (or empty) set for saving or judgment.
243
+ if (criteriaValue !== undefined && criteria === null) return null;
244
+ const normalizedCriteria = criteria ? criteria.map(normalizeCriterion) : null;
245
+ if (normalizedCriteria?.some((criterion) => !criterion)) return null;
246
+ const readiness = normalizeReadiness(firstValue(source, 'readiness', 'judgmentReadiness', 'judgment_readiness'));
247
+ const constraintRefsValue = firstValue(source, 'constraintRefs', 'constraint_refs', 'constraints');
248
+ const constraintRefs = asArray(constraintRefsValue);
249
+ const normalized = {
250
+ id,
251
+ type: type ?? 'objective',
252
+ revision,
253
+ digest: nonEmptyText(firstValue(source, 'digest', 'hash')),
254
+ meaning: nonEmptyText(firstValue(definition, 'meaning')),
255
+ beneficiaryIds: asArray(firstValue(definition, 'beneficiaryIds', 'beneficiary_ids')),
256
+ desiredState: nonEmptyText(firstValue(definition, 'desiredState', 'desired_state')),
257
+ criteria: normalizedCriteria,
258
+ evaluationPeriod: normalizePeriod(firstValue(definition, 'evaluationPeriod', 'evaluation_period')),
259
+ accountableId: nonEmptyText(firstValue(definition, 'accountableId', 'accountable_id')),
260
+ epistemicState: nonEmptyText(firstValue(definition, 'epistemicState', 'epistemic_state')),
261
+ adoptionState: nonEmptyText(firstValue(definition, 'adoptionState', 'adoption_state')),
262
+ authorizedUses: asArray(firstValue(definition, 'authorizedUses', 'authorized_uses')),
263
+ acl: objectValue(firstValue(definition, 'acl')),
264
+ storage: nonEmptyText(firstValue(definition, 'storage')),
265
+ provenance: asArray(firstValue(definition, 'provenance')),
266
+ scope: objectValue(firstValue(definition, 'scope')),
267
+ readiness,
268
+ constraintRefs: constraintRefs ? constraintRefs.map((ref) => normalizeReference(ref, 'constraint')).filter(Boolean) : null,
269
+ raw: source,
270
+ definition,
271
+ };
272
+ return normalized;
273
+ }
274
+
275
+ function collectionRecords(payload, keys) {
276
+ if (Array.isArray(payload)) return payload;
277
+ const object = objectValue(payload);
278
+ if (!object) return null;
279
+ for (const key of keys) {
280
+ if (Array.isArray(object[key])) return object[key];
281
+ }
282
+ return null;
283
+ }
284
+
285
+ /**
286
+ * Keeps an absent records array distinct from a confirmed empty result.
287
+ */
288
+ export function normalizeObjectiveCollection(payload) {
289
+ const recordsValue = collectionRecords(payload, ['records', 'objectives', 'items', 'data']);
290
+ const explicitState = normalizeState(payloadValue(payload, 'state', 'status'), 'unknown');
291
+ const explicitFailure = explicitCollectionFailure(explicitState, 'records');
292
+ if (explicitFailure) return explicitFailure;
293
+ if (recordsValue === null) {
294
+ return { state: explicitState === 'empty' ? 'unknown' : explicitState, records: null, absence_confirmed: false };
295
+ }
296
+ const records = recordsValue.map(normalizeObjectiveRecord);
297
+ if (records.some((record) => !record || record.type !== 'objective')) {
298
+ return { state: 'unknown', records: null, absence_confirmed: false };
299
+ }
300
+ const absenceConfirmed = payloadValue(payload, 'absence_confirmed', 'absenceConfirmed') === true;
301
+ if (records.length === 0 && !absenceConfirmed) {
302
+ return { state: 'unknown', records: null, absence_confirmed: false };
303
+ }
304
+ const state = explicitState === 'permission_denied' || explicitState === 'api_unavailable' || explicitState === 'conflict'
305
+ ? explicitState
306
+ : records.length ? 'ready' : 'empty';
307
+ return {
308
+ state,
309
+ records,
310
+ absence_confirmed: absenceConfirmed,
311
+ };
312
+ }
313
+
314
+ export function normalizeReferenceCollection(payload, keys = ['refs', 'references', 'constraints', 'records', 'items', 'data']) {
315
+ const recordsValue = collectionRecords(payload, keys);
316
+ const explicitState = normalizeState(payloadValue(payload, 'state', 'status'), 'unknown');
317
+ const explicitFailure = explicitCollectionFailure(explicitState, 'refs');
318
+ if (explicitFailure) return explicitFailure;
319
+ if (recordsValue === null) return { state: explicitState, refs: null, absence_confirmed: false };
320
+ const refs = recordsValue.map((value) => normalizeReference(value));
321
+ if (refs.some((ref) => !ref)) {
322
+ return { state: 'unknown', refs: null, absence_confirmed: false };
323
+ }
324
+ const absenceConfirmed = payloadValue(payload, 'absence_confirmed', 'absenceConfirmed') === true;
325
+ if (refs.length === 0 && !absenceConfirmed) {
326
+ return { state: 'unknown', refs: null, absence_confirmed: false };
327
+ }
328
+ return {
329
+ state: explicitState === 'permission_denied' || explicitState === 'api_unavailable' || explicitState === 'conflict'
330
+ ? explicitState : refs.length ? 'ready' : 'empty',
331
+ refs,
332
+ absence_confirmed: absenceConfirmed,
333
+ };
334
+ }
335
+
336
+ export function normalizeStoryObjectiveLinks(payload) {
337
+ const recordsValue = collectionRecords(payload, ['links', 'relations', 'records', 'items', 'data']);
338
+ const explicitState = normalizeState(payloadValue(payload, 'state', 'status'), 'unknown');
339
+ const explicitFailure = explicitCollectionFailure(explicitState, 'links');
340
+ if (explicitFailure) return explicitFailure;
341
+ if (recordsValue === null) return { state: explicitState, links: null, absence_confirmed: false };
342
+ const links = recordsValue.map((value) => {
343
+ const object = objectValue(value);
344
+ if (!object) return null;
345
+ const relation = nonEmptyText(firstValue(object, 'relation', 'linkKind', 'link_kind', 'kind'));
346
+ const objective = normalizeReference(firstValue(object, 'objective', 'target', 'objectiveRef', 'objective_ref'));
347
+ const story = normalizeReference(firstValue(object, 'story', 'source', 'storyRef', 'story_ref'), 'story');
348
+ // The relation target is the canonical Objective. A model, Story, or
349
+ // other resource must never be rendered as an Objective by defaulting its
350
+ // type from the relation position.
351
+ if (!relation || !objective || objective.type !== 'objective') return null;
352
+ return {
353
+ relation,
354
+ relationLabel: RELATION_LABELS[relation] ?? relation,
355
+ objective,
356
+ story,
357
+ timeCondition: firstValue(object, 'timeCondition', 'time_condition') ?? null,
358
+ raw: object,
359
+ };
360
+ }).filter(Boolean);
361
+ if (links.length !== recordsValue.length) {
362
+ return { state: 'unknown', links: null, absence_confirmed: false };
363
+ }
364
+ const absenceConfirmed = payloadValue(payload, 'absence_confirmed', 'absenceConfirmed') === true;
365
+ if (links.length === 0 && !absenceConfirmed) {
366
+ return { state: 'unknown', links: null, absence_confirmed: false };
367
+ }
368
+ return {
369
+ state: explicitState === 'permission_denied' || explicitState === 'api_unavailable' || explicitState === 'conflict'
370
+ ? explicitState : links.length ? 'ready' : 'empty',
371
+ links,
372
+ absence_confirmed: absenceConfirmed,
373
+ };
374
+ }
375
+
376
+ export function objectiveJudgmentState(objective) {
377
+ const normalized = normalizeObjectiveRecord(objective) ?? objective;
378
+ const readiness = normalized?.readiness;
379
+ if (readiness?.state === 'ready' && readiness.ready === true && normalized?.adoptionState !== 'retired') return 'judgment_available';
380
+ if (readiness?.state === 'ready' && readiness.ready === false) return 'draft';
381
+ if (['draft', 'proposed'].includes(normalized?.adoptionState)) return 'draft';
382
+ return 'judgment_unknown';
383
+ }
384
+
385
+ export function objectiveJudgmentLabel(objective) {
386
+ return STATUS_LABELS[objectiveJudgmentState(objective)] ?? STATUS_LABELS.unknown;
387
+ }
388
+
389
+ export function statusLabel(value) {
390
+ const state = normalizeState(value);
391
+ return STATUS_LABELS[state] ?? text(value);
392
+ }
393
+
394
+ export function normalizeObjectiveError(error) {
395
+ const code = optionalText(error?.code ?? error?.status ?? error?.name).trim().toLowerCase();
396
+ const status = Number(error?.statusCode ?? error?.status);
397
+ let state = normalizeState(code);
398
+ if (state === UNKNOWN || !state) {
399
+ if (status === 401 || status === 403) state = 'permission_denied';
400
+ else if (status === 404) state = 'missing';
401
+ else if (status === 409) state = 'conflict';
402
+ else if (status >= 500) state = 'api_unavailable';
403
+ else state = 'error_retryable';
404
+ }
405
+ if (!['permission_denied', 'api_unavailable', 'missing', 'conflict', 'saved_unverified', 'error_retryable'].includes(state)) state = 'error_retryable';
406
+ return {
407
+ state,
408
+ code: code || state,
409
+ currentRevision: nonEmptyText(error?.currentRevision ?? error?.current_revision),
410
+ message: nonEmptyText(error?.message) ?? STATUS_LABELS[state],
411
+ error,
412
+ };
413
+ }
414
+
415
+ function statusNotice(state, callbacks = {}) {
416
+ const normalized = typeof state === 'string' ? { state } : state ?? {};
417
+ const status = normalizeState(normalized.state);
418
+ const className = ['permission_denied', 'api_unavailable', 'conflict', 'missing'].includes(status)
419
+ ? `objective-editor-notice objective-editor-notice-${status}`
420
+ : 'objective-editor-notice';
421
+ const notice = makeElement('div', { className, attrs: { role: status === 'error_retryable' ? 'alert' : 'status' } });
422
+ append(notice,
423
+ makeElement('strong', { text: STATUS_LABELS[status] ?? STATUS_LABELS.unknown }),
424
+ makeElement('p', { text: normalized.message ?? STATUS_LABELS[status] ?? '状態を確認できません。' }),
425
+ );
426
+ if (status === 'conflict' && normalized.currentRevision) {
427
+ notice.append(makeElement('small', { text: `現在の版: ${normalized.currentRevision}。入力を保持したまま再読込してください。` }));
428
+ }
429
+ if (callbacks.onRetry) notice.append(makeButton('再読込', callbacks.onRetry, 'objective-editor-button-secondary'));
430
+ return notice;
431
+ }
432
+
433
+ function makeButton(label, onClick, className = 'objective-editor-button') {
434
+ const button = makeElement('button', { className, text: label, attrs: { type: 'button' } });
435
+ if (onClick) button.addEventListener('click', onClick);
436
+ return button;
437
+ }
438
+
439
+ function statusBadge(state, label = null) {
440
+ const normalized = normalizeState(state);
441
+ return makeElement('span', {
442
+ className: `objective-editor-badge objective-editor-badge-${normalized.replace(/[^a-z0-9_-]/g, '-')}`,
443
+ text: label ?? (STATUS_LABELS[normalized] ?? text(state)),
444
+ });
445
+ }
446
+
447
+ function field(labelText, name, value, options = {}) {
448
+ const label = makeElement('label', { className: 'objective-editor-field' });
449
+ label.append(makeElement('span', { className: 'objective-editor-label', text: labelText }));
450
+ const input = makeElement(options.tag ?? 'input', {
451
+ className: 'objective-editor-input',
452
+ value: value ?? '',
453
+ attrs: { name, ...(options.attrs ?? {}) },
454
+ disabled: options.disabled,
455
+ });
456
+ if (options.tag === 'textarea') input.textContent = value ?? '';
457
+ if (options.options) {
458
+ clear(input);
459
+ for (const option of options.options) {
460
+ input.append(makeElement('option', { text: option.label, value: option.value, attrs: { value: option.value } }));
461
+ }
462
+ input.value = value ?? '';
463
+ }
464
+ label.append(input);
465
+ if (options.help) label.append(makeElement('small', { className: 'objective-editor-help', text: options.help }));
466
+ return { label, input };
467
+ }
468
+
469
+ function findAll(node, predicate, result = []) {
470
+ if (!node) return result;
471
+ if (predicate(node)) result.push(node);
472
+ for (const child of node.children ?? []) findAll(child, predicate, result);
473
+ return result;
474
+ }
475
+
476
+ function nodeAttribute(node, name) {
477
+ if (!node) return undefined;
478
+ if (typeof node.getAttribute === 'function') return node.getAttribute(name) ?? undefined;
479
+ const attributes = node.attributes;
480
+ if (!attributes) return undefined;
481
+ if (typeof attributes.getNamedItem === 'function') return attributes.getNamedItem(name)?.value;
482
+ return attributes[name];
483
+ }
484
+
485
+ function findField(root, name) {
486
+ return findAll(root, (node) => nodeAttribute(node, 'name') === name)[0] ?? null;
487
+ }
488
+
489
+ function parseTarget(value) {
490
+ const input = optionalText(value).trim();
491
+ if (!input) return undefined;
492
+ if (input === 'true') return true;
493
+ if (input === 'false') return false;
494
+ if (/^-?(?:\d+|\d*\.\d+)$/.test(input)) return Number(input);
495
+ return input;
496
+ }
497
+
498
+ function copyDefinition(normalized) {
499
+ if (!normalized) return null;
500
+ const source = objectValue(normalized.definition) ?? objectValue(normalized.raw) ?? {};
501
+ return JSON.parse(JSON.stringify(source));
502
+ }
503
+
504
+ function editorDraftFromObjective(objective, options = {}) {
505
+ const definition = copyDefinition(objective) ?? {};
506
+ return {
507
+ ...definition,
508
+ id: objective?.id ?? definition.id ?? '',
509
+ type: 'objective',
510
+ revision: objective?.revision ?? definition.revision ?? '1',
511
+ meaning: objective?.meaning ?? definition.meaning ?? '',
512
+ beneficiaryIds: [...(objective?.beneficiaryIds ?? definition.beneficiaryIds ?? [])],
513
+ desiredState: objective?.desiredState ?? definition.desiredState ?? '',
514
+ criteria: JSON.parse(JSON.stringify(objective?.criteria ?? definition.criteria ?? [])),
515
+ evaluationPeriod: {
516
+ from: objective?.evaluationPeriod?.from ?? definition.evaluationPeriod?.from ?? '',
517
+ until: objective?.evaluationPeriod?.until ?? definition.evaluationPeriod?.until ?? '',
518
+ },
519
+ accountableId: objective?.accountableId ?? definition.accountableId ?? '',
520
+ adoptionState: objective?.adoptionState ?? definition.adoptionState ?? 'draft',
521
+ authorizedUses: [...(objective?.authorizedUses ?? definition.authorizedUses ?? ['draft'])],
522
+ ...options,
523
+ };
524
+ }
525
+
526
+ function renderObjectiveList(root, state, callbacks) {
527
+ const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': '目的一覧' } });
528
+ append(section,
529
+ makeElement('div', { className: 'objective-editor-panel-heading', text: '目的一覧' }),
530
+ makeElement('p', { className: 'objective-editor-muted', text: '目的本文、評価基準、利用可能な版を正本から確認します。' }),
531
+ );
532
+ if (state.objectives.state === 'loading') {
533
+ for (let index = 0; index < 3; index += 1) section.append(makeElement('div', { className: 'objective-editor-skeleton', text: '目的を読み込んでいます。' }));
534
+ return section;
535
+ }
536
+ if (['permission_denied', 'api_unavailable', 'error_retryable', 'unknown'].includes(state.objectives.state)) {
537
+ section.append(statusNotice(state.objectives, { onRetry: callbacks.onReload }));
538
+ return section;
539
+ }
540
+ if (state.objectives.state === 'empty') {
541
+ section.append(makeElement('div', { className: 'objective-editor-empty', text: '目的はまだ登録されていません。' }));
542
+ if (callbacks.onCreate) section.append(makeButton('目的を作成', callbacks.onCreate, 'objective-editor-button-primary'));
543
+ return section;
544
+ }
545
+ const list = makeElement('div', { className: 'objective-editor-list' });
546
+ for (const objective of state.objectives.records ?? []) {
547
+ const row = makeButton('', () => callbacks.onSelect?.(objective), 'objective-editor-list-row');
548
+ const copy = makeElement('span', { className: 'objective-editor-list-copy' });
549
+ append(copy,
550
+ makeElement('strong', { text: text(objective.meaning, objective.id) }),
551
+ makeElement('small', { text: `${objective.id}@${objective.revision}` }),
552
+ );
553
+ const badges = makeElement('span', { className: 'objective-editor-list-badges' });
554
+ badges.append(statusBadge(objective.adoptionState ?? 'unknown'), statusBadge(objectiveJudgmentState(objective), objectiveJudgmentLabel(objective)));
555
+ row.append(copy, badges);
556
+ list.append(row);
557
+ }
558
+ section.append(list);
559
+ return section;
560
+ }
561
+
562
+ function renderReadiness(root, readiness) {
563
+ const section = makeElement('section', { className: 'objective-editor-readiness', attrs: { 'aria-label': '判断利用可能性' } });
564
+ const normalized = normalizeReadiness(readiness);
565
+ section.append(makeElement('div', { className: 'objective-editor-subheading', text: '判断への利用' }));
566
+ if (normalized.state !== 'ready') {
567
+ section.append(statusBadge('judgment_unknown'), makeElement('p', { className: 'objective-editor-muted', text: 'readinessを正本APIから確認できていません。' }));
568
+ return section;
569
+ }
570
+ section.append(statusBadge(normalized.ready ? 'judgment_available' : 'draft'));
571
+ if (!normalized.ready && normalized.issues?.length) {
572
+ const list = makeElement('ul', { className: 'objective-editor-issue-list' });
573
+ for (const issue of normalized.issues) list.append(makeElement('li', { text: `${issue.path}: ${issue.message}` }));
574
+ section.append(list);
575
+ } else {
576
+ section.append(makeElement('p', { className: 'objective-editor-muted', text: '必要な定義・基準・参照が揃っています。観測値の存在は別途確認します。' }));
577
+ }
578
+ return section;
579
+ }
580
+
581
+ function renderConstraintRefs(root, state, callbacks) {
582
+ const section = makeElement('section', { className: 'objective-editor-reference-section', attrs: { 'aria-label': '制約参照' } });
583
+ section.append(makeElement('div', { className: 'objective-editor-subheading', text: '適用する制約参照' }));
584
+ if (state.constraints.state === 'loading') {
585
+ section.append(makeElement('p', { className: 'objective-editor-muted', text: '制約参照を読み込んでいます。' }));
586
+ return section;
587
+ }
588
+ if (['permission_denied', 'api_unavailable', 'error_retryable', 'unknown'].includes(state.constraints.state)) {
589
+ section.append(statusNotice(state.constraints, { onRetry: callbacks.onReloadConstraints }));
590
+ return section;
591
+ }
592
+ const refs = state.editor.constraintRefs ?? state.constraints.refs ?? [];
593
+ const list = makeElement('ul', { className: 'objective-editor-reference-list' });
594
+ if (!refs.length) list.append(makeElement('li', { className: 'objective-editor-muted', text: 'この目的に紐づく制約参照はありません。' }));
595
+ for (const ref of refs) {
596
+ const item = makeElement('li', { className: 'objective-editor-reference-item' });
597
+ item.append(makeElement('span', { text: `${ref.id}@${ref.revision}` }));
598
+ if (ref.meaning) item.append(makeElement('small', { text: ref.meaning }));
599
+ if (callbacks.canEdit && callbacks.onRemoveConstraintRef) item.append(makeButton('外す', () => callbacks.onRemoveConstraintRef(ref), 'objective-editor-button-danger'));
600
+ list.append(item);
601
+ }
602
+ section.append(list);
603
+ if (callbacks.canEdit && callbacks.onAddConstraintRef) {
604
+ const addForm = makeElement('form', { className: 'objective-editor-inline-form' });
605
+ const idField = field('制約ID', 'constraint_id', '', { attrs: { placeholder: 'constraint-id' } });
606
+ const revisionField = field('版', 'constraint_revision', '', { attrs: { placeholder: '1' } });
607
+ const submit = makeElement('button', { className: 'objective-editor-button-secondary', text: '参照を追加', attrs: { type: 'submit' } });
608
+ addForm.append(idField.label, revisionField.label, submit);
609
+ addForm.addEventListener('submit', (event) => {
610
+ event.preventDefault();
611
+ callbacks.onAddConstraintRef({ id: idField.input.value, type: 'constraint', revision: revisionField.input.value });
612
+ });
613
+ section.append(addForm);
614
+ }
615
+ return section;
616
+ }
617
+
618
+ function renderCriteria(root, draft, callbacks) {
619
+ const fieldset = makeElement('fieldset', { className: 'objective-editor-criteria' });
620
+ fieldset.append(makeElement('legend', { text: '評価基準' }));
621
+ const criteria = Array.isArray(draft.criteria) ? draft.criteria : [];
622
+ if (!criteria.length) fieldset.append(makeElement('p', { className: 'objective-editor-muted', text: '評価基準はまだありません。判断利用にはVariable参照が必要です。' }));
623
+ criteria.forEach((criterion, index) => {
624
+ const row = makeElement('div', { className: 'objective-editor-criterion-row' });
625
+ const variableRef = criterion?.variableRef ?? {};
626
+ const variableId = field('Variable ID', `criteria.${index}.variable_id`, variableRef.id ?? '', { attrs: { required: true } });
627
+ const variableRevision = field('Variable版', `criteria.${index}.variable_revision`, variableRef.revision ?? '', { attrs: { required: true } });
628
+ const operator = field('比較', `criteria.${index}.operator`, criterion?.operator ?? 'equals', { options: [
629
+ { value: 'at_least', label: '以上' },
630
+ { value: 'at_most', label: '以下' },
631
+ { value: 'equals', label: '一致' },
632
+ ] });
633
+ const target = field('基準値', `criteria.${index}.target`, criterion?.target ?? '', { attrs: { placeholder: '未指定は条件のみ' } });
634
+ const remove = callbacks.canEdit ? makeButton('削除', () => callbacks.onRemoveCriterion?.(index), 'objective-editor-button-danger') : null;
635
+ row.append(variableId.label, variableRevision.label, operator.label, target.label, remove);
636
+ fieldset.append(row);
637
+ });
638
+ if (callbacks.canEdit) fieldset.append(makeButton('基準を追加', callbacks.onAddCriterion, 'objective-editor-button-secondary'));
639
+ return fieldset;
640
+ }
641
+
642
+ function collectDefinition(form, baseDraft) {
643
+ const draft = JSON.parse(JSON.stringify(baseDraft ?? {}));
644
+ const value = (name) => findField(form, name)?.value ?? '';
645
+ draft.id = optionalText(value('id')).trim();
646
+ draft.meaning = value('meaning');
647
+ draft.desiredState = value('desired_state');
648
+ draft.beneficiaryIds = optionalText(value('beneficiary_ids')).split(',').map((item) => item.trim()).filter(Boolean);
649
+ draft.accountableId = optionalText(value('accountable_id')).trim() || undefined;
650
+ draft.adoptionState = value('adoption_state') || draft.adoptionState || 'draft';
651
+ draft.evaluationPeriod = {
652
+ from: canonicalDateTimeValue(value('evaluation_from')),
653
+ until: canonicalDateTimeValue(value('evaluation_until')),
654
+ };
655
+ draft.criteria = [];
656
+ const variableIds = findAll(form, (node) => /^criteria\.\d+\.variable_id$/.test(nodeAttribute(node, 'name') ?? ''));
657
+ for (const variableIdInput of variableIds) {
658
+ const match = nodeAttribute(variableIdInput, 'name').match(/^criteria\.(\d+)\.variable_id$/);
659
+ const index = Number(match[1]);
660
+ const variableRevision = value(`criteria.${index}.variable_revision`).trim();
661
+ const operator = value(`criteria.${index}.operator`) || 'equals';
662
+ const target = parseTarget(value(`criteria.${index}.target`));
663
+ if (!variableIdInput.value.trim() && !variableRevision) continue;
664
+ const criterion = {
665
+ variableRef: { id: variableIdInput.value.trim(), type: 'variable', revision: variableRevision },
666
+ operator,
667
+ };
668
+ if (target !== undefined) criterion.target = target;
669
+ draft.criteria.push(criterion);
670
+ }
671
+ return draft;
672
+ }
673
+
674
+ function renderObjectiveForm(root, state, callbacks) {
675
+ const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': '目的を編集' } });
676
+ const draft = state.editor.draft;
677
+ if (!draft) {
678
+ section.append(makeElement('div', { className: 'objective-editor-empty', text: '編集する目的を選択してください。' }));
679
+ return section;
680
+ }
681
+ const title = state.editor.mode === 'create' ? '目的を作成' : '目的を編集';
682
+ append(section,
683
+ makeElement('div', { className: 'objective-editor-panel-heading', text: title }),
684
+ makeElement('p', { className: 'objective-editor-muted', text: '入力はFoundationRevisionStoreへ渡す定義の編集です。組織の権限判定はホストのportが担当します。' }),
685
+ );
686
+ if (state.save.state !== 'idle' && state.save.state !== 'saving') section.append(statusNotice(state.save, { onRetry: callbacks.onRetrySave }));
687
+ const form = makeElement('form', { className: 'objective-editor-form' });
688
+ const identity = makeElement('div', { className: 'objective-editor-identity' });
689
+ const idField = field('目的ID', 'id', draft.id, { disabled: state.editor.mode !== 'create', attrs: { required: true } });
690
+ const revision = makeElement('div', { className: 'objective-editor-readonly-value' });
691
+ append(revision, makeElement('span', { className: 'objective-editor-label', text: '使用する版' }), makeElement('strong', { text: `${draft.revision ?? '1'}` }));
692
+ identity.append(idField.label, revision);
693
+ form.append(identity);
694
+ form.append(field('目的の意味', 'meaning', draft.meaning, { tag: 'textarea', attrs: { required: true, rows: '3' } }).label);
695
+ form.append(field('実現したい状態', 'desired_state', draft.desiredState, { tag: 'textarea', attrs: { required: true, rows: '3' } }).label);
696
+ form.append(field('対象者ID(カンマ区切り)', 'beneficiary_ids', (draft.beneficiaryIds ?? []).join(', '), { help: '対象者を推測して補完しません。' }).label);
697
+ const period = makeElement('div', { className: 'objective-editor-inline-fields' });
698
+ period.append(field('評価期間の開始', 'evaluation_from', dateTimeLocalValue(draft.evaluationPeriod?.from ?? ''), { attrs: { type: 'datetime-local' } }).label);
699
+ period.append(field('評価期間の終了', 'evaluation_until', dateTimeLocalValue(draft.evaluationPeriod?.until ?? ''), { attrs: { type: 'datetime-local' } }).label);
700
+ form.append(period);
701
+ const governance = makeElement('div', { className: 'objective-editor-inline-fields' });
702
+ governance.append(field('責任者ID', 'accountable_id', draft.accountableId ?? '', { help: '認可主体やRACIをここで決めません。' }).label);
703
+ governance.append(field('採用状態', 'adoption_state', draft.adoptionState ?? 'draft', { options: [
704
+ { value: 'draft', label: '下書き' },
705
+ { value: 'proposed', label: '提案中' },
706
+ { value: 'approved', label: '承認済み' },
707
+ { value: 'retired', label: '終了' },
708
+ ] }).label);
709
+ form.append(governance);
710
+ form.append(renderCriteria(root, draft, callbacks));
711
+ form.append(renderReadiness(root, state.readiness));
712
+ form.append(renderConstraintRefs(root, state, callbacks));
713
+ const actions = makeElement('div', { className: 'objective-editor-actions' });
714
+ actions.append(makeElement('span', { className: 'objective-editor-muted', text: callbacks.canEdit ? '保存後に同じID・新版を再取得して確認します。' : '読み取り専用です。書込み権限はホストから提供してください。' }));
715
+ if (callbacks.canEdit) actions.append(makeElement('button', { className: 'objective-editor-button-primary', text: state.save.state === 'saving' ? '保存中…' : '保存', attrs: { type: 'submit' }, disabled: state.save.state === 'saving' }));
716
+ actions.append(makeButton('一覧へ戻る', callbacks.onBack, 'objective-editor-button-secondary'));
717
+ form.append(actions);
718
+ form.addEventListener('submit', (event) => {
719
+ event.preventDefault();
720
+ callbacks.onSubmit?.(collectDefinition(form, draft));
721
+ });
722
+ section.append(form);
723
+ return section;
724
+ }
725
+
726
+ function renderStoryLinks(root, state, callbacks) {
727
+ const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': 'StoryからObjectiveを参照' } });
728
+ append(section,
729
+ makeElement('div', { className: 'objective-editor-panel-heading', text: 'StoryからObjectiveを参照' }),
730
+ makeElement('p', { className: 'objective-editor-muted', text: 'Story本文を複製せず、既存のtyped relationとObjectiveの版を表示します。' }),
731
+ );
732
+ const form = makeElement('form', { className: 'objective-editor-inline-form' });
733
+ const storyField = field('Story ID', 'story_id', state.story.storyId ?? '', { attrs: { required: true, placeholder: 'story-...' } });
734
+ form.append(storyField.label, makeElement('button', { className: 'objective-editor-button-secondary', text: '参照を取得', attrs: { type: 'submit' } }));
735
+ form.addEventListener('submit', (event) => { event.preventDefault(); callbacks.onLoadStoryLinks?.(storyField.input.value.trim()); });
736
+ section.append(form);
737
+ if (state.story.state === 'loading') section.append(makeElement('p', { className: 'objective-editor-muted', text: 'Storyの関係を読み込んでいます。' }));
738
+ else if (['permission_denied', 'api_unavailable', 'error_retryable', 'unknown'].includes(state.story.state)) section.append(statusNotice(state.story, { onRetry: callbacks.onRetryStory }));
739
+ else if (state.story.state === 'empty') section.append(makeElement('p', { className: 'objective-editor-empty', text: 'このStoryから参照されるObjectiveはありません。' }));
740
+ else if (state.story.links) {
741
+ const list = makeElement('ul', { className: 'objective-editor-link-list' });
742
+ for (const link of state.story.links) {
743
+ const item = makeElement('li', { className: 'objective-editor-link-item' });
744
+ append(item,
745
+ statusBadge(link.relation, link.relationLabel),
746
+ makeElement('strong', { text: `${link.objective.id}@${link.objective.revision}` }),
747
+ makeElement('span', { text: text(link.objective.meaning, 'Objectiveの意味 未確認') }),
748
+ );
749
+ if (link.timeCondition) item.append(makeElement('small', { text: `時間条件: ${JSON.stringify(link.timeCondition)}` }));
750
+ list.append(item);
751
+ }
752
+ section.append(list);
753
+ }
754
+ return section;
755
+ }
756
+
757
+ /**
758
+ * Render the common UI from a controller state. Hosts may use this pure
759
+ * renderer when their own application owns navigation and lifecycle.
760
+ */
761
+ export function renderObjectiveEditor(root, state, callbacks = {}) {
762
+ clear(root);
763
+ const wrapper = makeElement('div', { className: 'objective-editor-root', attrs: { 'data-contract-version': OBJECTIVE_EDITOR_CONTRACT_VERSION, 'data-view': state.view ?? 'list' } });
764
+ const header = makeElement('header', { className: 'objective-editor-header' });
765
+ const copy = makeElement('div', { className: 'objective-editor-header-copy' });
766
+ append(copy,
767
+ makeElement('div', { className: 'objective-editor-eyebrow', text: 'BRAINBASE / COMPANY OS' }),
768
+ makeElement('h1', { text: '目的と評価基準' }),
769
+ makeElement('p', { text: '世界の認識やStoryとは分けたObjectiveの正本を、版付きで編集・確認します。' }),
770
+ );
771
+ const headerStatus = state.save.state !== 'idle' ? state.save.state : state.objectives.state;
772
+ header.append(copy, statusBadge(headerStatus));
773
+ wrapper.append(header);
774
+ const toolbar = makeElement('div', { className: 'objective-editor-toolbar' });
775
+ const tabs = [
776
+ ['list', '目的一覧'],
777
+ ['editor', '目的を編集'],
778
+ ['story', 'Story参照'],
779
+ ];
780
+ for (const [id, label] of tabs) toolbar.append(makeButton(label, () => callbacks.onView?.(id), `objective-editor-tab${state.view === id ? ' is-active' : ''}`));
781
+ toolbar.append(makeButton('再読込', callbacks.onReload, 'objective-editor-button-secondary'));
782
+ if (callbacks.canEdit) toolbar.append(makeButton('新しい目的', callbacks.onCreate, 'objective-editor-button-primary'));
783
+ wrapper.append(toolbar);
784
+ const connectionState = state.connection;
785
+ if (connectionState && connectionState !== 'ready') wrapper.append(statusNotice({ state: connectionState, message: state.connectionMessage }, { onRetry: callbacks.onReload }));
786
+ const content = makeElement('main', { className: 'objective-editor-content' });
787
+ const renderer = state.view === 'editor' ? renderObjectiveForm : state.view === 'story' ? renderStoryLinks : renderObjectiveList;
788
+ content.append(renderer(content, state, callbacks));
789
+ wrapper.append(content);
790
+ root.append(wrapper);
791
+ return wrapper;
792
+ }
793
+
794
+ function extractMutationRef(payload) {
795
+ const object = objectValue(payload);
796
+ if (!object) return null;
797
+ return normalizeReference(payloadValue(object, 'ref', 'reference', 'objective', 'record', 'data') ?? object, 'objective');
798
+ }
799
+
800
+ function definitionsEqual(left, right) {
801
+ if (!left || !right) return false;
802
+ return left.id === right.id && left.type === right.type && left.revision === right.revision
803
+ && (!left.digest || !right.digest || left.digest === right.digest);
804
+ }
805
+
806
+ function referencesEqual(left, right) {
807
+ const lhs = (left ?? []).map((ref) => `${ref.type}:${ref.id}@${ref.revision}`).sort();
808
+ const rhs = (right ?? []).map((ref) => `${ref.type}:${ref.id}@${ref.revision}`).sort();
809
+ return JSON.stringify(lhs) === JSON.stringify(rhs);
810
+ }
811
+
812
+ function portMethod(port, name) {
813
+ return port && typeof port[name] === 'function' ? port[name].bind(port) : null;
814
+ }
815
+
816
+ /**
817
+ * Creates a host-injected Objective editor controller.
818
+ *
819
+ * ObjectiveEditorPort methods use the same argument order as
820
+ * CompanyOsObjectives where possible. The host owns FoundationStore,
821
+ * authentication and Story/Constraint adapters; the UI only supplies the
822
+ * trusted context it was given and never accepts owner/scope from form data.
823
+ */
824
+ export function createObjectiveEditorController(options = {}) {
825
+ const root = options.root ?? null;
826
+ const port = options.port ?? null;
827
+ const context = options.context ?? {};
828
+ const canEdit = options.canEdit === true;
829
+ const createDefaults = options.createDefaults ?? {};
830
+ const state = {
831
+ view: options.initialView ?? 'list',
832
+ connection: port ? 'ready' : 'api_unavailable',
833
+ connectionMessage: port ? null : 'ObjectiveEditorPortが提供されていません。組織サービスやDBへ直接接続しません。',
834
+ objectives: { state: 'idle', records: null, absence_confirmed: false },
835
+ selected: null,
836
+ readiness: { state: 'unknown', ready: null, issues: null },
837
+ constraints: { state: 'idle', refs: null, absence_confirmed: false },
838
+ story: { state: 'idle', storyId: '', links: null, absence_confirmed: false },
839
+ editor: { state: 'idle', mode: 'edit', draft: null, expectedRevision: null, constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false },
840
+ save: { state: 'idle' },
841
+ };
842
+
843
+ function render() {
844
+ if (!root) return null;
845
+ return renderObjectiveEditor(root, state, {
846
+ canEdit,
847
+ onView: (view) => { state.view = view; render(); },
848
+ onReload: loadObjectives,
849
+ onCreate: beginCreate,
850
+ onSelect: selectObjective,
851
+ onBack: () => { state.view = 'list'; render(); },
852
+ onSubmit: saveObjective,
853
+ onRetrySave: () => state.editor.mode === 'create' ? saveObjective(state.editor.draft) : saveObjective(state.editor.draft),
854
+ onAddCriterion: () => { state.editor.draft.criteria = [...(state.editor.draft.criteria ?? []), { variableRef: { id: '', type: 'variable', revision: '' }, operator: 'equals' }]; render(); },
855
+ onRemoveCriterion: (index) => { state.editor.draft.criteria = (state.editor.draft.criteria ?? []).filter((_, itemIndex) => itemIndex !== index); render(); },
856
+ onAddConstraintRef: addConstraintRef,
857
+ onRemoveConstraintRef: removeConstraintRef,
858
+ onReloadConstraints: () => loadConstraintRefs(state.selected),
859
+ onLoadStoryLinks: loadStoryLinks,
860
+ onRetryStory: () => loadStoryLinks(state.story.storyId),
861
+ });
862
+ }
863
+
864
+ function setPortState(error, target, fallbackMessage = null) {
865
+ const normalized = normalizeObjectiveError(error);
866
+ target.state = normalized.state;
867
+ target.message = normalized.message ?? fallbackMessage;
868
+ target.code = normalized.code;
869
+ target.currentRevision = normalized.currentRevision;
870
+ return normalized;
871
+ }
872
+
873
+ async function loadObjectives() {
874
+ state.objectives = { state: 'loading', records: null, absence_confirmed: false };
875
+ state.connection = port ? 'ready' : 'api_unavailable';
876
+ render();
877
+ const method = portMethod(port, 'listObjectives');
878
+ if (!method) {
879
+ state.objectives = { state: 'api_unavailable', records: null, absence_confirmed: false, message: '目的一覧APIが未提供です。' };
880
+ state.connection = 'api_unavailable';
881
+ render();
882
+ return state.objectives;
883
+ }
884
+ try {
885
+ state.objectives = normalizeObjectiveCollection(await method(context));
886
+ state.connection = state.objectives.state === 'ready' || state.objectives.state === 'empty' ? 'ready' : state.objectives.state;
887
+ } catch (error) {
888
+ state.objectives = { state: normalizeObjectiveError(error).state, records: null, absence_confirmed: false, ...normalizeObjectiveError(error) };
889
+ state.connection = state.objectives.state;
890
+ }
891
+ render();
892
+ return state.objectives;
893
+ }
894
+
895
+ async function loadConstraintRefs(objective) {
896
+ state.constraints = { state: 'loading', refs: null, absence_confirmed: false };
897
+ render();
898
+ if (!objective) {
899
+ state.constraints = { state: 'unknown', refs: null, absence_confirmed: false, message: 'Objectiveが未選択です。' };
900
+ render();
901
+ return state.constraints;
902
+ }
903
+ const method = portMethod(port, 'listObjectiveConstraintRefs') ?? portMethod(port, 'listConstraintReferences');
904
+ if (!method) {
905
+ state.constraints = { state: 'api_unavailable', refs: null, absence_confirmed: false, message: 'ObjectiveとConstraintの参照APIが未提供です。' };
906
+ state.editor.constraintRefs = null;
907
+ render();
908
+ return state.constraints;
909
+ }
910
+ try {
911
+ const payload = await method({ id: objective.id, type: 'objective', revision: objective.revision }, context);
912
+ state.constraints = normalizeReferenceCollection(payload, ['refs', 'references', 'constraints', 'records', 'items', 'data']);
913
+ if (state.constraints.refs) {
914
+ state.editor.constraintRefs = state.constraints.refs;
915
+ state.editor.originalConstraintRefs = JSON.parse(JSON.stringify(state.constraints.refs));
916
+ state.editor.constraintRefsChanged = false;
917
+ }
918
+ } catch (error) {
919
+ state.constraints = { state: normalizeObjectiveError(error).state, refs: null, absence_confirmed: false, ...normalizeObjectiveError(error) };
920
+ }
921
+ render();
922
+ return state.constraints;
923
+ }
924
+
925
+ async function loadReadiness(objective) {
926
+ if (!objective) return state.readiness;
927
+ const method = portMethod(port, 'checkObjectiveReadiness');
928
+ if (!method) {
929
+ state.readiness = { state: 'unknown', ready: null, issues: null, message: 'readiness APIが未提供です。' };
930
+ render();
931
+ return state.readiness;
932
+ }
933
+ try {
934
+ state.readiness = normalizeReadiness(await method(objective.id, context, objective.revision));
935
+ } catch (error) {
936
+ state.readiness = { state: normalizeObjectiveError(error).state, ready: null, issues: null, ...normalizeObjectiveError(error) };
937
+ }
938
+ if (state.selected) state.selected.readiness = state.readiness;
939
+ render();
940
+ return state.readiness;
941
+ }
942
+
943
+ async function selectObjective(objectiveOrReference) {
944
+ const reference = normalizeReference(objectiveOrReference, 'objective') ?? objectiveOrReference;
945
+ const id = reference?.id;
946
+ const revision = reference?.revision;
947
+ if (!id) return null;
948
+ state.view = 'editor';
949
+ state.editor = { state: 'loading', mode: 'edit', draft: null, expectedRevision: revision ?? null, constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false };
950
+ state.readiness = { state: 'unknown', ready: null, issues: null };
951
+ state.constraints = { state: 'loading', refs: null, absence_confirmed: false };
952
+ render();
953
+ const method = portMethod(port, 'readObjective');
954
+ if (!method) {
955
+ state.editor = { ...state.editor, state: 'api_unavailable', message: 'Objective読取APIが未提供です。' };
956
+ state.connection = 'api_unavailable';
957
+ render();
958
+ return null;
959
+ }
960
+ try {
961
+ const payload = await method(id, context, revision);
962
+ const objective = normalizeObjectiveRecord(payload);
963
+ if (!objective) {
964
+ state.editor = { ...state.editor, state: 'missing', message: '指定したObjectiveを正本から確認できません。' };
965
+ state.connection = 'missing';
966
+ render();
967
+ return null;
968
+ }
969
+ state.selected = objective;
970
+ state.editor = {
971
+ state: 'ready', mode: 'edit', draft: editorDraftFromObjective(objective), expectedRevision: objective.revision,
972
+ constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false,
973
+ };
974
+ state.connection = 'ready';
975
+ render();
976
+ await Promise.allSettled([loadReadiness(objective), loadConstraintRefs(objective)]);
977
+ return objective;
978
+ } catch (error) {
979
+ const normalized = normalizeObjectiveError(error);
980
+ state.editor = { ...state.editor, state: normalized.state, message: normalized.message, error: normalized.error };
981
+ state.connection = normalized.state;
982
+ render();
983
+ return null;
984
+ }
985
+ }
986
+
987
+ function beginCreate() {
988
+ state.view = 'editor';
989
+ state.selected = null;
990
+ state.readiness = { state: 'unknown', ready: null, issues: null };
991
+ state.constraints = { state: 'empty', refs: [], absence_confirmed: true };
992
+ state.editor = {
993
+ state: 'ready', mode: 'create', draft: editorDraftFromObjective({
994
+ id: '', revision: '1', meaning: '', beneficiaryIds: [], desiredState: '', criteria: [],
995
+ evaluationPeriod: { from: '', until: '' }, adoptionState: 'draft', authorizedUses: ['draft'],
996
+ definition: { type: 'objective', revision: '1', ...createDefaults },
997
+ }), expectedRevision: null, constraintRefs: [], originalConstraintRefs: [], constraintRefsChanged: false,
998
+ };
999
+ state.save = { state: 'idle' };
1000
+ render();
1001
+ }
1002
+
1003
+ function addConstraintRef(reference) {
1004
+ const normalized = normalizeReference(reference, 'constraint');
1005
+ if (!normalized || normalized.type !== 'constraint') return;
1006
+ const refs = state.editor.constraintRefs ?? [];
1007
+ if (refs.some((item) => item.id === normalized.id && item.revision === normalized.revision)) return;
1008
+ state.editor.constraintRefs = [...refs, normalized];
1009
+ state.editor.constraintRefsChanged = true;
1010
+ render();
1011
+ }
1012
+
1013
+ function removeConstraintRef(reference) {
1014
+ state.editor.constraintRefs = (state.editor.constraintRefs ?? []).filter((item) => !(item.id === reference.id && item.revision === reference.revision));
1015
+ state.editor.constraintRefsChanged = true;
1016
+ render();
1017
+ }
1018
+
1019
+ async function saveObjective(definition) {
1020
+ if (!definition) return null;
1021
+ const mode = state.editor.mode;
1022
+ const updateMethod = mode === 'create' ? null : portMethod(port, 'updateObjective');
1023
+ const createMethod = mode === 'create' ? portMethod(port, 'createObjective') : null;
1024
+ if ((mode === 'create' && !createMethod) || (mode !== 'create' && !updateMethod)) {
1025
+ state.save = { state: 'api_unavailable', message: 'Objectiveの保存APIが未提供です。' };
1026
+ render();
1027
+ return state.save;
1028
+ }
1029
+ if (state.editor.constraintRefsChanged && !portMethod(port, 'replaceObjectiveConstraintRefs')) {
1030
+ state.save = { state: 'api_unavailable', message: 'Constraint参照の保存APIが未提供のため、目的本文も保存していません。' };
1031
+ render();
1032
+ return state.save;
1033
+ }
1034
+ state.editor.draft = definition;
1035
+ state.save = { state: 'saving', action: mode };
1036
+ render();
1037
+ try {
1038
+ const mutation = mode === 'create'
1039
+ ? await createMethod(definition, context)
1040
+ : await updateMethod(definition.id, state.editor.expectedRevision, definition, context);
1041
+ const savedRef = extractMutationRef(mutation);
1042
+ if (!savedRef || savedRef.id !== definition.id || savedRef.type !== 'objective' || !savedRef.revision) {
1043
+ state.save = { state: 'saved_unverified', message: '保存応答にObjectiveの同じID・新版が含まれていません。', mutation };
1044
+ render();
1045
+ return state.save;
1046
+ }
1047
+ const expectedRevision = state.editor.expectedRevision;
1048
+ if (mode !== 'create' && (!expectedRevision || savedRef.revision === expectedRevision)) {
1049
+ state.save = {
1050
+ state: 'saved_unverified',
1051
+ message: '更新応答にexpectedRevisionとは異なる新版が含まれていません。',
1052
+ mutation,
1053
+ expectedRevision,
1054
+ reference: savedRef,
1055
+ };
1056
+ render();
1057
+ return state.save;
1058
+ }
1059
+ const readMethod = portMethod(port, 'readObjective');
1060
+ const readback = readMethod ? normalizeObjectiveRecord(await readMethod(savedRef.id, context, savedRef.revision)) : null;
1061
+ const returnedDefinition = readback ? { id: readback.id, type: readback.type, revision: readback.revision, digest: readback.digest } : null;
1062
+ const referenceMatches = definitionsEqual(savedRef, returnedDefinition);
1063
+ if (!referenceMatches || !readback || (mode !== 'create' && (!expectedRevision || readback.revision === expectedRevision))) {
1064
+ state.save = { state: 'saved_unverified', message: '保存応答後の同じID・新版のreadbackを確認できません。', mutation, readback };
1065
+ render();
1066
+ return state.save;
1067
+ }
1068
+ if (state.editor.constraintRefsChanged) {
1069
+ const replaceRefs = portMethod(port, 'replaceObjectiveConstraintRefs');
1070
+ await replaceRefs(savedRef, state.editor.constraintRefs ?? [], context);
1071
+ const readConstraintRefs = portMethod(port, 'listObjectiveConstraintRefs') ?? portMethod(port, 'listConstraintReferences');
1072
+ const constraintPayload = readConstraintRefs
1073
+ ? await readConstraintRefs(savedRef, context)
1074
+ : null;
1075
+ const verifiedRefs = normalizeReferenceCollection(constraintPayload).refs;
1076
+ if (!verifiedRefs || !referencesEqual(verifiedRefs, state.editor.constraintRefs)) {
1077
+ state.save = { state: 'saved_unverified', message: 'Objectiveは保存されましたが、Constraint参照のreadbackが一致しません。', mutation, readback, constraintRefs: verifiedRefs };
1078
+ render();
1079
+ return state.save;
1080
+ }
1081
+ }
1082
+ state.selected = readback;
1083
+ state.editor = {
1084
+ state: 'ready', mode: 'edit', draft: editorDraftFromObjective(readback), expectedRevision: readback.revision,
1085
+ constraintRefs: state.editor.constraintRefs ?? [], originalConstraintRefs: state.editor.constraintRefs ?? [], constraintRefsChanged: false,
1086
+ };
1087
+ state.save = { state: 'verified', action: mode, reference: savedRef, readback };
1088
+ state.connection = 'ready';
1089
+ render();
1090
+ await loadObjectives();
1091
+ return state.save;
1092
+ } catch (error) {
1093
+ const normalized = normalizeObjectiveError(error);
1094
+ state.save = { state: normalized.state, message: normalized.message, currentRevision: normalized.currentRevision, error: normalized.error, draft: definition };
1095
+ render();
1096
+ return state.save;
1097
+ }
1098
+ }
1099
+
1100
+ async function loadStoryLinks(storyId) {
1101
+ state.view = 'story';
1102
+ state.story = { state: 'loading', storyId: storyId ?? '', links: null, absence_confirmed: false };
1103
+ render();
1104
+ const method = portMethod(port, 'listStoryObjectiveLinks');
1105
+ if (!method) {
1106
+ state.story = { state: 'api_unavailable', storyId, links: null, absence_confirmed: false, message: 'Story→Objective relation APIが未提供です。' };
1107
+ render();
1108
+ return state.story;
1109
+ }
1110
+ try {
1111
+ state.story = { ...normalizeStoryObjectiveLinks(await method(storyId, context)), storyId };
1112
+ } catch (error) {
1113
+ state.story = { state: normalizeObjectiveError(error).state, storyId, links: null, absence_confirmed: false, ...normalizeObjectiveError(error) };
1114
+ }
1115
+ render();
1116
+ return state.story;
1117
+ }
1118
+
1119
+ const controller = {
1120
+ state,
1121
+ render,
1122
+ loadObjectives,
1123
+ selectObjective,
1124
+ beginCreate,
1125
+ saveObjective,
1126
+ loadConstraintRefs,
1127
+ loadReadiness,
1128
+ loadStoryLinks,
1129
+ addConstraintRef,
1130
+ removeConstraintRef,
1131
+ contractVersion: OBJECTIVE_EDITOR_CONTRACT_VERSION,
1132
+ };
1133
+ render();
1134
+ if (options.autoLoad !== false) void loadObjectives();
1135
+ return controller;
1136
+ }
1137
+
1138
+ export const createObjectiveEditorModule = createObjectiveEditorController;
1139
+ export default createObjectiveEditorController;