@unson/brainbase-mcp 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-dag/digest.json +2 -2
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-service.js +16 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-objectives.d.ts +11 -0
- package/dist/company-os-objectives.js +84 -47
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-problem-snapshot.d.ts +30 -2
- package/dist/judgment-problem-snapshot.js +302 -6
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +12 -0
- package/dist/personal-knowledge.js +29 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/server.d.ts +7 -2
- package/dist/server.js +9 -6
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +21 -3
- package/dist/world-model.js +94 -12
- package/package.json +94 -6
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +119 -45
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +180 -74
- package/ui/objective-editor.js +490 -43
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
- package/ui/icons/mana/LICENSE.tabler.txt +0 -21
- package/ui/icons/mana/affiliate.svg +0 -20
- package/ui/icons/mana/brand-github.svg +0 -15
- package/ui/icons/mana/brand-google-drive.svg +0 -17
- package/ui/icons/mana/brand-slack.svg +0 -18
- package/ui/icons/mana/cpu.svg +0 -24
- package/ui/outcome-mana.css +0 -255
- package/ui/outcome-mana.js +0 -2325
package/ui/objective-editor.js
CHANGED
|
@@ -6,8 +6,29 @@
|
|
|
6
6
|
* opens a database, or decides organization membership/RACI. An organization
|
|
7
7
|
* application can compose the same screen by adapting its authenticated
|
|
8
8
|
* FoundationRevisionStore boundary to the port.
|
|
9
|
+
*
|
|
10
|
+
* Two layouts: without `rail` the screen keeps its tabs (list, editor, Story
|
|
11
|
+
* links) as the organization edition composes it. With a `rail` element the
|
|
12
|
+
* screen follows the organization edition's pattern (workspace-kit): page
|
|
13
|
+
* head, metrics and a ledger of objectives in the workspace, and the selected
|
|
14
|
+
* objective (or its edit form) in the rail.
|
|
9
15
|
*/
|
|
10
16
|
|
|
17
|
+
import {
|
|
18
|
+
workspaceActions,
|
|
19
|
+
workspaceButton,
|
|
20
|
+
workspaceDefinition,
|
|
21
|
+
workspaceDetailEmpty,
|
|
22
|
+
workspaceHostActions,
|
|
23
|
+
workspaceHostNotice,
|
|
24
|
+
workspaceLedger,
|
|
25
|
+
workspaceMetrics,
|
|
26
|
+
workspaceNotice,
|
|
27
|
+
workspacePageHeader,
|
|
28
|
+
workspaceRailBlock,
|
|
29
|
+
workspaceRailHead,
|
|
30
|
+
} from './workspace-kit.js';
|
|
31
|
+
|
|
11
32
|
export const OBJECTIVE_EDITOR_CONTRACT_VERSION = 'brainbase.objective-editor.v1';
|
|
12
33
|
|
|
13
34
|
export const OBJECTIVE_EDITOR_STORY_IDS = Object.freeze([
|
|
@@ -29,6 +50,9 @@ const STATUS_LABELS = Object.freeze({
|
|
|
29
50
|
ready: '取得済み',
|
|
30
51
|
empty: '登録なし',
|
|
31
52
|
draft: '下書き',
|
|
53
|
+
proposed: '提案中',
|
|
54
|
+
approved: '承認済み',
|
|
55
|
+
retired: '終了',
|
|
32
56
|
judgment_available: '判断に利用可能',
|
|
33
57
|
judgment_unknown: '判断利用可否 未確認',
|
|
34
58
|
permission_denied: '権限不足',
|
|
@@ -447,7 +471,8 @@ function statusBadge(state, label = null) {
|
|
|
447
471
|
function field(labelText, name, value, options = {}) {
|
|
448
472
|
const label = makeElement('label', { className: 'objective-editor-field' });
|
|
449
473
|
label.append(makeElement('span', { className: 'objective-editor-label', text: labelText }));
|
|
450
|
-
|
|
474
|
+
// A field with choices is a select; an input cannot hold option elements.
|
|
475
|
+
const input = makeElement(options.tag ?? (options.options ? 'select' : 'input'), {
|
|
451
476
|
className: 'objective-editor-input',
|
|
452
477
|
value: value ?? '',
|
|
453
478
|
attrs: { name, ...(options.attrs ?? {}) },
|
|
@@ -523,6 +548,36 @@ function editorDraftFromObjective(objective, options = {}) {
|
|
|
523
548
|
};
|
|
524
549
|
}
|
|
525
550
|
|
|
551
|
+
function shortDate(value) {
|
|
552
|
+
const input = nonEmptyText(value);
|
|
553
|
+
if (!input) return null;
|
|
554
|
+
const date = new Date(input);
|
|
555
|
+
if (Number.isNaN(date.getTime())) return input;
|
|
556
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
557
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Readiness in the list: known readiness is named as such, not as the adoption state. */
|
|
561
|
+
function readinessBadge(objective) {
|
|
562
|
+
const readiness = objective.readiness;
|
|
563
|
+
if (readiness?.state === 'ready' && readiness.ready === false) {
|
|
564
|
+
const count = readiness.issues?.length ?? 0;
|
|
565
|
+
return statusBadge('draft', count > 0 ? `判断に使えない(不足${count}件)` : '判断に使えない');
|
|
566
|
+
}
|
|
567
|
+
return statusBadge(objectiveJudgmentState(objective), objectiveJudgmentLabel(objective));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** One line per Objective: desired state, evaluation period, accountable person and criteria count. */
|
|
571
|
+
function objectiveSummary(objective) {
|
|
572
|
+
const from = shortDate(objective.evaluationPeriod?.from);
|
|
573
|
+
const until = shortDate(objective.evaluationPeriod?.until);
|
|
574
|
+
return [
|
|
575
|
+
`評価期間 ${from || until ? `${from ?? '未設定'}〜${until ?? '未設定'}` : '未設定'}`,
|
|
576
|
+
`責任者 ${objective.accountableId ?? '未設定'}`,
|
|
577
|
+
`評価基準 ${objective.criteria ? `${objective.criteria.length}件` : '未確認'}`,
|
|
578
|
+
].join(' ・ ');
|
|
579
|
+
}
|
|
580
|
+
|
|
526
581
|
function renderObjectiveList(root, state, callbacks) {
|
|
527
582
|
const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': '目的一覧' } });
|
|
528
583
|
append(section,
|
|
@@ -548,10 +603,12 @@ function renderObjectiveList(root, state, callbacks) {
|
|
|
548
603
|
const copy = makeElement('span', { className: 'objective-editor-list-copy' });
|
|
549
604
|
append(copy,
|
|
550
605
|
makeElement('strong', { text: text(objective.meaning, objective.id) }),
|
|
606
|
+
objective.desiredState ? makeElement('span', { className: 'objective-editor-muted', text: `実現したい状態: ${objective.desiredState}` }) : null,
|
|
607
|
+
makeElement('small', { text: objectiveSummary(objective) }),
|
|
551
608
|
makeElement('small', { text: `${objective.id}@${objective.revision}` }),
|
|
552
609
|
);
|
|
553
610
|
const badges = makeElement('span', { className: 'objective-editor-list-badges' });
|
|
554
|
-
badges.append(statusBadge(objective.adoptionState ?? 'unknown'),
|
|
611
|
+
badges.append(statusBadge(objective.adoptionState ?? 'unknown'), readinessBadge(objective));
|
|
555
612
|
row.append(copy, badges);
|
|
556
613
|
list.append(row);
|
|
557
614
|
}
|
|
@@ -564,10 +621,10 @@ function renderReadiness(root, readiness) {
|
|
|
564
621
|
const normalized = normalizeReadiness(readiness);
|
|
565
622
|
section.append(makeElement('div', { className: 'objective-editor-subheading', text: '判断への利用' }));
|
|
566
623
|
if (normalized.state !== 'ready') {
|
|
567
|
-
section.append(statusBadge('judgment_unknown'), makeElement('p', { className: 'objective-editor-muted', text: '
|
|
624
|
+
section.append(statusBadge('judgment_unknown'), makeElement('p', { className: 'objective-editor-muted', text: '判断に使える状態かどうかを、保存先から確認できていません。' }));
|
|
568
625
|
return section;
|
|
569
626
|
}
|
|
570
|
-
section.append(
|
|
627
|
+
section.append(normalized.ready ? statusBadge('judgment_available') : statusBadge('draft', '判断に使えない'));
|
|
571
628
|
if (!normalized.ready && normalized.issues?.length) {
|
|
572
629
|
const list = makeElement('ul', { className: 'objective-editor-issue-list' });
|
|
573
630
|
for (const issue of normalized.issues) list.append(makeElement('li', { text: `${issue.path}: ${issue.message}` }));
|
|
@@ -596,11 +653,13 @@ function renderConstraintRefs(root, state, callbacks) {
|
|
|
596
653
|
const item = makeElement('li', { className: 'objective-editor-reference-item' });
|
|
597
654
|
item.append(makeElement('span', { text: `${ref.id}@${ref.revision}` }));
|
|
598
655
|
if (ref.meaning) item.append(makeElement('small', { text: ref.meaning }));
|
|
599
|
-
if (callbacks.canEdit && callbacks.onRemoveConstraintRef) item.append(makeButton('外す', () => callbacks.onRemoveConstraintRef(ref), 'objective-editor-button-danger'));
|
|
656
|
+
if (callbacks.canEdit && callbacks.constraintsEditable !== false && callbacks.onRemoveConstraintRef) item.append(makeButton('外す', () => callbacks.onRemoveConstraintRef(ref), 'objective-editor-button-danger'));
|
|
600
657
|
list.append(item);
|
|
601
658
|
}
|
|
602
659
|
section.append(list);
|
|
603
|
-
if (callbacks.
|
|
660
|
+
if (callbacks.constraintsEditable === false) {
|
|
661
|
+
section.append(makeElement('p', { className: 'objective-editor-muted', text: '制約の参照はここでは表示だけです。' }));
|
|
662
|
+
} else if (callbacks.canEdit && callbacks.onAddConstraintRef) {
|
|
604
663
|
const addForm = makeElement('form', { className: 'objective-editor-inline-form' });
|
|
605
664
|
const idField = field('制約ID', 'constraint_id', '', { attrs: { placeholder: 'constraint-id' } });
|
|
606
665
|
const revisionField = field('版', 'constraint_revision', '', { attrs: { placeholder: '1' } });
|
|
@@ -619,12 +678,12 @@ function renderCriteria(root, draft, callbacks) {
|
|
|
619
678
|
const fieldset = makeElement('fieldset', { className: 'objective-editor-criteria' });
|
|
620
679
|
fieldset.append(makeElement('legend', { text: '評価基準' }));
|
|
621
680
|
const criteria = Array.isArray(draft.criteria) ? draft.criteria : [];
|
|
622
|
-
if (!criteria.length) fieldset.append(makeElement('p', { className: 'objective-editor-muted', text: '
|
|
681
|
+
if (!criteria.length) fieldset.append(makeElement('p', { className: 'objective-editor-muted', text: '評価基準はまだありません。判断に使うには、測る変数の参照が必要です。' }));
|
|
623
682
|
criteria.forEach((criterion, index) => {
|
|
624
683
|
const row = makeElement('div', { className: 'objective-editor-criterion-row' });
|
|
625
684
|
const variableRef = criterion?.variableRef ?? {};
|
|
626
|
-
const variableId = field('
|
|
627
|
-
const variableRevision = field('
|
|
685
|
+
const variableId = field('変数ID', `criteria.${index}.variable_id`, variableRef.id ?? '', { attrs: { required: true } });
|
|
686
|
+
const variableRevision = field('変数の版', `criteria.${index}.variable_revision`, variableRef.revision ?? '', { attrs: { required: true } });
|
|
628
687
|
const operator = field('比較', `criteria.${index}.operator`, criterion?.operator ?? 'equals', { options: [
|
|
629
688
|
{ value: 'at_least', label: '以上' },
|
|
630
689
|
{ value: 'at_most', label: '以下' },
|
|
@@ -681,9 +740,19 @@ function renderObjectiveForm(root, state, callbacks) {
|
|
|
681
740
|
const title = state.editor.mode === 'create' ? '目的を作成' : '目的を編集';
|
|
682
741
|
append(section,
|
|
683
742
|
makeElement('div', { className: 'objective-editor-panel-heading', text: title }),
|
|
684
|
-
makeElement('p', { className: 'objective-editor-muted', text: '
|
|
743
|
+
makeElement('p', { className: 'objective-editor-muted', text: '保存すると新しい版になり、保存した版を読み戻して表示します。' }),
|
|
685
744
|
);
|
|
686
745
|
if (state.save.state !== 'idle' && state.save.state !== 'saving') section.append(statusNotice(state.save, { onRetry: callbacks.onRetrySave }));
|
|
746
|
+
section.append(buildObjectiveForm(state, callbacks));
|
|
747
|
+
return section;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* The edit form. The tabs layout ends it with 保存 and 一覧へ戻る; the
|
|
752
|
+
* workspace layout (in the rail) ends it with 保存 and キャンセル.
|
|
753
|
+
*/
|
|
754
|
+
function buildObjectiveForm(state, callbacks, { workspace = false } = {}) {
|
|
755
|
+
const draft = state.editor.draft;
|
|
687
756
|
const form = makeElement('form', { className: 'objective-editor-form' });
|
|
688
757
|
const identity = makeElement('div', { className: 'objective-editor-identity' });
|
|
689
758
|
const idField = field('目的ID', 'id', draft.id, { disabled: state.editor.mode !== 'create', attrs: { required: true } });
|
|
@@ -707,36 +776,37 @@ function renderObjectiveForm(root, state, callbacks) {
|
|
|
707
776
|
{ value: 'retired', label: '終了' },
|
|
708
777
|
] }).label);
|
|
709
778
|
form.append(governance);
|
|
710
|
-
form.append(renderCriteria(
|
|
711
|
-
form.append(renderReadiness(
|
|
712
|
-
form.append(renderConstraintRefs(
|
|
779
|
+
form.append(renderCriteria(form, draft, callbacks));
|
|
780
|
+
form.append(renderReadiness(form, state.readiness));
|
|
781
|
+
form.append(renderConstraintRefs(form, state, callbacks));
|
|
713
782
|
const actions = makeElement('div', { className: 'objective-editor-actions' });
|
|
714
783
|
actions.append(makeElement('span', { className: 'objective-editor-muted', text: callbacks.canEdit ? '保存後に同じID・新版を再取得して確認します。' : '読み取り専用です。書込み権限はホストから提供してください。' }));
|
|
715
|
-
|
|
716
|
-
actions.append(
|
|
784
|
+
const saving = state.save.state === 'saving';
|
|
785
|
+
if (callbacks.canEdit) actions.append(makeElement('button', { className: workspace ? 'bb-ws-button is-primary' : 'objective-editor-button-primary', text: saving ? '保存中…' : '保存', attrs: { type: 'submit' }, disabled: saving }));
|
|
786
|
+
if (workspace) actions.append(makeButton('キャンセル', callbacks.onCancelEdit, 'bb-ws-button'));
|
|
787
|
+
else actions.append(makeButton('一覧へ戻る', callbacks.onBack, 'objective-editor-button-secondary'));
|
|
717
788
|
form.append(actions);
|
|
718
789
|
form.addEventListener('submit', (event) => {
|
|
719
790
|
event.preventDefault();
|
|
720
791
|
callbacks.onSubmit?.(collectDefinition(form, draft));
|
|
721
792
|
});
|
|
722
|
-
|
|
723
|
-
return section;
|
|
793
|
+
return form;
|
|
724
794
|
}
|
|
725
795
|
|
|
726
796
|
function renderStoryLinks(root, state, callbacks) {
|
|
727
|
-
const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': '
|
|
797
|
+
const section = makeElement('section', { className: 'objective-editor-panel', attrs: { 'aria-label': 'ストーリーから参照される目的' } });
|
|
728
798
|
append(section,
|
|
729
|
-
makeElement('div', { className: 'objective-editor-panel-heading', text: '
|
|
730
|
-
makeElement('p', { className: 'objective-editor-muted', text: '
|
|
799
|
+
makeElement('div', { className: 'objective-editor-panel-heading', text: 'ストーリーから参照される目的' }),
|
|
800
|
+
makeElement('p', { className: 'objective-editor-muted', text: 'ストーリーの本文は複製せず、ストーリーが参照する目的とその版を表示します。' }),
|
|
731
801
|
);
|
|
732
802
|
const form = makeElement('form', { className: 'objective-editor-inline-form' });
|
|
733
|
-
const storyField = field('
|
|
803
|
+
const storyField = field('ストーリーID', 'story_id', state.story.storyId ?? '', { attrs: { required: true, placeholder: 'story-...' } });
|
|
734
804
|
form.append(storyField.label, makeElement('button', { className: 'objective-editor-button-secondary', text: '参照を取得', attrs: { type: 'submit' } }));
|
|
735
805
|
form.addEventListener('submit', (event) => { event.preventDefault(); callbacks.onLoadStoryLinks?.(storyField.input.value.trim()); });
|
|
736
806
|
section.append(form);
|
|
737
|
-
if (state.story.state === 'loading') section.append(makeElement('p', { className: 'objective-editor-muted', text: '
|
|
807
|
+
if (state.story.state === 'loading') section.append(makeElement('p', { className: 'objective-editor-muted', text: 'ストーリーとの関係を読み込んでいます。' }));
|
|
738
808
|
else if (['permission_denied', 'api_unavailable', 'error_retryable', 'unknown'].includes(state.story.state)) section.append(statusNotice(state.story, { onRetry: callbacks.onRetryStory }));
|
|
739
|
-
else if (state.story.state === 'empty') section.append(makeElement('p', { className: 'objective-editor-empty', text: '
|
|
809
|
+
else if (state.story.state === 'empty') section.append(makeElement('p', { className: 'objective-editor-empty', text: 'このストーリーから参照される目的はありません。' }));
|
|
740
810
|
else if (state.story.links) {
|
|
741
811
|
const list = makeElement('ul', { className: 'objective-editor-link-list' });
|
|
742
812
|
for (const link of state.story.links) {
|
|
@@ -744,7 +814,7 @@ function renderStoryLinks(root, state, callbacks) {
|
|
|
744
814
|
append(item,
|
|
745
815
|
statusBadge(link.relation, link.relationLabel),
|
|
746
816
|
makeElement('strong', { text: `${link.objective.id}@${link.objective.revision}` }),
|
|
747
|
-
makeElement('span', { text: text(link.objective.meaning, '
|
|
817
|
+
makeElement('span', { text: text(link.objective.meaning, '目的の意味 未確認') }),
|
|
748
818
|
);
|
|
749
819
|
if (link.timeCondition) item.append(makeElement('small', { text: `時間条件: ${JSON.stringify(link.timeCondition)}` }));
|
|
750
820
|
list.append(item);
|
|
@@ -764,9 +834,9 @@ export function renderObjectiveEditor(root, state, callbacks = {}) {
|
|
|
764
834
|
const header = makeElement('header', { className: 'objective-editor-header' });
|
|
765
835
|
const copy = makeElement('div', { className: 'objective-editor-header-copy' });
|
|
766
836
|
append(copy,
|
|
767
|
-
makeElement('div', { className: 'objective-editor-eyebrow', text: 'BRAINBASE /
|
|
837
|
+
makeElement('div', { className: 'objective-editor-eyebrow', text: 'BRAINBASE / OBJECTIVES' }),
|
|
768
838
|
makeElement('h1', { text: '目的と評価基準' }),
|
|
769
|
-
makeElement('p', { text: '
|
|
839
|
+
makeElement('p', { text: '目指す状態と評価基準を、版つきで編集・確認します。現状の理解(観測や仮説)とは分けて扱います。' }),
|
|
770
840
|
);
|
|
771
841
|
const headerStatus = state.save.state !== 'idle' ? state.save.state : state.objectives.state;
|
|
772
842
|
header.append(copy, statusBadge(headerStatus));
|
|
@@ -775,7 +845,7 @@ export function renderObjectiveEditor(root, state, callbacks = {}) {
|
|
|
775
845
|
const tabs = [
|
|
776
846
|
['list', '目的一覧'],
|
|
777
847
|
['editor', '目的を編集'],
|
|
778
|
-
['story', 'Story参照'],
|
|
848
|
+
...(callbacks.storyLinks === false ? [] : [['story', 'Story参照']]),
|
|
779
849
|
];
|
|
780
850
|
for (const [id, label] of tabs) toolbar.append(makeButton(label, () => callbacks.onView?.(id), `objective-editor-tab${state.view === id ? ' is-active' : ''}`));
|
|
781
851
|
toolbar.append(makeButton('再読込', callbacks.onReload, 'objective-editor-button-secondary'));
|
|
@@ -784,13 +854,300 @@ export function renderObjectiveEditor(root, state, callbacks = {}) {
|
|
|
784
854
|
const connectionState = state.connection;
|
|
785
855
|
if (connectionState && connectionState !== 'ready') wrapper.append(statusNotice({ state: connectionState, message: state.connectionMessage }, { onRetry: callbacks.onReload }));
|
|
786
856
|
const content = makeElement('main', { className: 'objective-editor-content' });
|
|
787
|
-
const renderer = state.view === 'editor' ? renderObjectiveForm
|
|
857
|
+
const renderer = state.view === 'editor' ? renderObjectiveForm
|
|
858
|
+
: state.view === 'story' && callbacks.storyLinks !== false ? renderStoryLinks
|
|
859
|
+
: renderObjectiveList;
|
|
788
860
|
content.append(renderer(content, state, callbacks));
|
|
789
861
|
wrapper.append(content);
|
|
790
862
|
root.append(wrapper);
|
|
791
863
|
return wrapper;
|
|
792
864
|
}
|
|
793
865
|
|
|
866
|
+
/* ---- Workspace layout (the organization edition's pattern; used when the host gives a rail) ---- */
|
|
867
|
+
|
|
868
|
+
const OPERATOR_LABELS = Object.freeze({ at_least: '以上', at_most: '以下', equals: '一致' });
|
|
869
|
+
const FAILED_STATES = Object.freeze(['permission_denied', 'api_unavailable', 'missing', 'conflict', 'error_retryable', 'unknown']);
|
|
870
|
+
|
|
871
|
+
/** `2026/7/1`, or `7/1` when the year is `sameYearAs`'s; text that is not a date stays as it is. */
|
|
872
|
+
function ledgerDate(value, sameYearAs = null) {
|
|
873
|
+
const input = nonEmptyText(value);
|
|
874
|
+
if (!input) return null;
|
|
875
|
+
const date = new Date(input);
|
|
876
|
+
if (Number.isNaN(date.getTime())) return input;
|
|
877
|
+
const day = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
878
|
+
return sameYearAs !== null && sameYearAs === date.getFullYear() ? day : `${date.getFullYear()}/${day}`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function periodText(period, { compact = false } = {}) {
|
|
882
|
+
const from = shortDate(period?.from);
|
|
883
|
+
const until = shortDate(period?.until);
|
|
884
|
+
if (!from && !until) return null;
|
|
885
|
+
if (!compact) return `${from ?? '未設定'}〜${until ?? '未設定'}`;
|
|
886
|
+
// In the ledger the dates are short and the end drops its year when it is the start's year.
|
|
887
|
+
const start = new Date(nonEmptyText(period?.from) ?? '');
|
|
888
|
+
const startYear = Number.isNaN(start.getTime()) ? null : start.getFullYear();
|
|
889
|
+
return `${ledgerDate(period?.from) ?? '未設定'}〜${ledgerDate(period?.until, startYear) ?? '未設定'}`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** A state as a kit notice: failures are alerts, and never read as zero items. */
|
|
893
|
+
function workspaceStatusNotice(doc, value, { label, onRetry, retryLabel = '再試行' } = {}) {
|
|
894
|
+
const normalized = typeof value === 'string' ? { state: value } : value ?? {};
|
|
895
|
+
const status = normalizeState(normalized.state);
|
|
896
|
+
const tone = status === 'verified' ? 'info'
|
|
897
|
+
: ['unknown', 'saved_unverified'].includes(status) ? 'warning'
|
|
898
|
+
: 'danger';
|
|
899
|
+
const body = makeElement('div', { className: 'bb-objective-notice-body' });
|
|
900
|
+
body.append(makeElement('p', { text: `${STATUS_LABELS[status] ?? STATUS_LABELS.unknown}:${normalized.message ?? STATUS_LABELS[status] ?? '状態を確認できません。'}` }));
|
|
901
|
+
if (status === 'conflict' && normalized.currentRevision) {
|
|
902
|
+
body.append(makeElement('p', { text: `現在の版: ${normalized.currentRevision}。入力は残しています。現在の版を確かめてから直してください。` }));
|
|
903
|
+
}
|
|
904
|
+
if (onRetry) body.append(workspaceButton(doc, { text: retryLabel, variant: 'quiet', onClick: onRetry }));
|
|
905
|
+
return workspaceNotice(doc, { label, text: body, tone });
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function objectiveReadinessCell(objective) {
|
|
909
|
+
const readiness = objective.readiness;
|
|
910
|
+
if (readiness?.state === 'ready' && readiness.ready === false) {
|
|
911
|
+
const count = readiness.issues?.length ?? 0;
|
|
912
|
+
return { text: count > 0 ? `使えない・不足${count}件` : '使えない', className: 'bb-objective-judgment is-draft' };
|
|
913
|
+
}
|
|
914
|
+
const state = objectiveJudgmentState(objective);
|
|
915
|
+
if (state === 'judgment_available') return { text: '使える', className: 'bb-objective-judgment is-available' };
|
|
916
|
+
if (state === 'draft') return { text: '使えない・下書き', className: 'bb-objective-judgment is-draft' };
|
|
917
|
+
return { text: '未確認', className: 'bb-objective-judgment is-unresolved' };
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function renderObjectiveMetrics(doc, state) {
|
|
921
|
+
const records = ['ready', 'empty'].includes(state.objectives.state) && Array.isArray(state.objectives.records) ? state.objectives.records : null;
|
|
922
|
+
const judged = records ? records.map((objective) => objectiveJudgmentState(objective)) : [];
|
|
923
|
+
const unknown = judged.filter((value) => value === 'judgment_unknown').length;
|
|
924
|
+
return workspaceMetrics(doc, [
|
|
925
|
+
{ label: '目的', value: records ? records.length : null, note: '読めた目的' },
|
|
926
|
+
{
|
|
927
|
+
label: '判断に使える',
|
|
928
|
+
value: records ? judged.filter((value) => value === 'judgment_available').length : null,
|
|
929
|
+
note: unknown > 0 ? `使えるか未確認 ${unknown}件` : '評価基準と参照が揃った目的',
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
label: '下書き',
|
|
933
|
+
value: records ? records.filter((objective) => ['draft', 'proposed'].includes(objective.adoptionState)).length : null,
|
|
934
|
+
note: '下書き・提案中',
|
|
935
|
+
},
|
|
936
|
+
], { ariaLabel: '目的の集計' });
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const OBJECTIVE_COLUMNS = Object.freeze(['目的', '望ましい状態', '状態', '判断に使えるか', '評価期間']);
|
|
940
|
+
|
|
941
|
+
function renderObjectiveLedger(doc, state, callbacks) {
|
|
942
|
+
const objectives = state.objectives;
|
|
943
|
+
if (objectives.state === 'idle' && state.connection !== 'ready') {
|
|
944
|
+
return workspaceStatusNotice(doc, { state: state.connection, message: state.connectionMessage }, { label: '目的', onRetry: callbacks.onReload, retryLabel: '再読込' });
|
|
945
|
+
}
|
|
946
|
+
if (FAILED_STATES.includes(objectives.state)) {
|
|
947
|
+
return workspaceStatusNotice(doc, objectives, { label: '目的', onRetry: callbacks.onReload, retryLabel: '再読込' });
|
|
948
|
+
}
|
|
949
|
+
const loading = objectives.state === 'loading' || objectives.state === 'idle';
|
|
950
|
+
const records = loading ? [] : objectives.records ?? [];
|
|
951
|
+
return workspaceLedger(doc, {
|
|
952
|
+
className: 'bb-objective-ledger',
|
|
953
|
+
ariaLabel: '目的の一覧',
|
|
954
|
+
columns: OBJECTIVE_COLUMNS,
|
|
955
|
+
empty: loading ? '目的を読み込んでいます。' : '目的はまだ登録されていません。',
|
|
956
|
+
rows: records.map((objective) => {
|
|
957
|
+
const period = periodText(objective.evaluationPeriod, { compact: true });
|
|
958
|
+
return {
|
|
959
|
+
key: objective.id,
|
|
960
|
+
selected: objective.id === state.selectedId,
|
|
961
|
+
onSelect: () => callbacks.onSelect?.(objective),
|
|
962
|
+
cells: [
|
|
963
|
+
{ primary: text(objective.meaning, objective.id), secondary: `${objective.id}@${objective.revision}` },
|
|
964
|
+
objective.desiredState ?? { text: '未設定', className: 'is-unresolved' },
|
|
965
|
+
objective.adoptionState ? (STATUS_LABELS[objective.adoptionState] ?? objective.adoptionState) : { text: '未記録', className: 'is-unresolved' },
|
|
966
|
+
objectiveReadinessCell(objective),
|
|
967
|
+
period ?? { text: '未設定', className: 'is-unresolved' },
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
}),
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Render the workspace (page head, metrics, ledger) and the rail. `host` is
|
|
976
|
+
* what the host adds to the page head: `{ title, lead, sourceNotice, pageActions }`
|
|
977
|
+
* (see `createObjectiveEditorController`).
|
|
978
|
+
*/
|
|
979
|
+
export function renderObjectiveWorkspace(root, rail, state, callbacks = {}, page = {}, host = {}) {
|
|
980
|
+
const doc = getDocument();
|
|
981
|
+
clear(root);
|
|
982
|
+
const wrapper = makeElement('div', {
|
|
983
|
+
className: 'bb-objective-workspace',
|
|
984
|
+
attrs: { 'data-contract-version': OBJECTIVE_EDITOR_CONTRACT_VERSION, 'data-layout': 'workspace' },
|
|
985
|
+
});
|
|
986
|
+
const actions = [{ text: '再読込', onClick: callbacks.onReload }];
|
|
987
|
+
if (callbacks.canEdit) actions.push({ text: '新しい目的', variant: 'primary', onClick: callbacks.onCreate });
|
|
988
|
+
actions.push(...workspaceHostActions(host?.pageActions));
|
|
989
|
+
wrapper.append(workspacePageHeader(doc, {
|
|
990
|
+
crumbs: Array.isArray(page?.crumbs) ? page.crumbs : [],
|
|
991
|
+
title: nonEmptyText(host?.title) ?? '目的と現状',
|
|
992
|
+
lead: nonEmptyText(host?.lead) ?? '目指す状態と評価基準を、版つきで確かめて直します。下の「現状と見通し」は、目的とは分けた、いまの理解(観測と仮説)です。',
|
|
993
|
+
source: nonEmptyText(page?.source),
|
|
994
|
+
actions,
|
|
995
|
+
}));
|
|
996
|
+
const notice = workspaceHostNotice(doc, host?.sourceNotice);
|
|
997
|
+
if (notice) wrapper.append(notice);
|
|
998
|
+
wrapper.append(renderObjectiveMetrics(doc, state), renderObjectiveLedger(doc, state, callbacks));
|
|
999
|
+
if (callbacks.storyLinks !== false) {
|
|
1000
|
+
const story = makeElement('div', { className: 'bb-objective-story' });
|
|
1001
|
+
story.append(renderStoryLinks(story, state, callbacks));
|
|
1002
|
+
wrapper.append(story);
|
|
1003
|
+
}
|
|
1004
|
+
root.append(wrapper);
|
|
1005
|
+
if (rail) renderObjectiveRail(doc, rail, state, callbacks);
|
|
1006
|
+
return wrapper;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function criteriaContent(doc, criteria) {
|
|
1010
|
+
if (criteria === null || criteria === undefined) return makeElement('p', { className: 'is-unrecorded', text: '評価基準を確認できません。' });
|
|
1011
|
+
if (!criteria.length) return makeElement('p', { text: '評価基準はまだありません。判断に使うには、測る変数の参照が必要です。' });
|
|
1012
|
+
const list = makeElement('ul', { className: 'bb-objective-list' });
|
|
1013
|
+
for (const criterion of criteria) {
|
|
1014
|
+
const item = makeElement('li');
|
|
1015
|
+
const operator = OPERATOR_LABELS[criterion.operator] ?? criterion.operator;
|
|
1016
|
+
const target = criterion.target === undefined ? null : optionalText(criterion.target);
|
|
1017
|
+
item.append(
|
|
1018
|
+
makeElement('code', { text: `${criterion.variableRef.id}@${criterion.variableRef.revision}` }),
|
|
1019
|
+
makeElement('span', { text: target === null ? `基準値なし(${operator})` : criterion.operator === 'equals' ? `${target} と一致` : `${target} ${operator}` }),
|
|
1020
|
+
);
|
|
1021
|
+
list.append(item);
|
|
1022
|
+
}
|
|
1023
|
+
return list;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function constraintContent(doc, state, callbacks) {
|
|
1027
|
+
if (state.constraints.state === 'loading') return makeElement('p', { text: '制約の参照を読み込んでいます。' });
|
|
1028
|
+
if (['permission_denied', 'api_unavailable', 'error_retryable', 'unknown'].includes(state.constraints.state)) {
|
|
1029
|
+
return workspaceStatusNotice(doc, state.constraints, { label: '制約', onRetry: callbacks.onReloadConstraints, retryLabel: '再読込' });
|
|
1030
|
+
}
|
|
1031
|
+
const refs = state.editor.constraintRefs ?? state.constraints.refs ?? [];
|
|
1032
|
+
const content = [];
|
|
1033
|
+
if (!refs.length) content.push(makeElement('p', { text: 'この目的に紐づく制約の参照はありません。' }));
|
|
1034
|
+
else {
|
|
1035
|
+
const list = makeElement('ul', { className: 'bb-objective-list' });
|
|
1036
|
+
for (const ref of refs) {
|
|
1037
|
+
const item = makeElement('li');
|
|
1038
|
+
item.append(makeElement('code', { text: `${ref.id}@${ref.revision}` }));
|
|
1039
|
+
if (ref.meaning) item.append(makeElement('span', { text: ref.meaning }));
|
|
1040
|
+
list.append(item);
|
|
1041
|
+
}
|
|
1042
|
+
content.push(list);
|
|
1043
|
+
}
|
|
1044
|
+
content.push(makeElement('p', {
|
|
1045
|
+
className: 'bb-objective-note',
|
|
1046
|
+
text: callbacks.constraintsEditable === false || !callbacks.canEdit
|
|
1047
|
+
? '制約の参照はここでは表示だけです。'
|
|
1048
|
+
: '参照の追加と外しは「目的を直す」から行えます。',
|
|
1049
|
+
}));
|
|
1050
|
+
return content;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function readinessContent(doc, readiness) {
|
|
1054
|
+
const normalized = normalizeReadiness(readiness);
|
|
1055
|
+
if (normalized.state !== 'ready') {
|
|
1056
|
+
return [statusBadge('judgment_unknown'), makeElement('p', { text: '判断に使える状態かどうかを、保存先から確認できていません。' })];
|
|
1057
|
+
}
|
|
1058
|
+
if (normalized.ready) {
|
|
1059
|
+
return [statusBadge('judgment_available'), makeElement('p', { text: '必要な定義・基準・参照が揃っています。観測値の存在は別途確認します。' })];
|
|
1060
|
+
}
|
|
1061
|
+
const content = [statusBadge('draft', '判断に使えない')];
|
|
1062
|
+
if (normalized.issues?.length) {
|
|
1063
|
+
const list = makeElement('ul', { className: 'bb-objective-issues' });
|
|
1064
|
+
for (const issue of normalized.issues) list.append(makeElement('li', { text: `${issue.path}: ${issue.message}` }));
|
|
1065
|
+
content.push(list);
|
|
1066
|
+
}
|
|
1067
|
+
return content;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function renderObjectiveDetail(doc, state, callbacks) {
|
|
1071
|
+
const objective = state.selected;
|
|
1072
|
+
const parts = [workspaceRailHead(doc, { kicker: '目的', title: text(objective.meaning, objective.id), sub: `${objective.id}@${objective.revision}` })];
|
|
1073
|
+
if (state.save.state === 'verified' && state.save.reference?.id === objective.id) {
|
|
1074
|
+
parts.push(workspaceNotice(doc, {
|
|
1075
|
+
label: '保存',
|
|
1076
|
+
text: `${STATUS_LABELS.verified}:保存した版(${state.save.reference.id}@${state.save.reference.revision})を読み戻して確かめました。`,
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
parts.push(workspaceRailBlock(doc, {
|
|
1080
|
+
title: '概要',
|
|
1081
|
+
content: workspaceDefinition(doc, [
|
|
1082
|
+
['望ましい状態', objective.desiredState],
|
|
1083
|
+
['対象者', objective.beneficiaryIds?.length ? objective.beneficiaryIds.join('、') : null],
|
|
1084
|
+
['責任者', objective.accountableId],
|
|
1085
|
+
['採用状態', objective.adoptionState ? (STATUS_LABELS[objective.adoptionState] ?? objective.adoptionState) : null],
|
|
1086
|
+
['評価期間', periodText(objective.evaluationPeriod)],
|
|
1087
|
+
]),
|
|
1088
|
+
}));
|
|
1089
|
+
parts.push(workspaceRailBlock(doc, { title: `評価基準${objective.criteria ? ` ${objective.criteria.length}件` : ''}`, className: 'bb-objective-criteria-block', content: criteriaContent(doc, objective.criteria) }));
|
|
1090
|
+
parts.push(workspaceRailBlock(doc, { title: '制約の参照', className: 'bb-objective-constraints-block', content: constraintContent(doc, state, callbacks) }));
|
|
1091
|
+
parts.push(workspaceRailBlock(doc, { title: '判断に使えるか', className: 'bb-objective-readiness-block', content: readinessContent(doc, state.readiness) }));
|
|
1092
|
+
if (callbacks.canEdit) {
|
|
1093
|
+
parts.push(workspaceActions(doc, [{ text: '目的を直す', variant: 'primary', onClick: callbacks.onEdit }]));
|
|
1094
|
+
}
|
|
1095
|
+
return parts;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function renderObjectiveFormRail(doc, state, callbacks) {
|
|
1099
|
+
const creating = state.editor.mode === 'create';
|
|
1100
|
+
const draft = state.editor.draft;
|
|
1101
|
+
const parts = [workspaceRailHead(doc, {
|
|
1102
|
+
kicker: '目的',
|
|
1103
|
+
title: creating ? '新しい目的' : '目的を直す',
|
|
1104
|
+
sub: creating ? '保存すると版1になります。' : `${draft.id}@${state.editor.expectedRevision ?? draft.revision} を元に新しい版を作ります。`,
|
|
1105
|
+
})];
|
|
1106
|
+
if (state.save.state !== 'idle' && state.save.state !== 'saving') {
|
|
1107
|
+
// A conflict keeps the input; saving again with the same revision would fail again, so no retry there.
|
|
1108
|
+
parts.push(workspaceStatusNotice(doc, state.save, {
|
|
1109
|
+
label: '保存',
|
|
1110
|
+
onRetry: state.save.state === 'conflict' ? null : callbacks.onRetrySave,
|
|
1111
|
+
retryLabel: 'もう一度保存',
|
|
1112
|
+
}));
|
|
1113
|
+
}
|
|
1114
|
+
parts.push(buildObjectiveForm(state, callbacks, { workspace: true }));
|
|
1115
|
+
return parts;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function renderObjectiveRail(doc, rail, state, callbacks) {
|
|
1119
|
+
const panel = makeElement('div', { className: 'bb-objective-rail', attrs: { 'aria-label': '選択中の目的' } });
|
|
1120
|
+
const pending = state.selectedId ? (state.objectives.records ?? []).find((objective) => objective.id === state.selectedId) : null;
|
|
1121
|
+
if (state.railMode === 'form' && state.editor.draft) {
|
|
1122
|
+
panel.setAttribute('data-rail-mode', 'form');
|
|
1123
|
+
panel.append(...renderObjectiveFormRail(doc, state, callbacks));
|
|
1124
|
+
} else if (state.editor.state === 'loading') {
|
|
1125
|
+
panel.setAttribute('data-rail-mode', 'loading');
|
|
1126
|
+
panel.append(
|
|
1127
|
+
workspaceRailHead(doc, { kicker: '目的', title: pending ? text(pending.meaning, pending.id) : '目的', sub: pending ? `${pending.id}@${pending.revision}` : null }),
|
|
1128
|
+
workspaceNotice(doc, { label: '目的', text: '目的を読み込んでいます。' }),
|
|
1129
|
+
);
|
|
1130
|
+
} else if (FAILED_STATES.includes(state.editor.state)) {
|
|
1131
|
+
panel.setAttribute('data-rail-mode', 'error');
|
|
1132
|
+
panel.append(
|
|
1133
|
+
workspaceRailHead(doc, { kicker: '目的', title: pending ? text(pending.meaning, pending.id) : '目的', sub: pending ? `${pending.id}@${pending.revision}` : null }),
|
|
1134
|
+
workspaceStatusNotice(doc, state.editor, { label: '目的', onRetry: pending ? () => callbacks.onSelect?.(pending) : null, retryLabel: '再読込' }),
|
|
1135
|
+
);
|
|
1136
|
+
} else if (state.selected) {
|
|
1137
|
+
panel.setAttribute('data-rail-mode', 'detail');
|
|
1138
|
+
panel.append(...renderObjectiveDetail(doc, state, callbacks));
|
|
1139
|
+
} else {
|
|
1140
|
+
panel.setAttribute('data-rail-mode', 'empty');
|
|
1141
|
+
panel.append(workspaceDetailEmpty(doc, {
|
|
1142
|
+
mark: '目',
|
|
1143
|
+
title: '目的を選択',
|
|
1144
|
+
text: '一覧から選ぶと、評価基準・制約の参照・判断に使えるかを表示します。',
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
rail.replaceChildren(panel);
|
|
1148
|
+
return panel;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
794
1151
|
function extractMutationRef(payload) {
|
|
795
1152
|
const object = objectValue(payload);
|
|
796
1153
|
if (!object) return null;
|
|
@@ -820,17 +1177,34 @@ function portMethod(port, name) {
|
|
|
820
1177
|
* CompanyOsObjectives where possible. The host owns FoundationStore,
|
|
821
1178
|
* authentication and Story/Constraint adapters; the UI only supplies the
|
|
822
1179
|
* trusted context it was given and never accepts owner/scope from form data.
|
|
1180
|
+
*
|
|
1181
|
+
* Host options used only in the workspace layout (when `rail` is given); the
|
|
1182
|
+
* tabs layout without `rail` ignores them:
|
|
1183
|
+
* - `title` / `lead`: replace the page head's 目的と現状 and its lead.
|
|
1184
|
+
* - `sourceNotice: { label, text }`: a notice right after the page head
|
|
1185
|
+
* (`text` is a string or an element). Default: none.
|
|
1186
|
+
* - `pageActions: [{ text, variant, onClick, disabled }]`: host-owned buttons in
|
|
1187
|
+
* the page head after 再読込 / 新しい目的.
|
|
1188
|
+
* All four are read from `options` on every render, so a host may change them
|
|
1189
|
+
* and call `render()`.
|
|
823
1190
|
*/
|
|
824
1191
|
export function createObjectiveEditorController(options = {}) {
|
|
825
1192
|
const root = options.root ?? null;
|
|
1193
|
+
// With a rail the screen uses the workspace layout; without one it keeps its tabs.
|
|
1194
|
+
const rail = options.rail ?? null;
|
|
1195
|
+
const page = options.page ?? {};
|
|
826
1196
|
const port = options.port ?? null;
|
|
827
1197
|
const context = options.context ?? {};
|
|
828
1198
|
const canEdit = options.canEdit === true;
|
|
1199
|
+
// Hosts without a constraint-link writer show constraints read-only.
|
|
1200
|
+
const constraintsEditable = options.constraintsEditable !== false;
|
|
1201
|
+
// Hosts without a Story provider hide the Story tab instead of showing an unavailable API.
|
|
1202
|
+
const storyLinks = options.storyLinks !== false;
|
|
829
1203
|
const createDefaults = options.createDefaults ?? {};
|
|
830
1204
|
const state = {
|
|
831
1205
|
view: options.initialView ?? 'list',
|
|
832
1206
|
connection: port ? 'ready' : 'api_unavailable',
|
|
833
|
-
connectionMessage: port ? null : '
|
|
1207
|
+
connectionMessage: port ? null : '目的を読み書きする経路が、この画面に渡されていません。',
|
|
834
1208
|
objectives: { state: 'idle', records: null, absence_confirmed: false },
|
|
835
1209
|
selected: null,
|
|
836
1210
|
readiness: { state: 'unknown', ready: null, issues: null },
|
|
@@ -838,12 +1212,20 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
838
1212
|
story: { state: 'idle', storyId: '', links: null, absence_confirmed: false },
|
|
839
1213
|
editor: { state: 'idle', mode: 'edit', draft: null, expectedRevision: null, constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false },
|
|
840
1214
|
save: { state: 'idle' },
|
|
1215
|
+
/** The objective the owner picked (workspace layout: the selected ledger row). */
|
|
1216
|
+
selectedId: null,
|
|
1217
|
+
/** What the rail shows in the workspace layout: the selected objective, or the edit form. */
|
|
1218
|
+
railMode: 'detail',
|
|
841
1219
|
};
|
|
1220
|
+
// The objective shown before 新しい目的, to return to when the owner cancels.
|
|
1221
|
+
let beforeCreate = null;
|
|
842
1222
|
|
|
843
1223
|
function render() {
|
|
844
1224
|
if (!root) return null;
|
|
845
|
-
|
|
1225
|
+
const callbacks = {
|
|
846
1226
|
canEdit,
|
|
1227
|
+
constraintsEditable,
|
|
1228
|
+
storyLinks,
|
|
847
1229
|
onView: (view) => { state.view = view; render(); },
|
|
848
1230
|
onReload: loadObjectives,
|
|
849
1231
|
onCreate: beginCreate,
|
|
@@ -858,7 +1240,10 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
858
1240
|
onReloadConstraints: () => loadConstraintRefs(state.selected),
|
|
859
1241
|
onLoadStoryLinks: loadStoryLinks,
|
|
860
1242
|
onRetryStory: () => loadStoryLinks(state.story.storyId),
|
|
861
|
-
|
|
1243
|
+
onEdit: beginEdit,
|
|
1244
|
+
onCancelEdit: cancelEdit,
|
|
1245
|
+
};
|
|
1246
|
+
return rail ? renderObjectiveWorkspace(root, rail, state, callbacks, page, options) : renderObjectiveEditor(root, state, callbacks);
|
|
862
1247
|
}
|
|
863
1248
|
|
|
864
1249
|
function setPortState(error, target, fallbackMessage = null) {
|
|
@@ -876,7 +1261,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
876
1261
|
render();
|
|
877
1262
|
const method = portMethod(port, 'listObjectives');
|
|
878
1263
|
if (!method) {
|
|
879
|
-
state.objectives = { state: 'api_unavailable', records: null, absence_confirmed: false, message: '
|
|
1264
|
+
state.objectives = { state: 'api_unavailable', records: null, absence_confirmed: false, message: '目的の一覧を読む経路がありません。' };
|
|
880
1265
|
state.connection = 'api_unavailable';
|
|
881
1266
|
render();
|
|
882
1267
|
return state.objectives;
|
|
@@ -889,6 +1274,9 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
889
1274
|
state.connection = state.objectives.state;
|
|
890
1275
|
}
|
|
891
1276
|
render();
|
|
1277
|
+
// As on the organization screens, the workspace opens with the first row selected.
|
|
1278
|
+
const first = state.objectives.records?.[0];
|
|
1279
|
+
if (rail && first && !state.selectedId && state.railMode !== 'form') await selectObjective(first);
|
|
892
1280
|
return state.objectives;
|
|
893
1281
|
}
|
|
894
1282
|
|
|
@@ -896,13 +1284,13 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
896
1284
|
state.constraints = { state: 'loading', refs: null, absence_confirmed: false };
|
|
897
1285
|
render();
|
|
898
1286
|
if (!objective) {
|
|
899
|
-
state.constraints = { state: 'unknown', refs: null, absence_confirmed: false, message: '
|
|
1287
|
+
state.constraints = { state: 'unknown', refs: null, absence_confirmed: false, message: '目的が選ばれていません。' };
|
|
900
1288
|
render();
|
|
901
1289
|
return state.constraints;
|
|
902
1290
|
}
|
|
903
1291
|
const method = portMethod(port, 'listObjectiveConstraintRefs') ?? portMethod(port, 'listConstraintReferences');
|
|
904
1292
|
if (!method) {
|
|
905
|
-
state.constraints = { state: 'api_unavailable', refs: null, absence_confirmed: false, message: '
|
|
1293
|
+
state.constraints = { state: 'api_unavailable', refs: null, absence_confirmed: false, message: '目的と制約の参照を読む経路がありません。' };
|
|
906
1294
|
state.editor.constraintRefs = null;
|
|
907
1295
|
render();
|
|
908
1296
|
return state.constraints;
|
|
@@ -926,7 +1314,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
926
1314
|
if (!objective) return state.readiness;
|
|
927
1315
|
const method = portMethod(port, 'checkObjectiveReadiness');
|
|
928
1316
|
if (!method) {
|
|
929
|
-
state.readiness = { state: 'unknown', ready: null, issues: null, message: '
|
|
1317
|
+
state.readiness = { state: 'unknown', ready: null, issues: null, message: '判断に使える状態を確かめる経路がありません。' };
|
|
930
1318
|
render();
|
|
931
1319
|
return state.readiness;
|
|
932
1320
|
}
|
|
@@ -945,6 +1333,12 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
945
1333
|
const id = reference?.id;
|
|
946
1334
|
const revision = reference?.revision;
|
|
947
1335
|
if (!id) return null;
|
|
1336
|
+
state.selectedId = id;
|
|
1337
|
+
if (rail) {
|
|
1338
|
+
state.railMode = 'detail';
|
|
1339
|
+
state.save = { state: 'idle' };
|
|
1340
|
+
beforeCreate = null;
|
|
1341
|
+
}
|
|
948
1342
|
state.view = 'editor';
|
|
949
1343
|
state.editor = { state: 'loading', mode: 'edit', draft: null, expectedRevision: revision ?? null, constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false };
|
|
950
1344
|
state.readiness = { state: 'unknown', ready: null, issues: null };
|
|
@@ -952,7 +1346,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
952
1346
|
render();
|
|
953
1347
|
const method = portMethod(port, 'readObjective');
|
|
954
1348
|
if (!method) {
|
|
955
|
-
state.editor = { ...state.editor, state: 'api_unavailable', message: '
|
|
1349
|
+
state.editor = { ...state.editor, state: 'api_unavailable', message: '目的を読む経路がありません。' };
|
|
956
1350
|
state.connection = 'api_unavailable';
|
|
957
1351
|
render();
|
|
958
1352
|
return null;
|
|
@@ -961,7 +1355,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
961
1355
|
const payload = await method(id, context, revision);
|
|
962
1356
|
const objective = normalizeObjectiveRecord(payload);
|
|
963
1357
|
if (!objective) {
|
|
964
|
-
state.editor = { ...state.editor, state: 'missing', message: '
|
|
1358
|
+
state.editor = { ...state.editor, state: 'missing', message: '指定した目的を保存先から確認できません。' };
|
|
965
1359
|
state.connection = 'missing';
|
|
966
1360
|
render();
|
|
967
1361
|
return null;
|
|
@@ -985,6 +1379,8 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
985
1379
|
}
|
|
986
1380
|
|
|
987
1381
|
function beginCreate() {
|
|
1382
|
+
if (rail && state.railMode !== 'form') beforeCreate = state.selected;
|
|
1383
|
+
state.railMode = 'form';
|
|
988
1384
|
state.view = 'editor';
|
|
989
1385
|
state.selected = null;
|
|
990
1386
|
state.readiness = { state: 'unknown', ready: null, issues: null };
|
|
@@ -1000,7 +1396,49 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1000
1396
|
render();
|
|
1001
1397
|
}
|
|
1002
1398
|
|
|
1399
|
+
/** Workspace layout: open the edit form for the selected objective in the rail. */
|
|
1400
|
+
function beginEdit() {
|
|
1401
|
+
if (!state.selected) return;
|
|
1402
|
+
state.editor = {
|
|
1403
|
+
...state.editor,
|
|
1404
|
+
state: 'ready', mode: 'edit', draft: editorDraftFromObjective(state.selected), expectedRevision: state.selected.revision,
|
|
1405
|
+
constraintRefs: state.constraints.refs ?? state.editor.constraintRefs ?? null,
|
|
1406
|
+
originalConstraintRefs: state.constraints.refs ? JSON.parse(JSON.stringify(state.constraints.refs)) : state.editor.originalConstraintRefs,
|
|
1407
|
+
constraintRefsChanged: false,
|
|
1408
|
+
};
|
|
1409
|
+
state.save = { state: 'idle' };
|
|
1410
|
+
state.railMode = 'form';
|
|
1411
|
+
render();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/** Workspace layout: leave the form without saving and show the objective again. */
|
|
1415
|
+
function cancelEdit() {
|
|
1416
|
+
const creating = state.editor.mode === 'create';
|
|
1417
|
+
state.railMode = 'detail';
|
|
1418
|
+
state.save = { state: 'idle' };
|
|
1419
|
+
if (creating) {
|
|
1420
|
+
const previous = beforeCreate;
|
|
1421
|
+
beforeCreate = null;
|
|
1422
|
+
state.editor = { state: 'idle', mode: 'edit', draft: null, expectedRevision: null, constraintRefs: null, originalConstraintRefs: null, constraintRefsChanged: false };
|
|
1423
|
+
state.selectedId = null;
|
|
1424
|
+
if (previous) {
|
|
1425
|
+
void selectObjective(previous);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
render();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (state.selected) {
|
|
1432
|
+
state.editor = {
|
|
1433
|
+
state: 'ready', mode: 'edit', draft: editorDraftFromObjective(state.selected), expectedRevision: state.selected.revision,
|
|
1434
|
+
constraintRefs: state.constraints.refs ?? null, originalConstraintRefs: state.constraints.refs ?? null, constraintRefsChanged: false,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
render();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1003
1440
|
function addConstraintRef(reference) {
|
|
1441
|
+
if (!constraintsEditable) return;
|
|
1004
1442
|
const normalized = normalizeReference(reference, 'constraint');
|
|
1005
1443
|
if (!normalized || normalized.type !== 'constraint') return;
|
|
1006
1444
|
const refs = state.editor.constraintRefs ?? [];
|
|
@@ -1011,6 +1449,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1011
1449
|
}
|
|
1012
1450
|
|
|
1013
1451
|
function removeConstraintRef(reference) {
|
|
1452
|
+
if (!constraintsEditable) return;
|
|
1014
1453
|
state.editor.constraintRefs = (state.editor.constraintRefs ?? []).filter((item) => !(item.id === reference.id && item.revision === reference.revision));
|
|
1015
1454
|
state.editor.constraintRefsChanged = true;
|
|
1016
1455
|
render();
|
|
@@ -1022,12 +1461,12 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1022
1461
|
const updateMethod = mode === 'create' ? null : portMethod(port, 'updateObjective');
|
|
1023
1462
|
const createMethod = mode === 'create' ? portMethod(port, 'createObjective') : null;
|
|
1024
1463
|
if ((mode === 'create' && !createMethod) || (mode !== 'create' && !updateMethod)) {
|
|
1025
|
-
state.save = { state: 'api_unavailable', message: '
|
|
1464
|
+
state.save = { state: 'api_unavailable', message: '目的を保存する経路がありません。' };
|
|
1026
1465
|
render();
|
|
1027
1466
|
return state.save;
|
|
1028
1467
|
}
|
|
1029
1468
|
if (state.editor.constraintRefsChanged && !portMethod(port, 'replaceObjectiveConstraintRefs')) {
|
|
1030
|
-
state.save = { state: 'api_unavailable', message: '
|
|
1469
|
+
state.save = { state: 'api_unavailable', message: '制約の参照を保存する経路が無いため、目的の本文も保存していません。' };
|
|
1031
1470
|
render();
|
|
1032
1471
|
return state.save;
|
|
1033
1472
|
}
|
|
@@ -1040,7 +1479,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1040
1479
|
: await updateMethod(definition.id, state.editor.expectedRevision, definition, context);
|
|
1041
1480
|
const savedRef = extractMutationRef(mutation);
|
|
1042
1481
|
if (!savedRef || savedRef.id !== definition.id || savedRef.type !== 'objective' || !savedRef.revision) {
|
|
1043
|
-
state.save = { state: 'saved_unverified', message: '
|
|
1482
|
+
state.save = { state: 'saved_unverified', message: '保存の応答に、同じ目的の新しい版が含まれていません。', mutation };
|
|
1044
1483
|
render();
|
|
1045
1484
|
return state.save;
|
|
1046
1485
|
}
|
|
@@ -1061,7 +1500,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1061
1500
|
const returnedDefinition = readback ? { id: readback.id, type: readback.type, revision: readback.revision, digest: readback.digest } : null;
|
|
1062
1501
|
const referenceMatches = definitionsEqual(savedRef, returnedDefinition);
|
|
1063
1502
|
if (!referenceMatches || !readback || (mode !== 'create' && (!expectedRevision || readback.revision === expectedRevision))) {
|
|
1064
|
-
state.save = { state: 'saved_unverified', message: '
|
|
1503
|
+
state.save = { state: 'saved_unverified', message: '保存した新しい版を読み戻して確かめられません。', mutation, readback };
|
|
1065
1504
|
render();
|
|
1066
1505
|
return state.save;
|
|
1067
1506
|
}
|
|
@@ -1074,19 +1513,25 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1074
1513
|
: null;
|
|
1075
1514
|
const verifiedRefs = normalizeReferenceCollection(constraintPayload).refs;
|
|
1076
1515
|
if (!verifiedRefs || !referencesEqual(verifiedRefs, state.editor.constraintRefs)) {
|
|
1077
|
-
state.save = { state: 'saved_unverified', message: '
|
|
1516
|
+
state.save = { state: 'saved_unverified', message: '目的は保存されましたが、制約の参照を読み戻した内容が一致しません。', mutation, readback, constraintRefs: verifiedRefs };
|
|
1078
1517
|
render();
|
|
1079
1518
|
return state.save;
|
|
1080
1519
|
}
|
|
1081
1520
|
}
|
|
1082
1521
|
state.selected = readback;
|
|
1522
|
+
state.selectedId = readback.id;
|
|
1523
|
+
state.railMode = 'detail';
|
|
1524
|
+
beforeCreate = null;
|
|
1083
1525
|
state.editor = {
|
|
1084
1526
|
state: 'ready', mode: 'edit', draft: editorDraftFromObjective(readback), expectedRevision: readback.revision,
|
|
1085
1527
|
constraintRefs: state.editor.constraintRefs ?? [], originalConstraintRefs: state.editor.constraintRefs ?? [], constraintRefsChanged: false,
|
|
1086
1528
|
};
|
|
1087
1529
|
state.save = { state: 'verified', action: mode, reference: savedRef, readback };
|
|
1088
1530
|
state.connection = 'ready';
|
|
1531
|
+
// Readiness belongs to a revision; re-read it for the one just saved.
|
|
1532
|
+
state.readiness = { state: 'unknown', ready: null, issues: null };
|
|
1089
1533
|
render();
|
|
1534
|
+
await loadReadiness(readback);
|
|
1090
1535
|
await loadObjectives();
|
|
1091
1536
|
return state.save;
|
|
1092
1537
|
} catch (error) {
|
|
@@ -1103,7 +1548,7 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1103
1548
|
render();
|
|
1104
1549
|
const method = portMethod(port, 'listStoryObjectiveLinks');
|
|
1105
1550
|
if (!method) {
|
|
1106
|
-
state.story = { state: 'api_unavailable', storyId, links: null, absence_confirmed: false, message: '
|
|
1551
|
+
state.story = { state: 'api_unavailable', storyId, links: null, absence_confirmed: false, message: 'ストーリーと目的の関係を読む経路がありません。' };
|
|
1107
1552
|
render();
|
|
1108
1553
|
return state.story;
|
|
1109
1554
|
}
|
|
@@ -1128,6 +1573,8 @@ export function createObjectiveEditorController(options = {}) {
|
|
|
1128
1573
|
loadStoryLinks,
|
|
1129
1574
|
addConstraintRef,
|
|
1130
1575
|
removeConstraintRef,
|
|
1576
|
+
beginEdit,
|
|
1577
|
+
cancelEdit,
|
|
1131
1578
|
contractVersion: OBJECTIVE_EDITOR_CONTRACT_VERSION,
|
|
1132
1579
|
};
|
|
1133
1580
|
render();
|