@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/outcome-knowledge.js
CHANGED
|
@@ -296,7 +296,7 @@ export function renderKnowledgeDestination(root, model = {}, callbacks = {}) {
|
|
|
296
296
|
section.append(makeElement('p', { className: 'knowledge-inline-status', text: '保存先を読み込んでいます。', attrs: { role: 'status' } }));
|
|
297
297
|
}
|
|
298
298
|
if (model.state === 'conflict') {
|
|
299
|
-
section.append(statusNotice(
|
|
299
|
+
section.append(statusNotice(model, { retry: Boolean(callbacks.onReload), onRetry: callbacks.onReload, retryLabel: '最新版を確認' }));
|
|
300
300
|
} else if (['permission_denied', 'error_retryable', 'unknown'].includes(model.state)) {
|
|
301
301
|
section.append(statusNotice(model, { retry: Boolean(callbacks.onReload), onRetry: callbacks.onReload }));
|
|
302
302
|
} else if (model.state === 'verified') {
|
|
@@ -417,13 +417,97 @@ export function normalizeKnowledgeCollection(payload) {
|
|
|
417
417
|
};
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
function isRecord(value) {
|
|
421
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function conflictMetadataFrom(value) {
|
|
425
|
+
if (!isRecord(value)) return {};
|
|
426
|
+
// A request body or locally retained draft is not evidence of the server's
|
|
427
|
+
// current version. Only explicit latest/current fields from the response
|
|
428
|
+
// may be rendered as the latest value. This keeps a submitted revision
|
|
429
|
+
// from being relabelled as the server's current revision after a 409.
|
|
430
|
+
const sources = [
|
|
431
|
+
value,
|
|
432
|
+
value.details,
|
|
433
|
+
value.conflict,
|
|
434
|
+
value.payload,
|
|
435
|
+
value.latest,
|
|
436
|
+
value.current,
|
|
437
|
+
].filter(isRecord);
|
|
438
|
+
const read = (...keys) => {
|
|
439
|
+
for (const source of sources) {
|
|
440
|
+
const found = fieldValue(source, ...keys);
|
|
441
|
+
if (found !== undefined && found !== null && optionalText(found).trim() !== '') return found;
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
const readNestedCurrent = (key) => {
|
|
446
|
+
for (const source of [value.latest, value.current]) {
|
|
447
|
+
if (!isRecord(source)) continue;
|
|
448
|
+
const found = fieldValue(source, key);
|
|
449
|
+
if (found !== undefined && found !== null && optionalText(found).trim() !== '') return found;
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
};
|
|
453
|
+
const latestVersion = read('latest_version', 'latestVersion', 'current_version', 'currentVersion') ?? readNestedCurrent('version');
|
|
454
|
+
const latestRevision = read('latest_revision', 'latestRevision', 'current_revision', 'currentRevision') ?? readNestedCurrent('revision');
|
|
455
|
+
const expectedVersion = read('expected_version', 'expectedVersion');
|
|
456
|
+
const expectedRevision = read('expected_revision', 'expectedRevision');
|
|
457
|
+
const reason = read('conflict_reason', 'conflictReason', 'reason', 'detail', 'message');
|
|
458
|
+
const result = {};
|
|
459
|
+
if (latestVersion !== undefined) result.latest_version = latestVersion;
|
|
460
|
+
if (latestRevision !== undefined) result.latest_revision = latestRevision;
|
|
461
|
+
if (expectedVersion !== undefined) result.expected_version = expectedVersion;
|
|
462
|
+
if (expectedRevision !== undefined) result.expected_revision = expectedRevision;
|
|
463
|
+
if (reason !== undefined && optionalText(reason).trim() !== optionalText(value.code ?? value.message).trim()) result.conflict_reason = reason;
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function draftRecordFrom(value) {
|
|
468
|
+
if (!isRecord(value)) return null;
|
|
469
|
+
if (isRecord(value.draft)) return value.draft;
|
|
470
|
+
if (isRecord(value.data?.draft)) return value.data.draft;
|
|
471
|
+
if (isRecord(value.data)) return value.data;
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function draftReadbackFrom(value, expectedDraftId = null) {
|
|
476
|
+
const draft = draftRecordFrom(value);
|
|
477
|
+
if (!draft) return { valid: false, reason: 'payload_missing' };
|
|
478
|
+
const draftId = fieldValue(draft, 'draft_id', 'draftId', 'id');
|
|
479
|
+
if (expectedDraftId === null || expectedDraftId === undefined || optionalText(expectedDraftId).trim() === '') return { valid: false, reason: 'expected_id_missing', draftId };
|
|
480
|
+
if (draftId === undefined || draftId === null || optionalText(draftId).trim() === '') return { valid: false, reason: 'response_id_missing', draftId: null };
|
|
481
|
+
if (String(draftId) !== String(expectedDraftId)) return { valid: false, reason: 'response_id_mismatch', draftId };
|
|
482
|
+
const revision = fieldValue(draft, 'revision', 'version');
|
|
483
|
+
const content = fieldValue(draft, 'content', 'input_text', 'text');
|
|
484
|
+
const source = fieldValue(draft, 'source', 'provenance');
|
|
485
|
+
return {
|
|
486
|
+
valid: true,
|
|
487
|
+
draft_id: String(draftId),
|
|
488
|
+
revision: revision === undefined || revision === null || optionalText(revision).trim() === '' ? null : revision,
|
|
489
|
+
...(content !== undefined && content !== null ? { content: String(content) } : {}),
|
|
490
|
+
...(isRecord(source) ? { source } : {}),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
420
495
|
function errorFromPayload(payload, status = null) {
|
|
421
|
-
const
|
|
422
|
-
const
|
|
496
|
+
const body = isRecord(payload) ? payload : {};
|
|
497
|
+
const source = isRecord(body.error) ? body.error : body;
|
|
498
|
+
const rawCode = optionalText(fieldValue(source, 'code', 'error_code', 'type')).trim();
|
|
499
|
+
const rawState = optionalText(fieldValue(source, 'state', 'status')).trim().toLowerCase();
|
|
500
|
+
const permissionFailure = Number(status) === 403
|
|
501
|
+
|| ['forbidden', 'permission_denied', 'permission_insufficient', 'not_allowed', 'read_only_permission_denied', 'permission_decision_denied'].includes(rawCode.toLowerCase())
|
|
502
|
+
|| ['forbidden', 'permission_denied', 'permission_insufficient', 'not_allowed', 'read_only_permission_denied', 'permission_decision_denied'].includes(rawState);
|
|
503
|
+
const code = permissionFailure ? 'permission_denied' : rawCode || 'unknown_upstream_error';
|
|
423
504
|
const error = new Error(code);
|
|
424
505
|
error.code = code;
|
|
425
|
-
error.status = status ?? fieldValue(source, 'status', 'http_status');
|
|
506
|
+
error.status = status ?? fieldValue(source, 'status', 'http_status') ?? fieldValue(body, 'status', 'http_status');
|
|
426
507
|
error.detail = fieldValue(source, 'message', 'detail', 'reason');
|
|
508
|
+
error.next_action = fieldValue(source, 'next_action', 'nextAction') ?? fieldValue(body, 'next_action', 'nextAction');
|
|
509
|
+
error.payload = body;
|
|
510
|
+
Object.assign(error, conflictMetadataFrom({ ...body, ...source, code, payload: body, details: source.details ?? body.details, conflict: source.conflict ?? body.conflict }));
|
|
427
511
|
return error;
|
|
428
512
|
}
|
|
429
513
|
|
|
@@ -443,6 +527,25 @@ export async function callKnowledgeApi(api, path, init = {}) {
|
|
|
443
527
|
return await unwrapResponse(await api(path, init));
|
|
444
528
|
} catch (caught) {
|
|
445
529
|
if (caught?.code) throw caught;
|
|
530
|
+
const status = caught?.status ?? caught?.response?.status ?? null;
|
|
531
|
+
const rawPayload = caught?.payload
|
|
532
|
+
?? caught?.response?.data
|
|
533
|
+
?? caught?.response?.payload
|
|
534
|
+
?? caught?.body;
|
|
535
|
+
const hasStatus = status !== null && status !== undefined;
|
|
536
|
+
if (hasStatus || rawPayload !== undefined) {
|
|
537
|
+
const payload = isRecord(rawPayload)
|
|
538
|
+
? rawPayload
|
|
539
|
+
: { message: caught instanceof Error ? caught.message : String(caught) };
|
|
540
|
+
const error = errorFromPayload(payload, status);
|
|
541
|
+
if (!error.detail && caught instanceof Error && caught.message) error.detail = caught.message;
|
|
542
|
+
// ORG apiMutation throws a plain Error with status/payload but no code.
|
|
543
|
+
// Keep the original object so callers can inspect the upstream failure
|
|
544
|
+
// while the normalized error preserves conflict/permission metadata.
|
|
545
|
+
error.cause = caught;
|
|
546
|
+
if (rawPayload !== undefined) error.payload = rawPayload;
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
446
549
|
const error = new Error(caught instanceof Error ? caught.message : String(caught));
|
|
447
550
|
error.code = 'unknown_upstream_error';
|
|
448
551
|
error.cause = caught;
|
|
@@ -504,24 +607,52 @@ function operationKey(name, id, version, body = {}) {
|
|
|
504
607
|
|
|
505
608
|
function apiErrorText(error) {
|
|
506
609
|
const code = optionalText(error?.code || error?.message) || 'unknown_upstream_error';
|
|
610
|
+
if (isVersionConflict(error)) return '保存先の最新版と競合しました。';
|
|
611
|
+
const reason = optionalText(error?.detail).trim();
|
|
612
|
+
const nextAction = optionalText(error?.next_action ?? error?.nextAction).trim();
|
|
613
|
+
const appendNextAction = (message) => `${message}${nextAction ? ` 次の操作: ${nextAction}` : ''}`;
|
|
507
614
|
const messages = {
|
|
508
615
|
authentication_required: 'ログインが必要です。セッションを確認してください。',
|
|
509
616
|
project_not_allowed: 'このプロジェクトを確認する権限がありません。',
|
|
510
617
|
knowledge_project_not_accessible: 'このプロジェクトの知識を確認する権限がありません。',
|
|
511
|
-
permission_denied: '
|
|
512
|
-
forbidden: '
|
|
513
|
-
version_conflict: '
|
|
618
|
+
permission_denied: 'この役割では対象範囲の知識を確認できません。',
|
|
619
|
+
forbidden: 'この役割では対象範囲の知識を確認できません。',
|
|
620
|
+
version_conflict: '保存先の最新版と競合しました。',
|
|
514
621
|
readback_mismatch: '保存後の正本確認が一致しませんでした。未完了として再確認してください。',
|
|
515
622
|
upstream_unavailable: '知識APIに接続できません。入力を保持して再試行してください。',
|
|
516
623
|
unknown_upstream_error: '知識APIの結果を確認できません。入力を保持して再試行してください。',
|
|
517
624
|
api_unavailable: '知識APIが未接続です。接続を確認してください。',
|
|
518
625
|
project_context_missing: 'プロジェクトが選択されていません。',
|
|
519
626
|
};
|
|
520
|
-
|
|
627
|
+
const message = messages[code] ?? `知識APIを確認できません(${code})。入力を保持して再試行してください。`;
|
|
628
|
+
if (code === 'permission_denied' || code === 'forbidden') return appendNextAction(reason ? `${message} ${reason}` : message);
|
|
629
|
+
return appendNextAction(message);
|
|
521
630
|
}
|
|
522
631
|
|
|
523
632
|
function isVersionConflict(error) {
|
|
524
|
-
return
|
|
633
|
+
return Number(error?.status) === 409
|
|
634
|
+
|| ['version_conflict', 'knowledge_draft_revision_conflict', 'knowledge_lifecycle_version_conflict', 'knowledge_save_idempotency_conflict', 'knowledge_draft_not_editable', 'destination_revision_conflict'].includes(error?.code);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function conflictNoticeVersion(model) {
|
|
638
|
+
const metadata = conflictMetadataFrom(model);
|
|
639
|
+
const latest = metadata.latest_version ?? metadata.latest_revision;
|
|
640
|
+
const expected = metadata.expected_version ?? metadata.expected_revision;
|
|
641
|
+
return {
|
|
642
|
+
latest: latest === undefined ? null : latest,
|
|
643
|
+
expected: expected === undefined ? null : expected,
|
|
644
|
+
reason: metadata.conflict_reason,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function pendingDraftUpdateReadyForReapply(pending) {
|
|
649
|
+
if (!isRecord(pending)) return false;
|
|
650
|
+
const draftId = optionalText(pending.draftId ?? pending.draft_id).trim();
|
|
651
|
+
const revision = optionalText(pending.latest_revision).trim();
|
|
652
|
+
const latest = pending.latest_draft_metadata;
|
|
653
|
+
const latestDraftId = optionalText(latest?.draft_id ?? latest?.draftId).trim();
|
|
654
|
+
const latestRevision = optionalText(latest?.revision).trim();
|
|
655
|
+
return Boolean(draftId && revision && latestDraftId && latestDraftId === draftId && latestRevision === revision);
|
|
525
656
|
}
|
|
526
657
|
|
|
527
658
|
function statusNotice(model, options = {}) {
|
|
@@ -531,8 +662,48 @@ function statusNotice(model, options = {}) {
|
|
|
531
662
|
const title = stateLabel(state);
|
|
532
663
|
const detail = model?.message || model?.error || model?.reason;
|
|
533
664
|
append(notice, makeElement('strong', { text: title }), makeElement('span', { text: detail || options.detail || '' }));
|
|
665
|
+
if (state === 'conflict') {
|
|
666
|
+
const conflict = conflictNoticeVersion(model);
|
|
667
|
+
if (conflict.reason && conflict.reason !== detail) notice.append(makeElement('span', { className: 'knowledge-conflict-detail', text: `競合理由: ${text(conflict.reason)}` }));
|
|
668
|
+
if (conflict.latest !== null) notice.append(makeElement('span', { className: 'knowledge-conflict-detail', text: `最新版: 版 ${text(conflict.latest)}` }));
|
|
669
|
+
if (conflict.expected !== null) notice.append(makeElement('span', { className: 'knowledge-conflict-detail', text: `送信時の版: 版 ${text(conflict.expected)}` }));
|
|
670
|
+
notice.append(makeElement('span', { className: 'knowledge-conflict-detail', text: '入力と出典は保持しています。最新版を確認してから、必要なら再適用してください。' }));
|
|
671
|
+
const pending = model?.pending_draft_update;
|
|
672
|
+
const latest = pending?.latest_draft_metadata;
|
|
673
|
+
const localInput = pending?.local_input_text;
|
|
674
|
+
if (latest && (latest.content !== undefined || localInput !== undefined)) {
|
|
675
|
+
const comparison = makeElement('div', { className: 'knowledge-comparison knowledge-conflict-comparison' });
|
|
676
|
+
const latestColumn = makeElement('section', { className: 'knowledge-source-column' });
|
|
677
|
+
append(latestColumn,
|
|
678
|
+
makeElement('h4', { text: `確認した最新版${latest.revision !== undefined && latest.revision !== null ? `(版 ${text(latest.revision)})` : ''}` }),
|
|
679
|
+
makeElement('p', { className: 'knowledge-long-text', text: latest.content !== undefined ? latest.content : '最新版の本文を確認できません。' }),
|
|
680
|
+
);
|
|
681
|
+
const latestSource = latest.source;
|
|
682
|
+
const latestSourceLabel = latestSource && typeof latestSource === 'object'
|
|
683
|
+
? [sourceKindLabel(latestSource.kind), sourcePointer(latestSource)].filter((value) => optionalText(value).trim()).join(' / ')
|
|
684
|
+
: optionalText(latestSource).trim();
|
|
685
|
+
if (latestSourceLabel) latestColumn.append(makeElement('p', { className: 'knowledge-conflict-detail', text: `確認した最新版の出典: ${latestSourceLabel}` }));
|
|
686
|
+
const localColumn = makeElement('section', { className: 'knowledge-proposal-column' });
|
|
687
|
+
append(localColumn,
|
|
688
|
+
makeElement('h4', { text: '入力中の原文(保持)' }),
|
|
689
|
+
makeElement('p', { className: 'knowledge-long-text', text: localInput !== undefined ? localInput : '入力中の原文を確認できません。' }),
|
|
690
|
+
);
|
|
691
|
+
const localSource = pending.local_source;
|
|
692
|
+
const sourceLabelText = localSource && typeof localSource === 'object'
|
|
693
|
+
? [sourceKindLabel(localSource.kind), sourcePointer(localSource)].filter((value) => optionalText(value).trim()).join(' / ')
|
|
694
|
+
: optionalText(localSource).trim();
|
|
695
|
+
if (sourceLabelText) localColumn.append(makeElement('p', { className: 'knowledge-conflict-detail', text: `保持中の出典: ${sourceLabelText}` }));
|
|
696
|
+
append(comparison, latestColumn, localColumn);
|
|
697
|
+
notice.append(comparison);
|
|
698
|
+
}
|
|
699
|
+
if (pendingDraftUpdateReadyForReapply(pending) && typeof options.onReapply === 'function') {
|
|
700
|
+
const reapply = makeElement('button', { className: 'knowledge-button primary', text: options.reapplyLabel ?? '保持した内容を再適用して保存', attrs: { type: 'button' } });
|
|
701
|
+
reapply.addEventListener('click', () => options.onReapply());
|
|
702
|
+
notice.append(reapply);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
534
705
|
if (options.retry && typeof options.onRetry === 'function') {
|
|
535
|
-
const retry = makeElement('button', { className: 'knowledge-button secondary', text: '再試行', attrs: { type: 'button' } });
|
|
706
|
+
const retry = makeElement('button', { className: 'knowledge-button secondary', text: options.retryLabel ?? '再試行', attrs: { type: 'button' } });
|
|
536
707
|
retry.addEventListener('click', () => options.onRetry());
|
|
537
708
|
notice.append(retry);
|
|
538
709
|
}
|
|
@@ -616,12 +787,14 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
616
787
|
const searchedScope = Array.isArray(collection.searched_scope) && collection.searched_scope.length
|
|
617
788
|
? collection.searched_scope.map(scopeLabel).join(' / ')
|
|
618
789
|
: null;
|
|
790
|
+
const state = normaliseStatus(collection.state);
|
|
791
|
+
const countKnown = Array.isArray(collection.records)
|
|
792
|
+
&& ['ready', 'results', EMPTY].includes(state);
|
|
619
793
|
const section = makeElement('section', { className: 'knowledge-list-panel', attrs: { 'aria-labelledby': 'knowledge-list-title' } });
|
|
620
794
|
const header = makeElement('div', { className: 'knowledge-panel-head' });
|
|
621
|
-
append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text:
|
|
795
|
+
append(header, makeElement('div', {},), makeElement('span', { className: 'knowledge-count', text: countKnown ? `${collection.records.length}件` : UNKNOWN }));
|
|
622
796
|
header.firstChild.append(makeElement('h3', { text: '知識・判断', attrs: { id: 'knowledge-list-title' } }), makeElement('p', { text: searchedScope ? `検索範囲: ${searchedScope}` : '検索範囲: 未確認' }));
|
|
623
797
|
section.append(header);
|
|
624
|
-
const state = normaliseStatus(collection.state);
|
|
625
798
|
if (state === 'loading') {
|
|
626
799
|
const skeleton = makeElement('div', { className: 'knowledge-list-rows', attrs: { 'aria-label': '知識を読み込み中' } });
|
|
627
800
|
for (let index = 0; index < 4; index += 1) skeleton.append(skeletonRow());
|
|
@@ -629,7 +802,13 @@ export function renderKnowledgeList(root, model = {}, callbacks = {}) {
|
|
|
629
802
|
} else if (state === EMPTY) {
|
|
630
803
|
section.append(makeElement('div', { className: 'knowledge-empty', text: 'この条件に一致する知識はありません。自然文から登録案を作成できます。' }));
|
|
631
804
|
} else if (state === 'permission_denied') {
|
|
632
|
-
|
|
805
|
+
// Keep the upstream reason and recovery action visible. Replacing the
|
|
806
|
+
// payload with a generic detail makes a denied scope look like an
|
|
807
|
+
// unexplained empty result and leaves the user without a next step.
|
|
808
|
+
const permissionModel = collection.message
|
|
809
|
+
? collection
|
|
810
|
+
: { ...collection, message: '必要なプロジェクト権限を確認してください。' };
|
|
811
|
+
section.append(statusNotice(permissionModel, { retry: true, onRetry: callbacks.onRetry }));
|
|
633
812
|
} else if (['error', 'error_retryable'].includes(state)) {
|
|
634
813
|
section.append(statusNotice(collection, { retry: true, onRetry: callbacks.onRetry }));
|
|
635
814
|
} else if (state === 'unknown' || !Array.isArray(collection.records)) {
|
|
@@ -765,18 +944,29 @@ function formField(labelText, control, hint = '') {
|
|
|
765
944
|
export function renderDraftCapture(root, model = {}, callbacks = {}) {
|
|
766
945
|
clear(root);
|
|
767
946
|
const draft = model.draft ?? model;
|
|
947
|
+
const editLocked = callbacks.editLocked === true;
|
|
768
948
|
const section = makeElement('section', { className: 'knowledge-capture-panel', attrs: { 'aria-labelledby': 'knowledge-capture-title' } });
|
|
769
949
|
append(section, makeElement('span', { className: 'knowledge-eyebrow', text: '登録案を作る' }), makeElement('h3', { text: '自然文から未確定候補を作成', attrs: { id: 'knowledge-capture-title' } }), makeElement('p', { text: '入力した文章や許可資料から提案を作ります。提案は確認が終わるまで正本へ保存されません。' }));
|
|
770
950
|
const form = makeElement('form', { className: 'knowledge-capture-form', attrs: { 'aria-busy': ['loading', 'saving'].includes(normaliseStatus(draft.state)) ? 'true' : 'false' } });
|
|
771
951
|
const resumeId = makeElement('input', { className: 'knowledge-control', value: draft.resume_id ?? '', attrs: { name: 'resume_id', maxlength: '200', placeholder: '保存済み下書きID' } });
|
|
772
|
-
const resume = makeElement('button', { className: 'knowledge-button secondary', text: '下書きを再開', attrs: { type: 'button' } });
|
|
773
|
-
resume.addEventListener('click', () => callbacks.onResume?.(resumeId.value.trim()));
|
|
952
|
+
const resume = makeElement('button', { className: 'knowledge-button secondary', text: '下書きを再開', attrs: { type: 'button' }, disabled: editLocked });
|
|
953
|
+
resume.addEventListener('click', () => { if (!editLocked) callbacks.onResume?.(resumeId.value.trim()); });
|
|
774
954
|
const resumeRow = makeElement('div', { className: 'knowledge-form-actions' });
|
|
775
955
|
resumeRow.append(resume);
|
|
776
956
|
const sourceText = optionalText(draft.input_text ?? draft.text ?? model.input_text);
|
|
777
957
|
const input = makeElement('textarea', { className: 'knowledge-control knowledge-textarea', value: sourceText, attrs: { name: 'input_text', rows: '8', maxlength: '20000', required: 'required', placeholder: '例: 顧客への初回提案では、まず利用目的と判断期限を確認する。' } });
|
|
778
958
|
const sourcePointerInput = makeElement('input', { className: 'knowledge-control', value: draft.source?.pointer ?? draft.source_pointer ?? '', attrs: { name: 'source_pointer', maxlength: '2048', placeholder: '許可された資料のURLまたはrepo相対参照(任意)' } });
|
|
779
959
|
const sourceKindInput = makeElement('input', { className: 'knowledge-control', value: draft.source?.kind ?? draft.source_kind ?? '', attrs: { name: 'source_kind', maxlength: '120', placeholder: '資料種別(任意)' } });
|
|
960
|
+
for (const control of [resumeId, input, sourcePointerInput, sourceKindInput]) control.disabled = editLocked;
|
|
961
|
+
const readDraftInput = () => ({
|
|
962
|
+
input_text: input.value,
|
|
963
|
+
source: { pointer: sourcePointerInput.value.trim(), kind: sourceKindInput.value.trim() },
|
|
964
|
+
});
|
|
965
|
+
for (const control of [input, sourcePointerInput, sourceKindInput]) {
|
|
966
|
+
const syncInput = () => { if (!editLocked) callbacks.onInput?.(readDraftInput()); };
|
|
967
|
+
control.addEventListener('input', syncInput);
|
|
968
|
+
control.addEventListener('change', syncInput);
|
|
969
|
+
}
|
|
780
970
|
append(form,
|
|
781
971
|
formField('保存済み下書きを再開', resumeId, '一覧APIはないため、保存時に表示された下書きIDを入力してください。'),
|
|
782
972
|
resumeRow,
|
|
@@ -788,20 +978,17 @@ export function renderDraftCapture(root, model = {}, callbacks = {}) {
|
|
|
788
978
|
if (savedId) form.append(makeElement('p', { className: 'knowledge-inline-status', text: `保存済み下書きID: ${savedId}${draft.revision ?? draft.version ? ` / 版: ${draft.revision ?? draft.version}` : ''}` }));
|
|
789
979
|
if (savedId && draft.resume_url) form.append(makeElement('a', { className: 'knowledge-button secondary', text: 'この下書きの再開リンク', attrs: { href: draft.resume_url } }));
|
|
790
980
|
const actions = makeElement('div', { className: 'knowledge-form-actions' });
|
|
791
|
-
const submit = makeElement('button', { className: 'knowledge-button primary', text: normaliseStatus(draft.state) === 'saving' ? 'AIで整理中…' : 'AIで整理', attrs: { type: 'submit' }, disabled: normaliseStatus(draft.state) === 'saving' });
|
|
792
|
-
const discard = makeElement('button', { className: 'knowledge-button secondary', text: '入力を破棄', attrs: { type: 'button' } });
|
|
981
|
+
const submit = makeElement('button', { className: 'knowledge-button primary', text: normaliseStatus(draft.state) === 'saving' ? 'AIで整理中…' : 'AIで整理', attrs: { type: 'submit' }, disabled: editLocked || normaliseStatus(draft.state) === 'saving' });
|
|
982
|
+
const discard = makeElement('button', { className: 'knowledge-button secondary', text: '入力を破棄', attrs: { type: 'button' }, disabled: editLocked });
|
|
793
983
|
discard.addEventListener('click', () => callbacks.onDiscard?.());
|
|
794
984
|
actions.append(submit, discard);
|
|
795
985
|
form.append(actions);
|
|
796
|
-
if (['error', 'error_retryable', 'permission_denied', 'conflict', 'unknown'].includes(normaliseStatus(draft.state))) form.append(statusNotice(
|
|
986
|
+
if (['error', 'error_retryable', 'permission_denied', 'conflict', 'unknown'].includes(normaliseStatus(draft.state))) form.append(statusNotice(draft, { retry: !editLocked && Boolean(callbacks.onRetry), onRetry: editLocked ? null : callbacks.onRetry }));
|
|
797
987
|
if (draft.state === 'saving') form.append(makeElement('p', { className: 'knowledge-inline-status', text: '候補生成の応答を待っています。入力は保持されます。', attrs: { role: 'status' } }));
|
|
798
988
|
form.addEventListener('submit', (event) => {
|
|
799
989
|
event.preventDefault();
|
|
800
990
|
if (typeof form.reportValidity === 'function' && !form.reportValidity()) return;
|
|
801
|
-
callbacks.onSubmit?.(
|
|
802
|
-
input_text: input.value,
|
|
803
|
-
source: { pointer: sourcePointerInput.value.trim(), kind: sourceKindInput.value.trim() },
|
|
804
|
-
});
|
|
991
|
+
if (!editLocked) callbacks.onSubmit?.(readDraftInput());
|
|
805
992
|
});
|
|
806
993
|
section.append(form);
|
|
807
994
|
root.append(section);
|
|
@@ -816,6 +1003,7 @@ function candidateShape(model) {
|
|
|
816
1003
|
export function renderCandidateReview(root, model = {}, callbacks = {}) {
|
|
817
1004
|
clear(root);
|
|
818
1005
|
const candidate = candidateShape(model);
|
|
1006
|
+
const editLocked = callbacks.editLocked === true;
|
|
819
1007
|
const section = makeElement('section', { className: 'knowledge-review-panel', attrs: { 'aria-labelledby': 'knowledge-review-title' } });
|
|
820
1008
|
append(section, makeElement('span', { className: 'knowledge-eyebrow', text: '確認が必要です' }), makeElement('h3', { text: '未確定候補を確認', attrs: { id: 'knowledge-review-title' } }), makeElement('p', { text: '候補はAIの提案です。根拠のない責任者・事実を確定せず、原文と提案の差分を確認してください。' }));
|
|
821
1009
|
if (model.canonical === false || model.persisted === false || model.readback?.state === 'proposal_only') {
|
|
@@ -875,17 +1063,20 @@ export function renderCandidateReview(root, model = {}, callbacks = {}) {
|
|
|
875
1063
|
createLabel.append(create, makeElement('span', { text: '別の正本項目として作成' }));
|
|
876
1064
|
const canonicalId = makeElement('input', { className: 'knowledge-control', value: candidate.canonical_id ?? model.canonical_id ?? '', attrs: { name: 'canonical_id', maxlength: '240', placeholder: '再利用する正本ID' } });
|
|
877
1065
|
const canonicalVersion = makeElement('input', { className: 'knowledge-control', value: candidate.canonical_version ?? candidate.expected_version ?? model.canonical_version ?? '', attrs: { name: 'canonical_version', maxlength: '240', placeholder: '再利用する正本の現在版' } });
|
|
878
|
-
for (const control of [type, title, summary, scope, decisionDomain, owner, applicability, relationType, relations, reuse, create, canonicalId, canonicalVersion]) control.disabled = !proposalApplied;
|
|
1066
|
+
for (const control of [type, title, summary, scope, decisionDomain, owner, applicability, relationType, relations, reuse, create, canonicalId, canonicalVersion]) control.disabled = !proposalApplied || editLocked;
|
|
879
1067
|
append(form, makeElement('fieldset', { className: 'knowledge-choice-group' }));
|
|
880
1068
|
form.lastChild.append(makeElement('legend', { text: '保存方法' }), reuseLabel, createLabel);
|
|
881
1069
|
form.append(formField('再利用する正本', canonicalId, '既存の正本候補を再利用する場合は正本IDが必要です。'));
|
|
882
1070
|
form.append(formField('再利用する正本の版', canonicalVersion, '並行更新を見逃さないよう、確認した現在版を指定します。'));
|
|
883
1071
|
const actions = makeElement('div', { className: 'knowledge-form-actions' });
|
|
884
|
-
const apply = makeElement('button', { className: 'knowledge-button secondary', text: proposalApplied ? '入力欄へ反映済み' : '入力欄に反映', attrs: { type: 'button' }, disabled: proposalApplied || !callbacks.onApply });
|
|
885
|
-
apply.addEventListener('click', () => callbacks.onApply?.());
|
|
886
|
-
const save = makeElement('button', { className: 'knowledge-button secondary', text: '下書き保存', attrs: { type: 'submit' }, disabled: !proposalApplied || !callbacks.onSave });
|
|
887
|
-
const commit = makeElement('button', { className: 'knowledge-button primary', text: '確認して正本へ保存', attrs: { type: 'button' }, disabled: !callbacks.onCommit });
|
|
1072
|
+
const apply = makeElement('button', { className: 'knowledge-button secondary', text: proposalApplied ? '入力欄へ反映済み' : '入力欄に反映', attrs: { type: 'button' }, disabled: editLocked || proposalApplied || !callbacks.onApply });
|
|
1073
|
+
apply.addEventListener('click', () => { if (!editLocked) callbacks.onApply?.(); });
|
|
1074
|
+
const save = makeElement('button', { className: 'knowledge-button secondary', text: '下書き保存', attrs: { type: 'submit' }, disabled: editLocked || !proposalApplied || !callbacks.onSave });
|
|
1075
|
+
const commit = makeElement('button', { className: 'knowledge-button primary', text: '確認して正本へ保存', attrs: { type: 'button' }, disabled: editLocked || !callbacks.onCommit });
|
|
888
1076
|
actions.append(apply, save, commit); form.append(actions);
|
|
1077
|
+
// The retry action is a recovery control, not an editor. Keep it available
|
|
1078
|
+
// while the fields and mutation buttons are locked so the user can finish a
|
|
1079
|
+
// same-draft readback without losing the local candidate edits.
|
|
889
1080
|
if (model.state && !['candidate', 'ready'].includes(normaliseStatus(model.state))) form.append(statusNotice(model, { retry: Boolean(callbacks.onRetry), onRetry: callbacks.onRetry }));
|
|
890
1081
|
const collect = () => ({
|
|
891
1082
|
type: type.value,
|
|
@@ -909,8 +1100,8 @@ export function renderCandidateReview(root, model = {}, callbacks = {}) {
|
|
|
909
1100
|
relationType.setCustomValidity?.('');
|
|
910
1101
|
return collect();
|
|
911
1102
|
};
|
|
912
|
-
form.addEventListener('submit', (event) => { event.preventDefault(); if (typeof form.reportValidity === 'function' && !form.reportValidity()) return; const values = collectValid(); if (values) callbacks.onSave?.(values); });
|
|
913
|
-
commit.addEventListener('click', () => { const values = collectValid(); if (values) callbacks.onCommit?.(values); });
|
|
1103
|
+
form.addEventListener('submit', (event) => { event.preventDefault(); if (editLocked) return; if (typeof form.reportValidity === 'function' && !form.reportValidity()) return; const values = collectValid(); if (values) callbacks.onSave?.(values); });
|
|
1104
|
+
commit.addEventListener('click', () => { if (editLocked) return; const values = collectValid(); if (values) callbacks.onCommit?.(values); });
|
|
914
1105
|
proposal.append(form); comparison.append(original, proposal); section.append(comparison);
|
|
915
1106
|
const evidence = asArray(model.evidence);
|
|
916
1107
|
const exclusions = asArray(model.exclusions);
|
|
@@ -946,7 +1137,14 @@ export function renderCommitState(root, model = {}, callbacks = {}) {
|
|
|
946
1137
|
retry.addEventListener('click', () => callbacks.onReadback()); section.append(retry);
|
|
947
1138
|
}
|
|
948
1139
|
} else if (state === 'verified') section.append(makeElement('p', { className: 'knowledge-success-copy', text: '正本と関係のreadbackが一致しました。検索反映の状態は別に確認します。', attrs: { role: 'status' } }));
|
|
949
|
-
else if (state === 'conflict') section.append(statusNotice(
|
|
1140
|
+
else if (state === 'conflict') section.append(statusNotice(model, {
|
|
1141
|
+
detail: '最新版を確認してから、必要なら再適用してください。',
|
|
1142
|
+
retry: Boolean(callbacks.onReadback),
|
|
1143
|
+
onRetry: callbacks.onReadback,
|
|
1144
|
+
retryLabel: '再試行(入力を保持)',
|
|
1145
|
+
onReapply: callbacks.onReapply,
|
|
1146
|
+
reapplyLabel: '保持した内容を再適用して保存',
|
|
1147
|
+
}));
|
|
950
1148
|
else if (['error', 'error_retryable', 'permission_denied', 'unknown'].includes(state)) section.append(statusNotice(model, { retry: Boolean(callbacks.onReadback), onRetry: callbacks.onReadback }));
|
|
951
1149
|
renderDefinitionList(section, [
|
|
952
1150
|
['正本ID', model.item_id ?? model.item?.id ?? UNKNOWN],
|
|
@@ -1263,7 +1461,10 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1263
1461
|
}
|
|
1264
1462
|
header.append(nav); shell.append(header);
|
|
1265
1463
|
const body = makeElement('div', { className: 'knowledge-outcome-body knowledge-workspace', attrs: { 'data-view': state.view } });
|
|
1266
|
-
|
|
1464
|
+
// The host application owns the page-level main landmark. Keeping the
|
|
1465
|
+
// outcome workspace as a labelled section avoids a duplicate main when
|
|
1466
|
+
// this module is mounted inside that page.
|
|
1467
|
+
const primary = makeElement('section', { className: 'knowledge-outcome-primary knowledge-master', attrs: { 'aria-label': state.view === 'discovery' ? '知識の一覧' : '知識の操作' } });
|
|
1267
1468
|
const discovery = makeElement('section', { className: 'knowledge-discovery-view', hidden: state.view !== 'discovery', attrs: { 'aria-labelledby': 'knowledge-discovery-title' } });
|
|
1268
1469
|
const discoveryHeader = makeElement('div', { className: 'knowledge-view-heading' });
|
|
1269
1470
|
append(discoveryHeader,
|
|
@@ -1285,16 +1486,41 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1285
1486
|
captureHeading.firstChild.append(makeElement('h3', { id: 'knowledge-capture-workspace-title', text: '登録案を作る' }), makeElement('p', { text: '自然文を候補へ整理し、正本へ保存する前に内容と根拠を確認します。' }));
|
|
1286
1487
|
captureWorkspace.append(captureHeading);
|
|
1287
1488
|
const capture = makeElement('div', { className: 'knowledge-capture-form-region' });
|
|
1288
|
-
|
|
1489
|
+
const pendingDraftUpdate = state.commit.pending_draft_update;
|
|
1490
|
+
const draftConflictNeedsRead = Boolean(pendingDraftUpdate && (pendingDraftUpdate.latest_revision === undefined || pendingDraftUpdate.latest_revision === null));
|
|
1491
|
+
const commitLocked = state.commit.state === 'saving' || Boolean(state.commit.pending_save) || draftConflictNeedsRead || ['verified', 'saved_unverified'].includes(state.commit.state);
|
|
1492
|
+
const candidateReadbackPending = state.candidate.candidate_readback_pending === true;
|
|
1493
|
+
const candidateInteractionLocked = state.candidate.state !== 'idle'
|
|
1494
|
+
&& (commitLocked || Boolean(pendingDraftUpdate) || candidateReadbackPending || state.candidate.state === 'saving');
|
|
1495
|
+
renderDraftCapture(capture, state.draft, {
|
|
1496
|
+
onSubmit: createDraft,
|
|
1497
|
+
onResume: resumeDraft,
|
|
1498
|
+
onDiscard: discardDraft,
|
|
1499
|
+
onInput: (next) => {
|
|
1500
|
+
state.draft = { ...state.draft, ...next };
|
|
1501
|
+
if (state.candidate.state !== 'idle') state.candidate = { ...state.candidate, ...next };
|
|
1502
|
+
},
|
|
1503
|
+
editLocked: candidateInteractionLocked,
|
|
1504
|
+
onRetry: state.candidate.state === 'idle' && !candidateInteractionLocked
|
|
1505
|
+
? () => createDraft({ input_text: state.draft.input_text, source: state.draft.source })
|
|
1506
|
+
: null,
|
|
1507
|
+
});
|
|
1289
1508
|
captureWorkspace.append(capture);
|
|
1290
1509
|
const review = makeElement('div', { className: 'knowledge-review-region', hidden: state.view !== 'capture' || state.candidate.state === 'idle' });
|
|
1291
1510
|
if (state.candidate.state !== 'idle') {
|
|
1292
|
-
const
|
|
1511
|
+
const candidateEditLocked = commitLocked || Boolean(pendingDraftUpdate) || candidateReadbackPending || state.candidate.state === 'saving';
|
|
1512
|
+
const candidateCommitLocked = candidateEditLocked || state.candidate.state === 'conflict';
|
|
1513
|
+
const persistedCandidateDraftId = state.candidate.draft_id ?? state.draft.draft_id ?? state.draft.id;
|
|
1293
1514
|
renderCandidateReview(review, { ...state.candidate, decision_domains: state.authority.domains ?? [] }, {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1515
|
+
editLocked: candidateEditLocked,
|
|
1516
|
+
onApply: candidateEditLocked ? null : applyProposal,
|
|
1517
|
+
onSave: candidateEditLocked ? null : saveCandidate,
|
|
1518
|
+
onCommit: candidateCommitLocked || !(state.candidate.draft_id ?? state.draft.id) ? null : commitCandidate,
|
|
1519
|
+
onRetry: state.commit.pending_save
|
|
1520
|
+
? retryPendingSave
|
|
1521
|
+
: persistedCandidateDraftId
|
|
1522
|
+
? retryCandidateDraftReadback
|
|
1523
|
+
: () => createDraft({ input_text: state.draft.input_text, source: state.draft.source }),
|
|
1298
1524
|
});
|
|
1299
1525
|
}
|
|
1300
1526
|
captureWorkspace.append(review);
|
|
@@ -1346,7 +1572,21 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1346
1572
|
const lifecycle = makeElement('div', { className: 'knowledge-inspector-lifecycle' });
|
|
1347
1573
|
const detailUnavailable = ['loading', 'error', 'error_retryable', 'permission_denied', 'unknown'].includes(state.detail.state);
|
|
1348
1574
|
if (state.selected && !detailUnavailable) renderLifecycle(lifecycle, state.selected, { canEdit: canManageKnowledge(session), ownerCandidates, revisionDraft: state.lifecycle.revisionDraft, supersessionDraft: state.lifecycle.supersessionDraft, supersessionCandidates: state.list.records, onRevision: reviseItem, onSupersede: supersedeItem, onRetire: retireItem }); detail.append(lifecycle);
|
|
1349
|
-
if (state.commit.state !== 'idle') {
|
|
1575
|
+
if (state.commit.state !== 'idle') {
|
|
1576
|
+
const commit = makeElement('div');
|
|
1577
|
+
const pendingDraftUpdate = state.commit.pending_draft_update;
|
|
1578
|
+
const draftConflictNeedsRead = Boolean(pendingDraftUpdate && (pendingDraftUpdate.latest_revision === undefined || pendingDraftUpdate.latest_revision === null));
|
|
1579
|
+
const onReadback = state.commit.pending_save || draftConflictNeedsRead
|
|
1580
|
+
? retryPendingSave
|
|
1581
|
+
: state.commit.state === 'conflict' && pendingDraftUpdate
|
|
1582
|
+
? null
|
|
1583
|
+
: readbackCommit;
|
|
1584
|
+
const onReapply = state.commit.state === 'conflict' && pendingDraftUpdateReadyForReapply(pendingDraftUpdate)
|
|
1585
|
+
? reapplyPendingDraftUpdate
|
|
1586
|
+
: null;
|
|
1587
|
+
renderCommitState(commit, state.commit, { onReadback, onReapply });
|
|
1588
|
+
detail.append(commit);
|
|
1589
|
+
}
|
|
1350
1590
|
body.append(detail); shell.append(body); root.append(shell); notify();
|
|
1351
1591
|
return shell;
|
|
1352
1592
|
}
|
|
@@ -1411,7 +1651,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1411
1651
|
message: matches ? null : '保存後の再取得結果が保存先の契約と一致しません。入力内容は保持します。',
|
|
1412
1652
|
};
|
|
1413
1653
|
} catch (error) {
|
|
1414
|
-
state.destination = { ...state.destination, state: isVersionConflict(error) || error.status === 409 ? 'conflict' : error.status === 403 ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code, draft: { ...input } };
|
|
1654
|
+
state.destination = { ...state.destination, state: isVersionConflict(error) || error.status === 409 ? 'conflict' : error.status === 403 ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error), draft: { ...input } };
|
|
1415
1655
|
}
|
|
1416
1656
|
render(); return state.destination;
|
|
1417
1657
|
}
|
|
@@ -1448,7 +1688,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1448
1688
|
const proposed = payload?.proposal ?? {};
|
|
1449
1689
|
state.draft = { ...state.draft, ...preserved, state: 'candidate' };
|
|
1450
1690
|
state.candidate = { ...payload, candidate: proposed, input_text: preserved.input_text, source: preserved.source, state: 'candidate', proposal_applied: false, draft_id: null, version: null };
|
|
1451
|
-
} catch (error) { state.draft = { ...state.draft, state: error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code }; }
|
|
1691
|
+
} catch (error) { state.draft = { ...state.draft, state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) }; }
|
|
1452
1692
|
render(); return state.candidate;
|
|
1453
1693
|
}
|
|
1454
1694
|
|
|
@@ -1469,7 +1709,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1469
1709
|
: error.code === 'knowledge_draft_not_found' || error.status === 404
|
|
1470
1710
|
? 'unknown'
|
|
1471
1711
|
: error.status === 409 || isVersionConflict(error) ? 'conflict' : 'error_retryable';
|
|
1472
|
-
state.draft = { ...state.draft, state: failureState, message: apiErrorText(error), error: error.code };
|
|
1712
|
+
state.draft = { ...state.draft, state: failureState, message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) };
|
|
1473
1713
|
}
|
|
1474
1714
|
render(); return state.candidate;
|
|
1475
1715
|
}
|
|
@@ -1484,7 +1724,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1484
1724
|
await callKnowledgeApi(apiMutation, knowledgePath(code, `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(draftId)}/discard`), { method: 'POST', body: { revision }, key: operationKey('draft-discard', draftId, revision, { revision }) });
|
|
1485
1725
|
state.draft = { state: 'idle', input_text: '' }; state.candidate = { state: 'idle' };
|
|
1486
1726
|
} catch (error) {
|
|
1487
|
-
state.draft = { ...preservedDraft, state: isVersionConflict(error) ? 'conflict' : 'error_retryable', message: apiErrorText(error), error: error.code };
|
|
1727
|
+
state.draft = { ...preservedDraft, state: isVersionConflict(error) ? 'conflict' : 'error_retryable', message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) };
|
|
1488
1728
|
state.candidate = preservedCandidate;
|
|
1489
1729
|
}
|
|
1490
1730
|
render(); return state.draft;
|
|
@@ -1510,6 +1750,11 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1510
1750
|
}
|
|
1511
1751
|
|
|
1512
1752
|
async function saveCandidate(values) {
|
|
1753
|
+
if (state.candidate.state === 'saving'
|
|
1754
|
+
|| state.candidate.candidate_readback_pending === true
|
|
1755
|
+
|| state.commit.state === 'saving'
|
|
1756
|
+
|| state.commit.pending_save
|
|
1757
|
+
|| state.commit.pending_draft_update) return state.candidate;
|
|
1513
1758
|
const draftId = state.candidate.draft_id ?? state.draft.id;
|
|
1514
1759
|
if (!state.candidate.proposal_applied) { state.candidate = { ...state.candidate, state: 'unknown', message: 'AI提案を入力欄へ反映してから下書きを保存してください。' }; render(); return null; }
|
|
1515
1760
|
const editedCandidate = { ...(state.candidate.candidate ?? {}), ...values };
|
|
@@ -1517,22 +1762,122 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1517
1762
|
try {
|
|
1518
1763
|
const candidateBody = serializeCandidate ? serializeCandidate(values, state.candidate) : defaultCandidateBody(values, state.draft);
|
|
1519
1764
|
const revision = state.candidate.version ?? state.draft.revision ?? state.draft.version;
|
|
1765
|
+
if (draftId && (revision === null || revision === undefined || optionalText(revision).trim() === '')) {
|
|
1766
|
+
state.candidate = {
|
|
1767
|
+
...state.candidate,
|
|
1768
|
+
candidate: editedCandidate,
|
|
1769
|
+
state: 'conflict',
|
|
1770
|
+
candidate_readback_pending: true,
|
|
1771
|
+
version: null,
|
|
1772
|
+
latest_revision: null,
|
|
1773
|
+
message: '下書きの版を確認できないため、保存を停止しました。最新版を確認してから再試行してください。',
|
|
1774
|
+
};
|
|
1775
|
+
state.draft = { ...state.draft, state: 'conflict', id: draftId, draft_id: draftId };
|
|
1776
|
+
render(); return state.candidate;
|
|
1777
|
+
}
|
|
1520
1778
|
const body = draftId ? { ...candidateBody, revision } : candidateBody;
|
|
1521
1779
|
const path = draftId ? `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(draftId)}` : KNOWLEDGE_ROUTES.drafts;
|
|
1522
1780
|
const payload = await callKnowledgeApi(apiMutation, knowledgePath(code, path), { method: draftId ? 'PATCH' : 'POST', body, key: operationKey(draftId ? 'draft-update' : 'draft', draftId ?? 'new', revision, body) });
|
|
1523
1781
|
const draft = payload?.draft ?? payload?.data ?? payload;
|
|
1524
|
-
state.candidate = { ...state.candidate, ...draft, candidate: { ...editedCandidate, ...(draft?.candidate ?? {}) }, state: 'candidate', proposal_applied: true, draft_id: draft?.draft_id ?? draft?.id ?? draftId, version: draft?.revision ?? draft?.version ?? state.candidate.version };
|
|
1782
|
+
state.candidate = { ...state.candidate, ...draft, candidate: { ...editedCandidate, ...(draft?.candidate ?? {}) }, state: 'candidate', proposal_applied: true, candidate_readback_pending: false, draft_id: draft?.draft_id ?? draft?.id ?? draftId, version: draft?.revision ?? draft?.version ?? state.candidate.version };
|
|
1525
1783
|
const savedId = draft?.draft_id ?? draft?.id ?? draftId;
|
|
1526
1784
|
state.draft = { ...state.draft, ...draft, id: savedId, draft_id: savedId, resume_url: resumeUrl(savedId), state: 'candidate' };
|
|
1527
|
-
} catch (error) { state.candidate = { ...state.candidate, state: isVersionConflict(error) ? 'conflict' : 'error_retryable', message: apiErrorText(error), error: error.code }; }
|
|
1785
|
+
} catch (error) { state.candidate = { ...state.candidate, state: isVersionConflict(error) ? 'conflict' : 'error_retryable', candidate_readback_pending: Boolean(draftId), message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) }; }
|
|
1786
|
+
render(); return state.candidate;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
async function retryCandidateDraftReadback() {
|
|
1790
|
+
if (state.candidate.state === 'saving') return state.candidate;
|
|
1791
|
+
const draftId = state.candidate.draft_id ?? state.draft.draft_id ?? state.draft.id;
|
|
1792
|
+
if (!draftId) return createDraft({ input_text: state.draft.input_text, source: state.draft.source });
|
|
1793
|
+
|
|
1794
|
+
// Keep the user's candidate edits separate from the server response. The
|
|
1795
|
+
// response is used only to refresh the draft identity/version; replacing
|
|
1796
|
+
// the candidate here would turn a recoverable save conflict into a silent
|
|
1797
|
+
// loss of the title and summary the user just edited.
|
|
1798
|
+
const localCandidate = { ...(state.candidate.candidate ?? {}) };
|
|
1799
|
+
const localInput = state.draft.input_text ?? state.candidate.input_text ?? '';
|
|
1800
|
+
const localSource = state.draft.source ?? state.candidate.source ?? null;
|
|
1801
|
+
state.candidate = {
|
|
1802
|
+
...state.candidate,
|
|
1803
|
+
state: 'saving',
|
|
1804
|
+
candidate_readback_pending: true,
|
|
1805
|
+
candidate: localCandidate,
|
|
1806
|
+
input_text: localInput,
|
|
1807
|
+
source: localSource,
|
|
1808
|
+
message: '下書きの最新版を確認しています。入力と提案は保持されます。',
|
|
1809
|
+
};
|
|
1810
|
+
render();
|
|
1811
|
+
try {
|
|
1812
|
+
const payload = await callKnowledgeApi(api, knowledgePath(code, `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(draftId)}`));
|
|
1813
|
+
const latestDraft = draftReadbackFrom(payload, draftId);
|
|
1814
|
+
if (!latestDraft.valid) {
|
|
1815
|
+
state.candidate = {
|
|
1816
|
+
...state.candidate,
|
|
1817
|
+
state: 'conflict',
|
|
1818
|
+
candidate_readback_pending: true,
|
|
1819
|
+
draft_id: draftId,
|
|
1820
|
+
version: null,
|
|
1821
|
+
latest_revision: null,
|
|
1822
|
+
candidate: localCandidate,
|
|
1823
|
+
input_text: localInput,
|
|
1824
|
+
source: localSource,
|
|
1825
|
+
message: '下書きの対象を確認できません。入力と提案は保持しています。対象を確認してから再試行してください。',
|
|
1826
|
+
};
|
|
1827
|
+
state.draft = { ...state.draft, state: 'conflict', id: draftId, draft_id: draftId, input_text: localInput, source: localSource };
|
|
1828
|
+
} else {
|
|
1829
|
+
const latestRevision = latestDraft.revision;
|
|
1830
|
+
state.candidate = {
|
|
1831
|
+
...state.candidate,
|
|
1832
|
+
state: 'conflict',
|
|
1833
|
+
candidate_readback_pending: latestRevision === null,
|
|
1834
|
+
draft_id: latestDraft.draft_id,
|
|
1835
|
+
version: latestRevision,
|
|
1836
|
+
latest_revision: latestRevision,
|
|
1837
|
+
candidate: localCandidate,
|
|
1838
|
+
input_text: localInput,
|
|
1839
|
+
source: localSource,
|
|
1840
|
+
message: latestRevision === null
|
|
1841
|
+
? '下書きの最新版は確認しましたが、版を確認できません。入力と提案は保持しています。再試行してください。'
|
|
1842
|
+
: '下書きの最新版を確認しました。入力と提案は保持しています。確認後に下書きを保存してください。',
|
|
1843
|
+
};
|
|
1844
|
+
state.draft = {
|
|
1845
|
+
...state.draft,
|
|
1846
|
+
state: 'candidate',
|
|
1847
|
+
id: latestDraft.draft_id,
|
|
1848
|
+
draft_id: latestDraft.draft_id,
|
|
1849
|
+
revision: latestRevision,
|
|
1850
|
+
input_text: localInput,
|
|
1851
|
+
source: localSource,
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
state.candidate = {
|
|
1856
|
+
...state.candidate,
|
|
1857
|
+
state: 'conflict',
|
|
1858
|
+
candidate_readback_pending: true,
|
|
1859
|
+
draft_id: draftId,
|
|
1860
|
+
version: null,
|
|
1861
|
+
latest_revision: null,
|
|
1862
|
+
candidate: localCandidate,
|
|
1863
|
+
input_text: localInput,
|
|
1864
|
+
source: localSource,
|
|
1865
|
+
message: `下書きの最新版を確認できません。入力と提案は保持しています。${apiErrorText(error)}`,
|
|
1866
|
+
error: error.code,
|
|
1867
|
+
...conflictMetadataFrom(error),
|
|
1868
|
+
};
|
|
1869
|
+
state.draft = { ...state.draft, state: 'conflict', id: draftId, draft_id: draftId, input_text: localInput, source: localSource };
|
|
1870
|
+
}
|
|
1528
1871
|
render(); return state.candidate;
|
|
1529
1872
|
}
|
|
1530
1873
|
|
|
1531
1874
|
async function commitCandidate(values) {
|
|
1532
1875
|
if (state.commit.state === 'saving' || state.commit.pending_save || ['verified', 'saved_unverified'].includes(state.commit.state)) return state.commit;
|
|
1876
|
+
if (state.candidate.state === 'saving' || state.candidate.candidate_readback_pending === true) return state.commit;
|
|
1877
|
+
const previousDraftConflict = state.commit.pending_draft_update;
|
|
1533
1878
|
const draftId = state.candidate.draft_id ?? state.draft.id;
|
|
1534
1879
|
if (!draftId) { state.commit = { state: 'unknown', message: '下書きIDを確認できません。' }; render(); return null; }
|
|
1535
|
-
state.commit = { state: 'saving', item_id: null, version: null }; render();
|
|
1880
|
+
state.commit = { state: 'saving', item_id: null, version: null, ...(previousDraftConflict ? { pending_draft_update: previousDraftConflict } : {}) }; render();
|
|
1536
1881
|
try {
|
|
1537
1882
|
const resolvedDomain = await resolveDecisionDomain?.({ project, session: typeof session === 'function' ? session() : session, candidate: values });
|
|
1538
1883
|
const decisionDomain = resolvedDomain ?? values.decision_domain ?? null;
|
|
@@ -1553,25 +1898,148 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1553
1898
|
state.commit = { state: 'unknown', message: '再利用する正本IDと現在版を確認できません。' };
|
|
1554
1899
|
render(); return state.commit;
|
|
1555
1900
|
}
|
|
1556
|
-
const revision =
|
|
1557
|
-
|
|
1901
|
+
const revision = previousDraftConflict
|
|
1902
|
+
? previousDraftConflict.latest_revision
|
|
1903
|
+
: state.candidate.version ?? state.draft.revision ?? state.draft.version ?? null;
|
|
1904
|
+
if (draftId && (revision === null || revision === undefined || optionalText(revision).trim() === '')) {
|
|
1905
|
+
state.commit = {
|
|
1906
|
+
state: 'unknown',
|
|
1907
|
+
item_id: null,
|
|
1908
|
+
version: null,
|
|
1909
|
+
message: '下書きの版を確認できないため、保存を停止しました。最新版を確認してから再試行してください。',
|
|
1910
|
+
};
|
|
1911
|
+
render(); return state.commit;
|
|
1912
|
+
}
|
|
1913
|
+
// Capture every value currently visible in the proposal form before the
|
|
1914
|
+
// request can fail. A conflict rerender must not reconstruct the form
|
|
1915
|
+
// from the original AI proposal and discard edits made by the user.
|
|
1916
|
+
state.candidate = {
|
|
1917
|
+
...state.candidate,
|
|
1918
|
+
candidate: { ...(state.candidate.candidate ?? {}), ...values },
|
|
1919
|
+
input_text: state.draft.input_text,
|
|
1920
|
+
draft_id: draftId,
|
|
1921
|
+
version: revision,
|
|
1922
|
+
state: 'candidate',
|
|
1923
|
+
proposal_applied: true,
|
|
1924
|
+
};
|
|
1925
|
+
const draftUpdate = {
|
|
1926
|
+
draftId,
|
|
1927
|
+
body: { ...candidateBody, revision },
|
|
1928
|
+
key: operationKey('draft-update', draftId, revision, candidateBody),
|
|
1929
|
+
latest_revision: previousDraftConflict?.latest_revision ?? null,
|
|
1930
|
+
local_input_text: state.draft.input_text,
|
|
1931
|
+
local_source: state.draft.source,
|
|
1932
|
+
};
|
|
1933
|
+
state.commit = { ...state.commit, pending_draft_update: draftUpdate };
|
|
1934
|
+
const updated = await callKnowledgeApi(apiMutation, knowledgePath(code, `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(draftId)}`), { method: 'PATCH', body: draftUpdate.body, key: draftUpdate.key });
|
|
1558
1935
|
const savedRevision = updated?.revision ?? revision;
|
|
1559
1936
|
state.candidate = { ...state.candidate, ...updated, candidate: updated?.candidate ?? values, draft_id: updated?.draft_id ?? updated?.id ?? draftId, version: savedRevision };
|
|
1560
|
-
state.draft = {
|
|
1937
|
+
state.draft = {
|
|
1938
|
+
...state.draft,
|
|
1939
|
+
...updated,
|
|
1940
|
+
id: updated?.draft_id ?? updated?.id ?? draftId,
|
|
1941
|
+
revision: savedRevision,
|
|
1942
|
+
input_text: draftUpdate.local_input_text,
|
|
1943
|
+
...(draftUpdate.local_source !== undefined ? { source: draftUpdate.local_source } : {}),
|
|
1944
|
+
};
|
|
1561
1945
|
const body = { revision: savedRevision, decision_domain: decisionDomain, ...(selection === 'reuse' ? { canonical_id: canonicalId, expected_version: canonicalVersion } : {}) };
|
|
1562
1946
|
const action = selection === 'reuse' ? 'reuse' : 'save';
|
|
1563
1947
|
const key = operationKey(action, draftId, savedRevision, body);
|
|
1564
|
-
state.commit = { ...state.commit, pending_save: { draftId, body, key, action, expected_relations: candidateBody.relations } };
|
|
1948
|
+
state.commit = { ...state.commit, pending_save: { draftId, body, key, action, expected_relations: candidateBody.relations }, pending_draft_update: null };
|
|
1565
1949
|
const payload = await callKnowledgeApi(apiMutation, knowledgePath(code, `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(draftId)}/${action}`), { method: 'POST', body, key });
|
|
1566
1950
|
const readback = await verifyMutationReadback(payload, candidateBody.relations, action);
|
|
1567
1951
|
state.commit = readback;
|
|
1568
1952
|
if (readback.item) { state.selected = readback.item; state.detail = { state: 'ready', item: readback.item }; state.lifecycle = { state: 'ready', item: readback.item }; }
|
|
1569
1953
|
onCommitted?.(readback);
|
|
1570
|
-
} catch (error) {
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
const pendingDraftUpdate = state.commit.pending_draft_update;
|
|
1956
|
+
const nextPendingDraftUpdate = pendingDraftUpdate && isVersionConflict(error)
|
|
1957
|
+
? { ...pendingDraftUpdate, latest_revision: null }
|
|
1958
|
+
: pendingDraftUpdate;
|
|
1959
|
+
state.commit = {
|
|
1960
|
+
...state.commit,
|
|
1961
|
+
...(nextPendingDraftUpdate ? { pending_draft_update: nextPendingDraftUpdate } : {}),
|
|
1962
|
+
state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable',
|
|
1963
|
+
message: apiErrorText(error),
|
|
1964
|
+
error: error.code,
|
|
1965
|
+
...conflictMetadataFrom(error),
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
render(); return state.commit;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
async function retryDraftConflictReadback() {
|
|
1972
|
+
const pending = state.commit.pending_draft_update;
|
|
1973
|
+
if (!pending) return readbackCommit();
|
|
1974
|
+
state.commit = { ...state.commit, state: 'saving', message: '下書きの最新版を確認しています。入力と出典は保持されます。' }; render();
|
|
1975
|
+
try {
|
|
1976
|
+
const payload = await callKnowledgeApi(api, knowledgePath(code, `${KNOWLEDGE_ROUTES.drafts}/${encodeURIComponent(pending.draftId)}`));
|
|
1977
|
+
const latestDraft = draftReadbackFrom(payload, pending.draftId);
|
|
1978
|
+
const latestRevision = latestDraft.valid ? latestDraft.revision : null;
|
|
1979
|
+
if (!latestDraft.valid) {
|
|
1980
|
+
state.commit = {
|
|
1981
|
+
...state.commit,
|
|
1982
|
+
state: 'conflict',
|
|
1983
|
+
latest_revision: null,
|
|
1984
|
+
pending_draft_update: { ...pending, latest_revision: null, latest_draft_metadata: null },
|
|
1985
|
+
message: '下書きの対象を確認できません。入力と出典は保持しています。対象を確認してから再試行してください。',
|
|
1986
|
+
};
|
|
1987
|
+
} else {
|
|
1988
|
+
state.commit = {
|
|
1989
|
+
...state.commit,
|
|
1990
|
+
state: 'conflict',
|
|
1991
|
+
latest_revision: latestRevision,
|
|
1992
|
+
pending_draft_update: {
|
|
1993
|
+
...pending,
|
|
1994
|
+
latest_revision: latestRevision,
|
|
1995
|
+
latest_draft_metadata: {
|
|
1996
|
+
draft_id: latestDraft.draft_id,
|
|
1997
|
+
revision: latestRevision,
|
|
1998
|
+
...(latestDraft.content !== undefined ? { content: latestDraft.content } : {}),
|
|
1999
|
+
...(latestDraft.source ? { source: latestDraft.source } : {}),
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
message: latestRevision === null
|
|
2003
|
+
? '下書きの対象は確認しましたが、最新版の版を確認できません。入力と出典は保持しています。再試行してください。'
|
|
2004
|
+
: '下書きの最新版を確認しました。入力と出典は保持しています。内容を比較してから再適用してください。',
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
state.commit = {
|
|
2009
|
+
...state.commit,
|
|
2010
|
+
state: 'conflict',
|
|
2011
|
+
latest_revision: null,
|
|
2012
|
+
pending_draft_update: { ...pending, latest_revision: null },
|
|
2013
|
+
message: `下書きの最新版を確認できません。入力と出典は保持しています。${apiErrorText(error)}`,
|
|
2014
|
+
error: error.code,
|
|
2015
|
+
...conflictMetadataFrom(error),
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
1571
2018
|
render(); return state.commit;
|
|
1572
2019
|
}
|
|
1573
2020
|
|
|
2021
|
+
async function reapplyPendingDraftUpdate() {
|
|
2022
|
+
const pending = state.commit.pending_draft_update;
|
|
2023
|
+
if (!pendingDraftUpdateReadyForReapply(pending)) return state.commit;
|
|
2024
|
+
// commitCandidate marks the commit as saving before its first await. This
|
|
2025
|
+
// also makes a second click on a detached button a no-op.
|
|
2026
|
+
if (state.commit.state === 'saving' || state.commit.pending_save) return state.commit;
|
|
2027
|
+
const candidateValues = { ...(state.candidate.candidate ?? {}) };
|
|
2028
|
+
if (!Object.keys(candidateValues).length) return state.commit;
|
|
2029
|
+
state.draft = {
|
|
2030
|
+
...state.draft,
|
|
2031
|
+
input_text: pending.local_input_text ?? state.draft.input_text,
|
|
2032
|
+
...(Object.prototype.hasOwnProperty.call(pending, 'local_source') ? { source: pending.local_source } : {}),
|
|
2033
|
+
};
|
|
2034
|
+
return commitCandidate(candidateValues);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1574
2037
|
async function retryPendingSave() {
|
|
2038
|
+
const pendingDraftUpdate = state.commit.pending_draft_update;
|
|
2039
|
+
if (pendingDraftUpdate) {
|
|
2040
|
+
if (pendingDraftUpdate.latest_revision === undefined || pendingDraftUpdate.latest_revision === null) return retryDraftConflictReadback();
|
|
2041
|
+
return state.commit;
|
|
2042
|
+
}
|
|
1575
2043
|
const pending = state.commit.pending_save;
|
|
1576
2044
|
if (!pending) return readbackCommit();
|
|
1577
2045
|
state.commit = { ...state.commit, state: 'saving' }; render();
|
|
@@ -1581,7 +2049,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1581
2049
|
if (state.commit.item) state.selected = state.commit.item;
|
|
1582
2050
|
onCommitted?.(state.commit);
|
|
1583
2051
|
} catch (error) {
|
|
1584
|
-
state.commit = { ...state.commit, pending_save: pending, state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code };
|
|
2052
|
+
state.commit = { ...state.commit, pending_save: pending, state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) };
|
|
1585
2053
|
}
|
|
1586
2054
|
render(); return state.commit;
|
|
1587
2055
|
}
|
|
@@ -1651,7 +2119,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1651
2119
|
const matches = expected?.id === state.selected.id && expected.version !== null && item?.id === expected.id && item?.version === expected.version && expectedHash !== null && actualHash === expectedHash && sameReturnedFields;
|
|
1652
2120
|
if (item) { state.selected = item; state.detail = { state: 'ready', item }; }
|
|
1653
2121
|
state.lifecycle = { state: matches ? 'verified' : 'saved_unverified', action: 'revision', item_id: expected?.id ?? state.selected?.id, version: item?.version, expected_version: expected?.version, expected_hash: expectedHash, item };
|
|
1654
|
-
} catch (error) { state.lifecycle = { state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', action: 'revision', revisionDraft: supplied, message: apiErrorText(error), error: error.code }; }
|
|
2122
|
+
} catch (error) { state.lifecycle = { state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', action: 'revision', revisionDraft: supplied, message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) }; }
|
|
1655
2123
|
render(); return state.lifecycle;
|
|
1656
2124
|
}
|
|
1657
2125
|
|
|
@@ -1666,7 +2134,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1666
2134
|
const item = normalizeKnowledgeItem(current?.item ?? current?.record ?? current?.data ?? current);
|
|
1667
2135
|
const result = { state: item?.id === state.selected.id && item.lifecycle.status === 'retired' ? 'verified' : 'saved_unverified', item_id: state.selected.id, version: item?.version ?? state.selected.version, item };
|
|
1668
2136
|
state.lifecycle = { ...result, action: 'retire' }; if (result.item) { state.selected = result.item; state.detail = { state: 'ready', item: result.item }; } onRetired?.(result);
|
|
1669
|
-
} catch (error) { state.lifecycle = { state: error
|
|
2137
|
+
} catch (error) { state.lifecycle = { state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) }; }
|
|
1670
2138
|
render(); return state.lifecycle;
|
|
1671
2139
|
}
|
|
1672
2140
|
|
|
@@ -1693,7 +2161,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1693
2161
|
if (replacement) { state.selected = replacement; state.detail = { state: 'ready', item: replacement }; }
|
|
1694
2162
|
state.lifecycle = { state: matches ? 'verified' : 'saved_unverified', action: 'supersession', item_id: replacement?.id, version: replacement?.version, superseded_item_id: superseded?.id, item: replacement };
|
|
1695
2163
|
} catch (error) {
|
|
1696
|
-
state.lifecycle = { state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', action: 'supersession', supersessionDraft: supplied, message: apiErrorText(error), error: error.code };
|
|
2164
|
+
state.lifecycle = { state: isVersionConflict(error) ? 'conflict' : error.code === 'permission_denied' ? 'permission_denied' : 'error_retryable', action: 'supersession', supersessionDraft: supplied, message: apiErrorText(error), error: error.code, ...conflictMetadataFrom(error) };
|
|
1697
2165
|
}
|
|
1698
2166
|
render(); return state.lifecycle;
|
|
1699
2167
|
}
|
|
@@ -1742,7 +2210,7 @@ export function createKnowledgeOutcomeController(options = {}) {
|
|
|
1742
2210
|
render(); return state.preview;
|
|
1743
2211
|
}
|
|
1744
2212
|
|
|
1745
|
-
const controller = { state, render, loadItems, loadAuthorityDomains, loadDestination, saveDestination, selectItem, createDraft, resumeDraft, discardDraft, applyProposal, saveCandidate, commitCandidate, retryPendingSave, readbackCommit, reviseItem, supersedeItem, retireItem, runPreview, paths: { items: () => buildKnowledgeItemsPath(code, state.filters), drafts: () => knowledgePath(code, KNOWLEDGE_ROUTES.drafts), destination: destinationPath } };
|
|
2213
|
+
const controller = { state, render, loadItems, loadAuthorityDomains, loadDestination, saveDestination, selectItem, createDraft, resumeDraft, discardDraft, applyProposal, saveCandidate, commitCandidate, retryPendingSave, reapplyPendingDraftUpdate, readbackCommit, reviseItem, supersedeItem, retireItem, runPreview, paths: { items: () => buildKnowledgeItemsPath(code, state.filters), drafts: () => knowledgePath(code, KNOWLEDGE_ROUTES.drafts), destination: destinationPath } };
|
|
1746
2214
|
render();
|
|
1747
2215
|
const initialLocation = options.location ?? globalThis.location;
|
|
1748
2216
|
if (initialLocation?.href) {
|