@unson/brainbase-mcp 0.40.0 → 0.41.1
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.
- package/dist/foundation-http.d.ts +12 -2
- package/dist/foundation-http.js +183 -8
- package/dist/graph-web.d.ts +2 -0
- package/dist/graph-web.js +10 -1
- package/dist/judgment-history.js +3 -1
- package/dist/local-web-host.js +1 -1
- package/dist/portable-graph.d.ts +2 -2
- package/dist/portable-graph.js +4 -3
- package/dist/types.d.ts +32 -1
- package/package.json +2 -2
- package/ui/foundation-overview.js +17 -1
- package/ui/graph-own-share.js +4 -2
- package/ui/graph-projects-view.js +167 -11
- package/ui/graph-registry-view.js +135 -5
- package/ui/graph-view-shared.js +6 -1
- package/ui/judgment-history.js +5 -1
- package/ui/meeting-minutes.css +19 -0
- package/ui/meeting-minutes.js +124 -6
- package/ui/objective-editor-http-port.js +21 -8
- package/ui/objective-editor.js +186 -24
- package/ui/objective-readback-contract.js +177 -0
- package/ui/outcome-knowledge.css +18 -1
- package/ui/outcome-knowledge.js +523 -55
- package/ui/project-workspace.js +177 -4
- package/ui/world/world-canvas-ui.css +32 -0
- package/ui/world/world-canvas-ui.js +30 -2
- package/ui/world/world-view.js +5 -0
package/ui/objective-editor.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
workspaceRailBlock,
|
|
29
29
|
workspaceRailHead,
|
|
30
30
|
} from './workspace-kit.js';
|
|
31
|
+
import { normalizeObjectiveReadback } from './objective-readback-contract.js';
|
|
31
32
|
|
|
32
33
|
export const OBJECTIVE_EDITOR_CONTRACT_VERSION = 'brainbase.objective-editor.v1';
|
|
33
34
|
|
|
@@ -65,6 +66,16 @@ const STATUS_LABELS = Object.freeze({
|
|
|
65
66
|
unknown: '確認できません',
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
const OBJECTIVE_READBACK_STATUS_LABELS = Object.freeze({
|
|
70
|
+
ready: '確認済み',
|
|
71
|
+
error: '取得失敗',
|
|
72
|
+
unknown: '確認できません',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const OBJECTIVE_READBACK_REASON_MESSAGES = Object.freeze({
|
|
76
|
+
readback_identity_mismatch: '取得した目的が選択中の目的と一致しません。読み直してください。',
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
const RELATION_LABELS = Object.freeze(Object.fromEntries(OBJECTIVE_EDITOR_RELATION_TYPES));
|
|
69
80
|
|
|
70
81
|
function getDocument() {
|
|
@@ -129,6 +140,21 @@ function firstValue(source, ...keys) {
|
|
|
129
140
|
return undefined;
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
function sourceSummary(source) {
|
|
144
|
+
const object = objectValue(source);
|
|
145
|
+
if (!object) return '出典を確認できません。';
|
|
146
|
+
const fields = [
|
|
147
|
+
['種別', firstValue(object, 'source_type', 'sourceType', 'kind', 'type')],
|
|
148
|
+
['状態', firstValue(object, 'state', 'status', 'connection_status', 'connectionStatus')],
|
|
149
|
+
['ID', firstValue(object, 'source_id', 'sourceId', 'id', 'reference')],
|
|
150
|
+
['版', firstValue(object, 'source_version', 'sourceVersion', 'revision', 'version')],
|
|
151
|
+
['ダイジェスト', firstValue(object, 'digest', 'hash')],
|
|
152
|
+
['根拠', Array.isArray(object.provenance) ? String(object.provenance.length) + '件' : null],
|
|
153
|
+
['確認日時', firstValue(object, 'checked_at', 'checkedAt', 'as_of', 'asOf')],
|
|
154
|
+
];
|
|
155
|
+
return fields.map(([label, value]) => `${label}: ${text(value)}`).join(' ・ ');
|
|
156
|
+
}
|
|
157
|
+
|
|
132
158
|
function payloadValue(payload, ...keys) {
|
|
133
159
|
const object = objectValue(payload);
|
|
134
160
|
if (!object) return undefined;
|
|
@@ -141,7 +167,7 @@ function payloadValue(payload, ...keys) {
|
|
|
141
167
|
function normalizeState(value, fallback = UNKNOWN) {
|
|
142
168
|
const state = optionalText(value).trim().toLowerCase();
|
|
143
169
|
if (!state) return fallback;
|
|
144
|
-
if (['forbidden', 'authorization_denied', 'permission_insufficient', 'not_allowed'].includes(state)) return 'permission_denied';
|
|
170
|
+
if (['forbidden', 'authorization_denied', 'permission_insufficient', 'not_allowed', 'permission_denied', 'foundation_permission_denied', 'project_not_allowed'].includes(state)) return 'permission_denied';
|
|
145
171
|
if (['unavailable', 'upstream_unavailable', 'api_not_implemented', 'not_implemented'].includes(state)) return 'api_unavailable';
|
|
146
172
|
if (['version_conflict', 'revision_conflict', 'cas_conflict'].includes(state)) return 'conflict';
|
|
147
173
|
if (['persisted', 'saved'].includes(state)) return 'saved_unverified';
|
|
@@ -174,6 +200,7 @@ function normalizeReference(value, defaultType = null) {
|
|
|
174
200
|
revision,
|
|
175
201
|
digest: nonEmptyText(firstValue(object, 'digest', 'hash')),
|
|
176
202
|
meaning: nonEmptyText(firstValue(object, 'meaning', 'label', 'name')),
|
|
203
|
+
source: objectValue(firstValue(object, 'source', 'provenance', 'origin')),
|
|
177
204
|
raw: object,
|
|
178
205
|
};
|
|
179
206
|
}
|
|
@@ -235,9 +262,13 @@ function normalizeReadiness(value) {
|
|
|
235
262
|
const object = objectValue(value);
|
|
236
263
|
if (!object || typeof object.ready !== 'boolean') return { state: 'unknown', ready: null, issues: null };
|
|
237
264
|
const issues = asArray(object.issues);
|
|
265
|
+
const messageValue = firstValue(object, 'message', 'reason');
|
|
266
|
+
const nextActionValue = firstValue(object, 'next_action', 'nextAction');
|
|
238
267
|
return {
|
|
239
268
|
state: 'ready',
|
|
240
269
|
ready: object.ready,
|
|
270
|
+
message: typeof messageValue === 'string' && messageValue.trim() ? messageValue.trim() : null,
|
|
271
|
+
next_action: typeof nextActionValue === 'string' && nextActionValue.trim() ? nextActionValue.trim() : null,
|
|
241
272
|
issues: issues ? issues.map((issue) => ({
|
|
242
273
|
code: text(firstValue(issue, 'code'), 'UNKNOWN'),
|
|
243
274
|
path: text(firstValue(issue, 'path'), 'definition'),
|
|
@@ -262,14 +293,21 @@ function recordPayload(payload) {
|
|
|
262
293
|
* Converts a canonical FoundationCatalogRecord (or a host projection of it)
|
|
263
294
|
* without filling missing fields with guessed values.
|
|
264
295
|
*/
|
|
265
|
-
export function normalizeObjectiveRecord(payload) {
|
|
266
|
-
const
|
|
296
|
+
export function normalizeObjectiveRecord(payload, expected) {
|
|
297
|
+
const readback = normalizeObjectiveReadback(payload, expected);
|
|
298
|
+
const payloadObject = objectValue(payload);
|
|
299
|
+
// An explicit readback envelope must pass its contract guard. Raw records
|
|
300
|
+
// remain accepted for non-HTTP host adapters and existing unit consumers.
|
|
301
|
+
if (payloadObject?.contract_version && !readback) return null;
|
|
302
|
+
const source = recordPayload(readback ?? payload);
|
|
267
303
|
if (!source) return null;
|
|
268
304
|
const definition = objectValue(source.definition) ?? source;
|
|
269
305
|
const type = nonEmptyText(firstValue(definition, 'type')) ?? nonEmptyText(firstValue(source, 'type'));
|
|
270
306
|
const id = nonEmptyText(firstValue(definition, 'id')) ?? nonEmptyText(firstValue(source, 'id'));
|
|
271
307
|
const revision = normalizeRevision(firstValue(definition, 'revision', 'version')) ?? normalizeRevision(firstValue(source, 'revision', 'version'));
|
|
272
308
|
if (!id || !revision) return null;
|
|
309
|
+
if (expected && (id !== expected.id
|
|
310
|
+
|| (expected.revision !== undefined && expected.revision !== null && revision !== expected.revision))) return null;
|
|
273
311
|
const criteriaValue = firstValue(definition, 'criteria');
|
|
274
312
|
const criteria = asArray(criteriaValue);
|
|
275
313
|
// A malformed criterion must invalidate the whole Objective readback. It
|
|
@@ -281,10 +319,16 @@ export function normalizeObjectiveRecord(payload) {
|
|
|
281
319
|
const readiness = normalizeReadiness(firstValue(source, 'readiness', 'judgmentReadiness', 'judgment_readiness'));
|
|
282
320
|
const constraintRefsValue = firstValue(source, 'constraintRefs', 'constraint_refs', 'constraints');
|
|
283
321
|
const constraintRefs = asArray(constraintRefsValue);
|
|
322
|
+
const objectiveSource = objectValue(readback?.source)
|
|
323
|
+
?? objectValue(firstValue(source, 'source', 'provenance', 'origin'))
|
|
324
|
+
?? objectValue(firstValue(definition, 'source', 'provenance', 'origin'));
|
|
284
325
|
const normalized = {
|
|
285
326
|
id,
|
|
286
327
|
type: type ?? 'objective',
|
|
287
328
|
revision,
|
|
329
|
+
readback_status: readback?.status ?? null,
|
|
330
|
+
checked_at: readback?.checked_at ?? null,
|
|
331
|
+
readback_reason: readback?.reason ?? null,
|
|
288
332
|
digest: nonEmptyText(firstValue(source, 'digest', 'hash')),
|
|
289
333
|
meaning: nonEmptyText(firstValue(definition, 'meaning')),
|
|
290
334
|
beneficiaryIds: asArray(firstValue(definition, 'beneficiaryIds', 'beneficiary_ids')),
|
|
@@ -301,6 +345,7 @@ export function normalizeObjectiveRecord(payload) {
|
|
|
301
345
|
scope: objectValue(firstValue(definition, 'scope')),
|
|
302
346
|
readiness,
|
|
303
347
|
constraintRefs: constraintRefs ? constraintRefs.map((ref) => normalizeReference(ref, 'constraint')).filter(Boolean) : null,
|
|
348
|
+
source: objectiveSource,
|
|
304
349
|
raw: source,
|
|
305
350
|
definition,
|
|
306
351
|
};
|
|
@@ -323,18 +368,23 @@ function collectionRecords(payload, keys) {
|
|
|
323
368
|
export function normalizeObjectiveCollection(payload) {
|
|
324
369
|
const recordsValue = collectionRecords(payload, ['records', 'objectives', 'items', 'data']);
|
|
325
370
|
const explicitState = normalizeState(payloadValue(payload, 'state', 'status'), 'unknown');
|
|
371
|
+
const source = objectValue(payloadValue(payload, 'source', 'provenance', 'origin'));
|
|
372
|
+
const correctionGuidance = objectValue(payloadValue(payload, 'correction_guidance', 'correctionGuidance', 'correction'));
|
|
326
373
|
const explicitFailure = explicitCollectionFailure(explicitState, 'records');
|
|
327
|
-
if (explicitFailure) return explicitFailure;
|
|
374
|
+
if (explicitFailure) return { ...explicitFailure, source, correctionGuidance };
|
|
328
375
|
if (recordsValue === null) {
|
|
329
|
-
return { state: explicitState === 'empty' ? 'unknown' : explicitState, records: null, absence_confirmed: false };
|
|
376
|
+
return { state: explicitState === 'empty' ? 'unknown' : explicitState, records: null, absence_confirmed: false, source, correctionGuidance };
|
|
330
377
|
}
|
|
331
|
-
|
|
378
|
+
// Do not pass Array#map's index as the readback identity expectation.
|
|
379
|
+
// `normalizeObjectiveRecord` accepts an optional expected `{ id, revision }`
|
|
380
|
+
// only for a specific read operation.
|
|
381
|
+
const records = recordsValue.map((record) => normalizeObjectiveRecord(record));
|
|
332
382
|
if (records.some((record) => !record || record.type !== 'objective')) {
|
|
333
|
-
return { state: 'unknown', records: null, absence_confirmed: false };
|
|
383
|
+
return { state: 'unknown', records: null, absence_confirmed: false, source, correctionGuidance };
|
|
334
384
|
}
|
|
335
385
|
const absenceConfirmed = payloadValue(payload, 'absence_confirmed', 'absenceConfirmed') === true;
|
|
336
386
|
if (records.length === 0 && !absenceConfirmed) {
|
|
337
|
-
return { state: 'unknown', records: null, absence_confirmed: false };
|
|
387
|
+
return { state: 'unknown', records: null, absence_confirmed: false, source, correctionGuidance };
|
|
338
388
|
}
|
|
339
389
|
const state = explicitState === 'permission_denied' || explicitState === 'api_unavailable' || explicitState === 'conflict'
|
|
340
390
|
? explicitState
|
|
@@ -343,28 +393,32 @@ export function normalizeObjectiveCollection(payload) {
|
|
|
343
393
|
state,
|
|
344
394
|
records,
|
|
345
395
|
absence_confirmed: absenceConfirmed,
|
|
396
|
+
source,
|
|
397
|
+
correctionGuidance,
|
|
346
398
|
};
|
|
347
399
|
}
|
|
348
400
|
|
|
349
401
|
export function normalizeReferenceCollection(payload, keys = ['refs', 'references', 'constraints', 'records', 'items', 'data']) {
|
|
350
402
|
const recordsValue = collectionRecords(payload, keys);
|
|
351
403
|
const explicitState = normalizeState(payloadValue(payload, 'state', 'status'), 'unknown');
|
|
404
|
+
const source = objectValue(payloadValue(payload, 'source', 'provenance', 'origin'));
|
|
352
405
|
const explicitFailure = explicitCollectionFailure(explicitState, 'refs');
|
|
353
|
-
if (explicitFailure) return explicitFailure;
|
|
354
|
-
if (recordsValue === null) return { state: explicitState, refs: null, absence_confirmed: false };
|
|
406
|
+
if (explicitFailure) return { ...explicitFailure, source };
|
|
407
|
+
if (recordsValue === null) return { state: explicitState, refs: null, absence_confirmed: false, source };
|
|
355
408
|
const refs = recordsValue.map((value) => normalizeReference(value));
|
|
356
409
|
if (refs.some((ref) => !ref)) {
|
|
357
|
-
return { state: 'unknown', refs: null, absence_confirmed: false };
|
|
410
|
+
return { state: 'unknown', refs: null, absence_confirmed: false, source };
|
|
358
411
|
}
|
|
359
412
|
const absenceConfirmed = payloadValue(payload, 'absence_confirmed', 'absenceConfirmed') === true;
|
|
360
413
|
if (refs.length === 0 && !absenceConfirmed) {
|
|
361
|
-
return { state: 'unknown', refs: null, absence_confirmed: false };
|
|
414
|
+
return { state: 'unknown', refs: null, absence_confirmed: false, source };
|
|
362
415
|
}
|
|
363
416
|
return {
|
|
364
417
|
state: explicitState === 'permission_denied' || explicitState === 'api_unavailable' || explicitState === 'conflict'
|
|
365
418
|
? explicitState : refs.length ? 'ready' : 'empty',
|
|
366
419
|
refs,
|
|
367
420
|
absence_confirmed: absenceConfirmed,
|
|
421
|
+
source,
|
|
368
422
|
};
|
|
369
423
|
}
|
|
370
424
|
|
|
@@ -427,7 +481,10 @@ export function statusLabel(value) {
|
|
|
427
481
|
}
|
|
428
482
|
|
|
429
483
|
export function normalizeObjectiveError(error) {
|
|
430
|
-
|
|
484
|
+
// Numeric HTTP status is interpreted below. Do not turn a 403 into the
|
|
485
|
+
// literal state "403", otherwise the status mapping is skipped and a
|
|
486
|
+
// canonical Foundation denial is rendered as a retryable error.
|
|
487
|
+
const code = optionalText(error?.code ?? (typeof error?.status === 'string' ? error.status : undefined)).trim().toLowerCase();
|
|
431
488
|
const status = Number(error?.statusCode ?? error?.status);
|
|
432
489
|
let state = normalizeState(code);
|
|
433
490
|
if (state === UNKNOWN || !state) {
|
|
@@ -461,6 +518,12 @@ function statusNotice(state, callbacks = {}) {
|
|
|
461
518
|
if (status === 'conflict' && normalized.currentRevision) {
|
|
462
519
|
notice.append(makeElement('small', { text: `現在の版: ${normalized.currentRevision}。入力を保持したまま再読込してください。` }));
|
|
463
520
|
}
|
|
521
|
+
if (normalized.checked_at) {
|
|
522
|
+
notice.append(
|
|
523
|
+
makeElement('small', { className: 'objective-editor-readback-meta', text: `正本の確認日時: ${normalized.checked_at}` }),
|
|
524
|
+
makeElement('small', { className: 'objective-editor-readback-meta', text: `出典: ${sourceSummary(normalized.readback_source)}` }),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
464
527
|
if (callbacks.onRetry) notice.append(makeButton('再読込', callbacks.onRetry, 'objective-editor-button-secondary'));
|
|
465
528
|
return notice;
|
|
466
529
|
}
|
|
@@ -605,6 +668,7 @@ function renderObjectiveList(root, state, callbacks) {
|
|
|
605
668
|
}
|
|
606
669
|
if (state.objectives.state === 'empty') {
|
|
607
670
|
section.append(makeElement('div', { className: 'objective-editor-empty', text: '目的はまだ登録されていません。' }));
|
|
671
|
+
if (state.objectives.correctionGuidance) section.append(renderCorrectionGuidance(state.objectives.correctionGuidance));
|
|
608
672
|
if (callbacks.onCreate) section.append(makeButton('目的を作成', callbacks.onCreate, 'objective-editor-button-primary'));
|
|
609
673
|
return section;
|
|
610
674
|
}
|
|
@@ -627,6 +691,23 @@ function renderObjectiveList(root, state, callbacks) {
|
|
|
627
691
|
return section;
|
|
628
692
|
}
|
|
629
693
|
|
|
694
|
+
function renderCorrectionGuidance(guidance) {
|
|
695
|
+
const section = makeElement('section', {
|
|
696
|
+
className: 'objective-editor-correction-guidance',
|
|
697
|
+
attrs: { 'aria-label': '次に確認すること' },
|
|
698
|
+
});
|
|
699
|
+
section.append(makeElement('strong', { text: '確認が必要な項目' }));
|
|
700
|
+
if (guidance?.message) section.append(makeElement('p', { text: guidance.message }));
|
|
701
|
+
const issues = Array.isArray(guidance?.issues) ? guidance.issues : [];
|
|
702
|
+
if (issues.length) {
|
|
703
|
+
const list = makeElement('ul', { className: 'objective-editor-issue-list' });
|
|
704
|
+
for (const issue of issues) list.append(makeElement('li', { text: `${text(issue?.path, '定義')}: ${text(issue?.message, '内容を確認してください。')}` }));
|
|
705
|
+
section.append(list);
|
|
706
|
+
}
|
|
707
|
+
if (guidance?.next_action) section.append(makeElement('p', { className: 'objective-editor-muted', text: `次の対応: ${guidance.next_action}` }));
|
|
708
|
+
return section;
|
|
709
|
+
}
|
|
710
|
+
|
|
630
711
|
function renderReadiness(root, readiness) {
|
|
631
712
|
const section = makeElement('section', { className: 'objective-editor-readiness', attrs: { 'aria-label': '判断利用可能性' } });
|
|
632
713
|
const normalized = normalizeReadiness(readiness);
|
|
@@ -640,7 +721,14 @@ function renderReadiness(root, readiness) {
|
|
|
640
721
|
const list = makeElement('ul', { className: 'objective-editor-issue-list' });
|
|
641
722
|
for (const issue of normalized.issues) list.append(makeElement('li', { text: `${issue.path}: ${issue.message}` }));
|
|
642
723
|
section.append(list);
|
|
643
|
-
}
|
|
724
|
+
}
|
|
725
|
+
if (!normalized.ready && normalized.message) {
|
|
726
|
+
section.append(makeElement('p', { className: 'objective-editor-muted', text: normalized.message }));
|
|
727
|
+
}
|
|
728
|
+
if (!normalized.ready && normalized.next_action) {
|
|
729
|
+
section.append(makeElement('p', { className: 'objective-editor-muted', text: `次の対応: ${normalized.next_action}` }));
|
|
730
|
+
}
|
|
731
|
+
if (normalized.ready) {
|
|
644
732
|
section.append(makeElement('p', { className: 'objective-editor-muted', text: '必要な定義・基準・参照が揃っています。観測値の存在は別途確認します。' }));
|
|
645
733
|
}
|
|
646
734
|
return section;
|
|
@@ -664,6 +752,7 @@ function renderConstraintRefs(root, state, callbacks) {
|
|
|
664
752
|
const item = makeElement('li', { className: 'objective-editor-reference-item' });
|
|
665
753
|
item.append(makeElement('span', { text: `${ref.id}@${ref.revision}` }));
|
|
666
754
|
if (ref.meaning) item.append(makeElement('small', { text: ref.meaning }));
|
|
755
|
+
if (ref.source) item.append(makeElement('small', { className: 'objective-editor-reference-source', text: sourceSummary(ref.source) }));
|
|
667
756
|
if (callbacks.canEdit && callbacks.constraintsEditable !== false && callbacks.onRemoveConstraintRef) item.append(makeButton('外す', () => callbacks.onRemoveConstraintRef(ref), 'objective-editor-button-danger'));
|
|
668
757
|
list.append(item);
|
|
669
758
|
}
|
|
@@ -745,6 +834,9 @@ function renderObjectiveForm(root, state, callbacks) {
|
|
|
745
834
|
const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': '目的を編集' } });
|
|
746
835
|
const draft = state.editor.draft;
|
|
747
836
|
if (!draft) {
|
|
837
|
+
if (FAILED_STATES.includes(state.editor.state)) {
|
|
838
|
+
section.append(statusNotice(state.editor, { onRetry: callbacks.onReload }));
|
|
839
|
+
}
|
|
748
840
|
section.append(makeElement('div', { className: 'objective-editor-empty', text: '編集する目的を選択してください。' }));
|
|
749
841
|
return section;
|
|
750
842
|
}
|
|
@@ -753,6 +845,16 @@ function renderObjectiveForm(root, state, callbacks) {
|
|
|
753
845
|
makeElement('div', { className: 'objective-editor-panel-heading', text: title }),
|
|
754
846
|
makeElement('p', { className: 'objective-editor-muted', text: '保存すると新しい版になり、保存した版を読み戻して表示します。' }),
|
|
755
847
|
);
|
|
848
|
+
if (state.selected) {
|
|
849
|
+
const status = OBJECTIVE_READBACK_STATUS_LABELS[state.selected.readback_status] ?? '未確認';
|
|
850
|
+
const readback = makeElement('div', { className: 'objective-editor-readback', attrs: { 'aria-label': '正本の確認情報' } });
|
|
851
|
+
readback.append(
|
|
852
|
+
makeElement('p', { className: 'objective-editor-muted', text: `正本の状態: ${status}` }),
|
|
853
|
+
makeElement('p', { className: 'objective-editor-muted', text: `確認日時: ${text(state.selected.checked_at)}` }),
|
|
854
|
+
makeElement('p', { className: 'objective-editor-muted', text: `出典: ${sourceSummary(state.selected.source)}` }),
|
|
855
|
+
);
|
|
856
|
+
section.append(readback);
|
|
857
|
+
}
|
|
756
858
|
if (state.save.state !== 'idle' && state.save.state !== 'saving') section.append(statusNotice(state.save, {
|
|
757
859
|
onRetry: ['conflict', 'saved_unverified'].includes(state.save.state) ? null : callbacks.onRetrySave,
|
|
758
860
|
}));
|
|
@@ -932,6 +1034,12 @@ function workspaceStatusNotice(doc, value, { label, onRetry, retryLabel = '再
|
|
|
932
1034
|
if (status === 'conflict' && normalized.currentRevision) {
|
|
933
1035
|
body.append(makeElement('p', { text: `現在の版: ${normalized.currentRevision}。入力は残しています。現在の版を確かめてから直してください。` }));
|
|
934
1036
|
}
|
|
1037
|
+
if (normalized.checked_at) {
|
|
1038
|
+
body.append(
|
|
1039
|
+
makeElement('p', { className: 'bb-objective-readback-meta', text: `正本の確認日時: ${normalized.checked_at}` }),
|
|
1040
|
+
makeElement('p', { className: 'bb-objective-readback-meta', text: `出典: ${sourceSummary(normalized.readback_source)}` }),
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
935
1043
|
if (onRetry) body.append(workspaceButton(doc, { text: retryLabel, variant: 'quiet', onClick: onRetry }));
|
|
936
1044
|
return workspaceNotice(doc, { label, text: body, tone });
|
|
937
1045
|
}
|
|
@@ -979,7 +1087,7 @@ function renderObjectiveLedger(doc, state, callbacks) {
|
|
|
979
1087
|
}
|
|
980
1088
|
const loading = objectives.state === 'loading' || objectives.state === 'idle';
|
|
981
1089
|
const records = loading ? [] : objectives.records ?? [];
|
|
982
|
-
|
|
1090
|
+
const ledger = workspaceLedger(doc, {
|
|
983
1091
|
className: 'bb-objective-ledger',
|
|
984
1092
|
ariaLabel: '目的の一覧',
|
|
985
1093
|
columns: OBJECTIVE_COLUMNS,
|
|
@@ -1000,6 +1108,12 @@ function renderObjectiveLedger(doc, state, callbacks) {
|
|
|
1000
1108
|
};
|
|
1001
1109
|
}),
|
|
1002
1110
|
});
|
|
1111
|
+
if (!loading && objectives.state === 'empty' && objectives.correctionGuidance) {
|
|
1112
|
+
const wrapper = makeElement('div', { className: 'bb-objective-ledger-with-guidance' });
|
|
1113
|
+
wrapper.append(ledger, renderCorrectionGuidance(objectives.correctionGuidance));
|
|
1114
|
+
return wrapper;
|
|
1115
|
+
}
|
|
1116
|
+
return ledger;
|
|
1003
1117
|
}
|
|
1004
1118
|
|
|
1005
1119
|
/**
|
|
@@ -1068,10 +1182,14 @@ function constraintContent(doc, state, callbacks) {
|
|
|
1068
1182
|
const item = makeElement('li');
|
|
1069
1183
|
item.append(makeElement('code', { text: `${ref.id}@${ref.revision}` }));
|
|
1070
1184
|
if (ref.meaning) item.append(makeElement('span', { text: ref.meaning }));
|
|
1185
|
+
if (ref.source) item.append(makeElement('small', { className: 'bb-objective-reference-source', text: sourceSummary(ref.source) }));
|
|
1071
1186
|
list.append(item);
|
|
1072
1187
|
}
|
|
1073
1188
|
content.push(list);
|
|
1074
1189
|
}
|
|
1190
|
+
if (state.constraints.source) {
|
|
1191
|
+
content.push(makeElement('p', { className: 'bb-objective-reference-source', text: `参照元: ${sourceSummary(state.constraints.source)}` }));
|
|
1192
|
+
}
|
|
1075
1193
|
content.push(makeElement('p', {
|
|
1076
1194
|
className: 'bb-objective-note',
|
|
1077
1195
|
text: callbacks.constraintsEditable === false || !callbacks.canEdit
|
|
@@ -1095,6 +1213,8 @@ function readinessContent(doc, readiness) {
|
|
|
1095
1213
|
for (const issue of normalized.issues) list.append(makeElement('li', { text: `${issue.path}: ${issue.message}` }));
|
|
1096
1214
|
content.push(list);
|
|
1097
1215
|
}
|
|
1216
|
+
if (normalized.message) content.push(makeElement('p', { className: 'bb-objective-note', text: normalized.message }));
|
|
1217
|
+
if (normalized.next_action) content.push(makeElement('p', { className: 'bb-objective-note', text: `次の対応: ${normalized.next_action}` }));
|
|
1098
1218
|
return content;
|
|
1099
1219
|
}
|
|
1100
1220
|
|
|
@@ -1115,6 +1235,9 @@ function renderObjectiveDetail(doc, state, callbacks) {
|
|
|
1115
1235
|
['責任者', objective.accountableId],
|
|
1116
1236
|
['採用状態', objective.adoptionState ? (STATUS_LABELS[objective.adoptionState] ?? objective.adoptionState) : null],
|
|
1117
1237
|
['評価期間', periodText(objective.evaluationPeriod)],
|
|
1238
|
+
['正本の状態', OBJECTIVE_READBACK_STATUS_LABELS[objective.readback_status] ?? '未確認'],
|
|
1239
|
+
['確認日時', objective.checked_at],
|
|
1240
|
+
['出典', sourceSummary(objective.source)],
|
|
1118
1241
|
]),
|
|
1119
1242
|
}));
|
|
1120
1243
|
parts.push(workspaceRailBlock(doc, { title: `評価基準${objective.criteria ? ` ${objective.criteria.length}件` : ''}`, className: 'bb-objective-criteria-block', content: criteriaContent(doc, objective.criteria) }));
|
|
@@ -1237,6 +1360,10 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1237
1360
|
view: options.initialView ?? 'list',
|
|
1238
1361
|
connection: port ? 'ready' : 'api_unavailable',
|
|
1239
1362
|
connectionMessage: port ? null : '目的を読み書きする経路が、この画面に渡されていません。',
|
|
1363
|
+
// A project catalog grant is the initial write authority. A canonical
|
|
1364
|
+
// Foundation 403 is stronger evidence for this editor and must make the
|
|
1365
|
+
// currently mounted controls read-only until the host remounts it.
|
|
1366
|
+
permissionDenied: false,
|
|
1240
1367
|
objectives: { state: 'idle', records: null, absence_confirmed: false },
|
|
1241
1368
|
selected: null,
|
|
1242
1369
|
readiness: { state: 'unknown', ready: null, issues: null },
|
|
@@ -1261,12 +1388,16 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1261
1388
|
let renderVersion = 0;
|
|
1262
1389
|
let activeSave = null;
|
|
1263
1390
|
|
|
1391
|
+
function effectiveCanEdit() {
|
|
1392
|
+
return canEdit && !state.permissionDenied;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1264
1395
|
function render() {
|
|
1265
1396
|
if (!root) return null;
|
|
1266
1397
|
const currentRender = ++renderVersion;
|
|
1267
1398
|
const currentEditor = editorVersion;
|
|
1268
1399
|
const callbacks = {
|
|
1269
|
-
canEdit,
|
|
1400
|
+
canEdit: effectiveCanEdit(),
|
|
1270
1401
|
saving: activeSave !== null,
|
|
1271
1402
|
constraintsEditable,
|
|
1272
1403
|
storyLinks,
|
|
@@ -1326,9 +1457,12 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1326
1457
|
state.connection = state.objectives.state === 'ready' || state.objectives.state === 'empty' ? 'ready' : state.objectives.state;
|
|
1327
1458
|
} catch (error) {
|
|
1328
1459
|
if (request !== listRequest) return state.objectives;
|
|
1329
|
-
|
|
1460
|
+
const normalized = normalizeObjectiveError(error);
|
|
1461
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1462
|
+
state.objectives = { state: normalized.state, records: null, absence_confirmed: false, ...normalized };
|
|
1330
1463
|
state.connection = state.objectives.state;
|
|
1331
1464
|
}
|
|
1465
|
+
if (state.objectives.state === 'permission_denied') state.permissionDenied = true;
|
|
1332
1466
|
render();
|
|
1333
1467
|
// As on the organization screens, the workspace opens with the first row selected.
|
|
1334
1468
|
const first = state.objectives.records?.[0];
|
|
@@ -1365,7 +1499,9 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1365
1499
|
}
|
|
1366
1500
|
} catch (error) {
|
|
1367
1501
|
if (!current()) return state.constraints;
|
|
1368
|
-
|
|
1502
|
+
const normalized = normalizeObjectiveError(error);
|
|
1503
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1504
|
+
state.constraints = { state: normalized.state, refs: null, absence_confirmed: false, ...normalized };
|
|
1369
1505
|
}
|
|
1370
1506
|
render();
|
|
1371
1507
|
return state.constraints;
|
|
@@ -1389,7 +1525,9 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1389
1525
|
state.readiness = normalizeReadiness(payload);
|
|
1390
1526
|
} catch (error) {
|
|
1391
1527
|
if (!current()) return state.readiness;
|
|
1392
|
-
|
|
1528
|
+
const normalized = normalizeObjectiveError(error);
|
|
1529
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1530
|
+
state.readiness = { state: normalized.state, ready: null, issues: null, ...normalized };
|
|
1393
1531
|
}
|
|
1394
1532
|
if (state.selected) state.selected.readiness = state.readiness;
|
|
1395
1533
|
render();
|
|
@@ -1425,7 +1563,25 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1425
1563
|
try {
|
|
1426
1564
|
const payload = await method(id, context, revision);
|
|
1427
1565
|
if (version !== selectionVersion) return null;
|
|
1428
|
-
const
|
|
1566
|
+
const expected = { id, revision };
|
|
1567
|
+
const readback = normalizeObjectiveReadback(payload, expected);
|
|
1568
|
+
if (readback && readback.status !== 'ready') {
|
|
1569
|
+
const readbackState = readback.status === 'error' ? 'error_retryable' : 'unknown';
|
|
1570
|
+
const message = OBJECTIVE_READBACK_REASON_MESSAGES[readback.reason?.code] ?? OBJECTIVE_READBACK_STATUS_LABELS[readback.status];
|
|
1571
|
+
state.editor = {
|
|
1572
|
+
...state.editor,
|
|
1573
|
+
state: readbackState,
|
|
1574
|
+
message,
|
|
1575
|
+
readback_status: readback.status,
|
|
1576
|
+
checked_at: readback.checked_at,
|
|
1577
|
+
readback_source: readback.source,
|
|
1578
|
+
readback_reason: readback.reason,
|
|
1579
|
+
};
|
|
1580
|
+
state.connection = readbackState;
|
|
1581
|
+
render();
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
const objective = normalizeObjectiveRecord(payload, expected);
|
|
1429
1585
|
if (!objective) {
|
|
1430
1586
|
state.editor = { ...state.editor, state: 'missing', message: '指定した目的を保存先から確認できません。' };
|
|
1431
1587
|
state.connection = 'missing';
|
|
@@ -1444,6 +1600,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1444
1600
|
} catch (error) {
|
|
1445
1601
|
if (version !== selectionVersion) return null;
|
|
1446
1602
|
const normalized = normalizeObjectiveError(error);
|
|
1603
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1447
1604
|
state.editor = { ...state.editor, state: normalized.state, message: normalized.message, error: normalized.error };
|
|
1448
1605
|
state.connection = normalized.state;
|
|
1449
1606
|
render();
|
|
@@ -1452,6 +1609,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1452
1609
|
}
|
|
1453
1610
|
|
|
1454
1611
|
function beginCreate() {
|
|
1612
|
+
if (!effectiveCanEdit()) return;
|
|
1455
1613
|
// A repeated click on New must not throw away an in-progress draft.
|
|
1456
1614
|
if (state.editor.mode === 'create' && state.railMode === 'form') return;
|
|
1457
1615
|
selectionVersion += 1;
|
|
@@ -1475,6 +1633,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1475
1633
|
|
|
1476
1634
|
/** Workspace layout: open the edit form for the selected objective in the rail. */
|
|
1477
1635
|
function beginEdit() {
|
|
1636
|
+
if (!effectiveCanEdit()) return;
|
|
1478
1637
|
if (!state.selected) return;
|
|
1479
1638
|
if (state.railMode === 'form' && state.editor.mode === 'edit') return;
|
|
1480
1639
|
editorVersion += 1;
|
|
@@ -1519,7 +1678,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1519
1678
|
}
|
|
1520
1679
|
|
|
1521
1680
|
function addConstraintRef(reference) {
|
|
1522
|
-
if (!
|
|
1681
|
+
if (!effectiveCanEdit() || !constraintsEditable || activeSave) return;
|
|
1523
1682
|
const normalized = normalizeReference(reference, 'constraint');
|
|
1524
1683
|
if (!normalized || normalized.type !== 'constraint') return;
|
|
1525
1684
|
const refs = state.editor.constraintRefs ?? [];
|
|
@@ -1530,7 +1689,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1530
1689
|
}
|
|
1531
1690
|
|
|
1532
1691
|
function removeConstraintRef(reference) {
|
|
1533
|
-
if (!
|
|
1692
|
+
if (!effectiveCanEdit() || !constraintsEditable || activeSave) return;
|
|
1534
1693
|
state.editor.constraintRefs = (state.editor.constraintRefs ?? []).filter((item) => !(item.id === reference.id && item.revision === reference.revision));
|
|
1535
1694
|
state.editor.constraintRefsChanged = true;
|
|
1536
1695
|
render();
|
|
@@ -1538,7 +1697,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1538
1697
|
|
|
1539
1698
|
async function saveObjective(definition) {
|
|
1540
1699
|
if (!definition) return null;
|
|
1541
|
-
if (!
|
|
1700
|
+
if (!effectiveCanEdit()) return { state: 'permission_denied', message: 'この画面では目的を保存できません。' };
|
|
1542
1701
|
if (activeSave) return { state: 'saving', action: activeSave.mode };
|
|
1543
1702
|
// Once a mutation succeeded, repeating it is unsafe until the owner has
|
|
1544
1703
|
// reloaded and selected the canonical revision again.
|
|
@@ -1636,6 +1795,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1636
1795
|
return result;
|
|
1637
1796
|
} catch (error) {
|
|
1638
1797
|
const normalized = normalizeObjectiveError(error);
|
|
1798
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1639
1799
|
return publish(mutationCompleted
|
|
1640
1800
|
? { state: 'saved_unverified', message: '目的は保存されましたが、保存結果を確かめられません。一覧を再読込して確かめてください。', mutation, error: normalized.error, draft: definition }
|
|
1641
1801
|
: { state: normalized.state, message: normalized.message, currentRevision: normalized.currentRevision, error: normalized.error, draft: definition });
|
|
@@ -1658,7 +1818,9 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1658
1818
|
try {
|
|
1659
1819
|
state.story = { ...normalizeStoryObjectiveLinks(await method(storyId, context)), storyId };
|
|
1660
1820
|
} catch (error) {
|
|
1661
|
-
|
|
1821
|
+
const normalized = normalizeObjectiveError(error);
|
|
1822
|
+
state.permissionDenied ||= normalized.state === 'permission_denied';
|
|
1823
|
+
state.story = { state: normalized.state, storyId, links: null, absence_confirmed: false, ...normalized };
|
|
1662
1824
|
}
|
|
1663
1825
|
render();
|
|
1664
1826
|
return state.story;
|