@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.
Files changed (91) hide show
  1. package/README.md +1 -1
  2. package/contracts/foundation/graph-history.sql +634 -0
  3. package/contracts/judgment-dag/digest.json +2 -2
  4. package/contracts/judgment-value-proof/schema.json +43 -1
  5. package/dist/canonical-task-service.js +16 -5
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +107 -0
  8. package/dist/company-os-objectives.d.ts +11 -0
  9. package/dist/company-os-objectives.js +84 -47
  10. package/dist/foundation-authenticated-tools.d.ts +56 -0
  11. package/dist/foundation-authenticated-tools.js +347 -0
  12. package/dist/foundation-graph-http.d.ts +43 -0
  13. package/dist/foundation-graph-http.js +243 -0
  14. package/dist/foundation-graph-write.d.ts +90 -0
  15. package/dist/foundation-graph-write.js +447 -0
  16. package/dist/foundation-public-provider.d.ts +140 -0
  17. package/dist/foundation-public-provider.js +192 -0
  18. package/dist/graph-corrections.d.ts +157 -0
  19. package/dist/graph-corrections.js +620 -0
  20. package/dist/graph-foundation-reader.d.ts +60 -0
  21. package/dist/graph-foundation-reader.js +494 -0
  22. package/dist/graph-retrieval.d.ts +6 -1
  23. package/dist/graph-revision-reader.d.ts +80 -0
  24. package/dist/graph-revision-reader.js +246 -0
  25. package/dist/graph-web-http.d.ts +26 -0
  26. package/dist/graph-web-http.js +215 -0
  27. package/dist/graph-web.d.ts +364 -0
  28. package/dist/graph-web.js +639 -0
  29. package/dist/judgment-problem-snapshot.d.ts +30 -2
  30. package/dist/judgment-problem-snapshot.js +302 -6
  31. package/dist/judgment-value-proof-review.d.ts +166 -0
  32. package/dist/judgment-value-proof-review.js +437 -0
  33. package/dist/judgment-value-proof.d.ts +28 -1
  34. package/dist/judgment-value-proof.js +53 -8
  35. package/dist/knowledge-continuation.d.ts +127 -0
  36. package/dist/knowledge-continuation.js +446 -0
  37. package/dist/knowledge-lookup.d.ts +178 -0
  38. package/dist/knowledge-lookup.js +758 -0
  39. package/dist/local-web-fetch-bridge.d.ts +5 -0
  40. package/dist/local-web-fetch-bridge.js +37 -0
  41. package/dist/local-web-host.d.ts +109 -0
  42. package/dist/local-web-host.js +688 -0
  43. package/dist/local-web-security.d.ts +25 -0
  44. package/dist/local-web-security.js +95 -0
  45. package/dist/organization-graph.d.ts +15 -0
  46. package/dist/organization-graph.js +24 -4
  47. package/dist/personal-knowledge.d.ts +12 -0
  48. package/dist/personal-knowledge.js +29 -0
  49. package/dist/personal-memory-handover.d.ts +108 -0
  50. package/dist/personal-memory-handover.js +369 -0
  51. package/dist/philosophy-revision-reader.d.ts +85 -0
  52. package/dist/philosophy-revision-reader.js +241 -0
  53. package/dist/portable-graph.d.ts +24 -0
  54. package/dist/portable-graph.js +77 -0
  55. package/dist/server.d.ts +7 -2
  56. package/dist/server.js +9 -6
  57. package/dist/value-proof-review-http.d.ts +25 -0
  58. package/dist/value-proof-review-http.js +185 -0
  59. package/dist/world-model.d.ts +21 -3
  60. package/dist/world-model.js +94 -12
  61. package/package.json +94 -6
  62. package/ui/brainbase-tokens.css +56 -0
  63. package/ui/graph-own-share.js +367 -0
  64. package/ui/graph-projects-view.css +23 -0
  65. package/ui/graph-projects-view.js +730 -0
  66. package/ui/graph-registry-view.css +101 -0
  67. package/ui/graph-registry-view.js +804 -0
  68. package/ui/graph-view-shared.css +522 -0
  69. package/ui/graph-view-shared.js +1227 -0
  70. package/ui/judgment-view.css +119 -45
  71. package/ui/local-web-shell.css +396 -0
  72. package/ui/local-web-shell.js +378 -0
  73. package/ui/objective-editor-http-port.js +154 -0
  74. package/ui/objective-editor.css +180 -74
  75. package/ui/objective-editor.js +490 -43
  76. package/ui/outcome-knowledge.css +50 -14
  77. package/ui/outcome-knowledge.js +15 -1
  78. package/ui/value-proof-review.css +504 -0
  79. package/ui/value-proof-review.js +1104 -0
  80. package/ui/workspace-kit.css +485 -0
  81. package/ui/workspace-kit.js +229 -0
  82. package/ui/world-model-view.css +188 -0
  83. package/ui/world-model-view.js +486 -0
  84. package/ui/icons/mana/LICENSE.tabler.txt +0 -21
  85. package/ui/icons/mana/affiliate.svg +0 -20
  86. package/ui/icons/mana/brand-github.svg +0 -15
  87. package/ui/icons/mana/brand-google-drive.svg +0 -17
  88. package/ui/icons/mana/brand-slack.svg +0 -18
  89. package/ui/icons/mana/cpu.svg +0 -24
  90. package/ui/outcome-mana.css +0 -255
  91. package/ui/outcome-mana.js +0 -2325
@@ -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
- const input = makeElement(options.tag ?? 'input', {
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'), statusBadge(objectiveJudgmentState(objective), objectiveJudgmentLabel(objective)));
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: 'readinessを正本APIから確認できていません。' }));
624
+ section.append(statusBadge('judgment_unknown'), makeElement('p', { className: 'objective-editor-muted', text: '判断に使える状態かどうかを、保存先から確認できていません。' }));
568
625
  return section;
569
626
  }
570
- section.append(statusBadge(normalized.ready ? 'judgment_available' : 'draft'));
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.canEdit && callbacks.onAddConstraintRef) {
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: '評価基準はまだありません。判断利用にはVariable参照が必要です。' }));
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('Variable ID', `criteria.${index}.variable_id`, variableRef.id ?? '', { attrs: { required: true } });
627
- const variableRevision = field('Variable版', `criteria.${index}.variable_revision`, variableRef.revision ?? '', { attrs: { required: true } });
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: '入力はFoundationRevisionStoreへ渡す定義の編集です。組織の権限判定はホストのportが担当します。' }),
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(root, draft, callbacks));
711
- form.append(renderReadiness(root, state.readiness));
712
- form.append(renderConstraintRefs(root, state, callbacks));
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
- if (callbacks.canEdit) actions.append(makeElement('button', { className: 'objective-editor-button-primary', text: state.save.state === 'saving' ? '保存中…' : '保存', attrs: { type: 'submit' }, disabled: state.save.state === 'saving' }));
716
- actions.append(makeButton('一覧へ戻る', callbacks.onBack, 'objective-editor-button-secondary'));
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
- section.append(form);
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': 'StoryからObjectiveを参照' } });
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: 'StoryからObjectiveを参照' }),
730
- makeElement('p', { className: 'objective-editor-muted', text: 'Story本文を複製せず、既存のtyped relationとObjectiveの版を表示します。' }),
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('Story ID', 'story_id', state.story.storyId ?? '', { attrs: { required: true, placeholder: 'story-...' } });
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: 'Storyの関係を読み込んでいます。' }));
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: 'このStoryから参照されるObjectiveはありません。' }));
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, 'Objectiveの意味 未確認') }),
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 / COMPANY OS' }),
837
+ makeElement('div', { className: 'objective-editor-eyebrow', text: 'BRAINBASE / OBJECTIVES' }),
768
838
  makeElement('h1', { text: '目的と評価基準' }),
769
- makeElement('p', { text: '世界の認識やStoryとは分けたObjectiveの正本を、版付きで編集・確認します。' }),
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 : state.view === 'story' ? renderStoryLinks : renderObjectiveList;
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 : 'ObjectiveEditorPortが提供されていません。組織サービスやDBへ直接接続しません。',
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
- return renderObjectiveEditor(root, state, {
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: '目的一覧APIが未提供です。' };
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: 'Objectiveが未選択です。' };
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: 'ObjectiveとConstraintの参照APIが未提供です。' };
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: 'readiness APIが未提供です。' };
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: 'Objective読取APIが未提供です。' };
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: '指定したObjectiveを正本から確認できません。' };
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: 'Objectiveの保存APIが未提供です。' };
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: 'Constraint参照の保存APIが未提供のため、目的本文も保存していません。' };
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: '保存応答にObjectiveの同じID・新版が含まれていません。', mutation };
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: '保存応答後の同じID・新版のreadbackを確認できません。', mutation, readback };
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: 'Objectiveは保存されましたが、Constraint参照のreadbackが一致しません。', mutation, readback, constraintRefs: verifiedRefs };
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: 'Story→Objective relation APIが未提供です。' };
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();