@unson/brainbase-mcp 0.7.0 → 0.8.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 (83) hide show
  1. package/contracts/foundation/graph-history.sql +634 -0
  2. package/contracts/judgment-value-proof/schema.json +43 -1
  3. package/dist/canonical-task-service.js +16 -5
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +107 -0
  6. package/dist/company-os-objectives.d.ts +11 -0
  7. package/dist/company-os-objectives.js +84 -47
  8. package/dist/foundation-authenticated-tools.d.ts +56 -0
  9. package/dist/foundation-authenticated-tools.js +347 -0
  10. package/dist/foundation-graph-http.d.ts +43 -0
  11. package/dist/foundation-graph-http.js +243 -0
  12. package/dist/foundation-graph-write.d.ts +90 -0
  13. package/dist/foundation-graph-write.js +447 -0
  14. package/dist/foundation-public-provider.d.ts +140 -0
  15. package/dist/foundation-public-provider.js +192 -0
  16. package/dist/graph-corrections.d.ts +157 -0
  17. package/dist/graph-corrections.js +620 -0
  18. package/dist/graph-foundation-reader.d.ts +60 -0
  19. package/dist/graph-foundation-reader.js +494 -0
  20. package/dist/graph-retrieval.d.ts +6 -1
  21. package/dist/graph-revision-reader.d.ts +80 -0
  22. package/dist/graph-revision-reader.js +246 -0
  23. package/dist/graph-web-http.d.ts +26 -0
  24. package/dist/graph-web-http.js +215 -0
  25. package/dist/graph-web.d.ts +364 -0
  26. package/dist/graph-web.js +639 -0
  27. package/dist/judgment-problem-snapshot.d.ts +30 -2
  28. package/dist/judgment-problem-snapshot.js +302 -6
  29. package/dist/judgment-value-proof-review.d.ts +166 -0
  30. package/dist/judgment-value-proof-review.js +437 -0
  31. package/dist/judgment-value-proof.d.ts +28 -1
  32. package/dist/judgment-value-proof.js +53 -8
  33. package/dist/knowledge-continuation.d.ts +127 -0
  34. package/dist/knowledge-continuation.js +446 -0
  35. package/dist/knowledge-lookup.d.ts +178 -0
  36. package/dist/knowledge-lookup.js +758 -0
  37. package/dist/local-web-fetch-bridge.d.ts +5 -0
  38. package/dist/local-web-fetch-bridge.js +37 -0
  39. package/dist/local-web-host.d.ts +109 -0
  40. package/dist/local-web-host.js +688 -0
  41. package/dist/local-web-security.d.ts +25 -0
  42. package/dist/local-web-security.js +95 -0
  43. package/dist/organization-graph.d.ts +15 -0
  44. package/dist/organization-graph.js +24 -4
  45. package/dist/personal-knowledge.d.ts +12 -0
  46. package/dist/personal-knowledge.js +29 -0
  47. package/dist/personal-memory-handover.d.ts +108 -0
  48. package/dist/personal-memory-handover.js +369 -0
  49. package/dist/philosophy-revision-reader.d.ts +85 -0
  50. package/dist/philosophy-revision-reader.js +241 -0
  51. package/dist/portable-graph.d.ts +24 -0
  52. package/dist/portable-graph.js +77 -0
  53. package/dist/server.d.ts +7 -2
  54. package/dist/server.js +9 -6
  55. package/dist/value-proof-review-http.d.ts +25 -0
  56. package/dist/value-proof-review-http.js +185 -0
  57. package/dist/world-model.d.ts +21 -3
  58. package/dist/world-model.js +94 -12
  59. package/package.json +94 -3
  60. package/ui/brainbase-tokens.css +56 -0
  61. package/ui/graph-own-share.js +367 -0
  62. package/ui/graph-projects-view.css +23 -0
  63. package/ui/graph-projects-view.js +730 -0
  64. package/ui/graph-registry-view.css +101 -0
  65. package/ui/graph-registry-view.js +804 -0
  66. package/ui/graph-view-shared.css +522 -0
  67. package/ui/graph-view-shared.js +1227 -0
  68. package/ui/judgment-view.css +119 -45
  69. package/ui/local-web-shell.css +396 -0
  70. package/ui/local-web-shell.js +378 -0
  71. package/ui/objective-editor-http-port.js +154 -0
  72. package/ui/objective-editor.css +180 -74
  73. package/ui/objective-editor.js +490 -43
  74. package/ui/outcome-knowledge.css +50 -14
  75. package/ui/outcome-knowledge.js +15 -1
  76. package/ui/outcome-mana.css +91 -14
  77. package/ui/outcome-mana.js +958 -61
  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
@@ -17,6 +17,55 @@ export const MANA_STORY_IDS = Object.freeze([
17
17
  'story-brainbase-outcome-mana-run-control',
18
18
  ]);
19
19
 
20
+ // These identifiers are accepted by the Mana contract API. The runtime
21
+ // preflight remains the authority for whether either profile can run.
22
+ // The contract form offers them only when the host passes them (or its own
23
+ // list with the same shape) as `executionProfiles`.
24
+ export const MANA_EXECUTION_PROFILES = Object.freeze([
25
+ Object.freeze({
26
+ id: 'meeting_minutes_github_v1',
27
+ label: '会議録を作成してGitHubへ保存',
28
+ input: 'この委任で選んだ会議資料',
29
+ output: '決定事項・担当・期限をまとめた会議録',
30
+ destination: 'この契約で選ぶGitHubリポジトリ',
31
+ inputConnectorIds: ['github'],
32
+ destinationConnectorId: 'github',
33
+ inputConnectorLabel: 'GitHub',
34
+ destinationConnectorLabel: 'GitHub',
35
+ }),
36
+ Object.freeze({
37
+ id: 'project_report_google_drive_v1',
38
+ label: 'プロジェクト報告を作成してGoogle Driveへ保存',
39
+ input: 'GitHubの確定コミットにあるプロジェクト資料',
40
+ output: '出典・版・取得証跡を保持したプロジェクト報告',
41
+ destination: 'この契約で選ぶGoogle Driveフォルダ',
42
+ inputConnectorIds: ['github'],
43
+ destinationConnectorId: 'drive',
44
+ inputConnectorLabel: 'GitHub',
45
+ destinationConnectorLabel: 'Google Drive',
46
+ }),
47
+ ]);
48
+
49
+ export function profileInputValidationReasons(profile, inputReferences = [], selectableResources = []) {
50
+ if (!profile) return ['委任する仕事を選んでください。'];
51
+ const references = Array.isArray(inputReferences) ? inputReferences : [];
52
+ const resources = Array.isArray(selectableResources) ? selectableResources : [];
53
+ const usable = references.filter((reference) => resources.some((resource) => resource.id === reference.id
54
+ && profile.inputConnectorIds.includes(resource.connectorId)));
55
+ const reasons = [];
56
+ if (usable.length === 0) reasons.push(`${profile.inputConnectorLabel}の接続確認済み入力資料を1つ以上選んでください。`);
57
+ if (usable.length !== references.length) reasons.push('選んだ仕事で利用できない入力資料が含まれています。入力資料を選び直してください。');
58
+ if (profile.id === 'project_report_google_drive_v1'
59
+ && references.some((reference) => !/^[0-9a-f]{40}$/u.test(reference.version ?? ''))) {
60
+ reasons.push('プロジェクト報告のGitHub資料は40文字のコミットSHAで版を固定してください。');
61
+ }
62
+ if (profile.id === 'meeting_minutes_github_v1'
63
+ && references.some((reference) => reference.id?.startsWith('github:repo:'))) {
64
+ reasons.push('会議録には文字起こしファイルを選んでください。リポジトリ全体の整理には「プロジェクト報告」を選んでください。');
65
+ }
66
+ return reasons;
67
+ }
68
+
20
69
  const UNKNOWN = '未確認';
21
70
  const EMPTY = 'empty';
22
71
  const STOPPED_RUN_RESTART_UNAVAILABLE = '停止した実行は再開できません。';
@@ -47,6 +96,37 @@ const STATUS_LABELS = Object.freeze({
47
96
  stale: '再試験が必要',
48
97
  permission_denied: '権限不足',
49
98
  error_retryable: '再試行可能なエラー',
99
+ available: '利用可能',
100
+ unavailable: '利用不可',
101
+ not_evaluated: '未評価',
102
+ });
103
+
104
+ const PREFLIGHT_ACTION_LABELS = Object.freeze({
105
+ review_contract: '成果契約を確認',
106
+ add_input_reference: '入力資料を追加',
107
+ connect_input_provider: '入力元アプリを接続',
108
+ configure_destination: '保存先を設定',
109
+ define_completion_criteria: '完了条件を設定',
110
+ connect_artifact_provider: '保存先アプリを接続',
111
+ verify_artifact_destination: '保存先を確認',
112
+ configure_generator: '生成機能を設定',
113
+ configure_trigger: '起動条件を設定',
114
+ connect_knowledge_provider: '知識基盤を接続',
115
+ configure_authority: '権限を設定',
116
+ });
117
+
118
+ const PREFLIGHT_REASON_LABELS = Object.freeze({
119
+ contract_invalid: '成果契約に不足があります',
120
+ input_missing: '入力資料が指定されていません',
121
+ input_adapter_missing: '入力元アプリが接続されていません',
122
+ destination_missing: '保存先が指定されていません',
123
+ completion_criteria_missing: '完了条件が定義されていません',
124
+ artifact_adapter_missing: '保存先アプリが接続されていません',
125
+ artifact_destination_unverified: '保存先を確認できていません',
126
+ generator_missing: '成果を生成する機能が設定されていません',
127
+ trigger_missing: '起動条件が設定されていません',
128
+ knowledge_adapter_missing: '知識基盤が接続されていません',
129
+ authority_missing: '実行権限が設定されていません',
50
130
  });
51
131
 
52
132
  const AUTHORITY_LABELS = Object.freeze({
@@ -101,6 +181,8 @@ const MANA_VIEWS = Object.freeze([
101
181
  { id: 'settings', label: 'Mana設定', description: 'runtime既定値と監査' },
102
182
  ]);
103
183
 
184
+ // The default connector catalog. A host may replace it with `connectors`
185
+ // (for example to add services or use its own brand assets).
104
186
  const CONNECTOR_DEFINITIONS = Object.freeze([
105
187
  { id: 'github', label: 'GitHub', description: 'コード・ドキュメントの参照', icon: '/icons/mana/brand-github.svg' },
106
188
  { id: 'drive', label: 'Google Drive', description: '資料・成果物の保存', icon: '/icons/mana/brand-google-drive.svg' },
@@ -117,6 +199,17 @@ const CONNECTOR_FOUNDATION_KINDS = Object.freeze({
117
199
  mana: 'mana',
118
200
  });
119
201
 
202
+ // Neutral wording for a host-managed connection catalog. A host replaces any
203
+ // of these through `connectionCatalog.copy`.
204
+ const CONNECTION_CATALOG_COPY = Object.freeze({
205
+ connectionsTitle: '1. 接続',
206
+ connectionsDescription: '認証済みの外部サービスです。プロジェクトごとに認証し直す必要はありません。',
207
+ resourcesDescription: '接続から選んだリポジトリ、フォルダ、チャンネルと、Brainbase内部基盤です。',
208
+ manageNote: '認証と接続の変更は接続の管理画面で行います。プロジェクトで使う資源は下の資源一覧で確認します。',
209
+ manageAction: '接続を管理',
210
+ noSelectableResources: '選択できる接続済み資源がありません。先に「接続状態」で接続とプロジェクト資源を確認してください。',
211
+ });
212
+
120
213
  function hasOwn(value, key) {
121
214
  return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key));
122
215
  }
@@ -201,6 +294,20 @@ export function normalizeConnector(value, fallbackId = '') {
201
294
  };
202
295
  }
203
296
 
297
+ function withConnectorDefinition(value, fallbackId = '', definitions = CONNECTOR_DEFINITIONS) {
298
+ const connector = normalizeConnector(value, fallbackId);
299
+ const definition = definitions.find((item) => item.id === connector.id);
300
+ if (!definition) return connector;
301
+ return {
302
+ ...connector,
303
+ label: connector.label || definition.label,
304
+ description: connector.description || definition.description,
305
+ // Brand assets are selected by Brainbase, rather than accepted from an
306
+ // API response. This keeps every connection surface visually consistent.
307
+ icon: definition.icon,
308
+ };
309
+ }
310
+
204
311
  export function normalizeApprovalTarget(value, fallbackContractVersion = null) {
205
312
  const source = objectValue(value);
206
313
  const effect = objectValue(source.effect);
@@ -329,8 +436,9 @@ function contractMatchesExpected(contract, { id, version = null, fields = null,
329
436
  const expected = normalizeContract(fields);
330
437
  const source = objectValue(fields);
331
438
  const comparisons = [
332
- ['project', 'project_code', 'projectCode'], ['outcome'], ['scope'],
439
+ ['project', 'project_code', 'projectCode'], ['profileId', 'profile_id'], ['outcome'], ['scope'],
333
440
  ['inputRefs', 'input_refs'], ['knowledgeRefs', 'knowledge_refs'], ['judgmentRefs', 'judgment_refs'],
441
+ ['allowedResources', 'allowed_resources'],
334
442
  ['artifactDestination', 'artifact_destination'], ['completionCriteria', 'completion_criteria'],
335
443
  ['owner'], ['limits'], ['trigger'],
336
444
  ];
@@ -383,6 +491,7 @@ export function normalizeContract(value) {
383
491
  return Array.isArray(refs) ? refs.map(normalizeReference) : [];
384
492
  };
385
493
  const criteria = source.completionCriteria ?? source.completion_criteria;
494
+ const allowedResources = source.allowedResources ?? source.allowed_resources;
386
495
  return {
387
496
  raw: value,
388
497
  schemaVersion: stringValue(firstPresent(source.schemaVersion, source.schema_version), OUTCOME_DELEGATION_CONTRACT_VERSION),
@@ -392,11 +501,13 @@ export function normalizeContract(value) {
392
501
  project: stringValue(firstPresent(source.project, source.project_code, source.projectCode), ''),
393
502
  status: normalizeStatus(source.status),
394
503
  nextAction: stringValue(firstPresent(source.nextAction, source.next_action), ''),
504
+ profileId: stringValue(firstPresent(source.profileId, source.profile_id), ''),
395
505
  outcome: stringValue(source.outcome, ''),
396
506
  scope: stringValue(source.scope, ''),
397
507
  inputRefs: references('inputRefs', 'input_refs'),
398
508
  knowledgeRefs: references('knowledgeRefs', 'knowledge_refs'),
399
509
  judgmentRefs: references('judgmentRefs', 'judgment_refs'),
510
+ allowedResources: Array.isArray(allowedResources) ? allowedResources : [],
400
511
  artifactDestination: {
401
512
  adapterId: stringValue(firstPresent(destination.adapterId, destination.adapter_id), ''),
402
513
  location: stringValue(destination.location, ''),
@@ -450,9 +561,11 @@ export function normalizeAuthority(value) {
450
561
 
451
562
  export function normalizeRun(value) {
452
563
  const raw = objectValue(value);
453
- const source = objectValue(raw.run ?? raw);
564
+ const source = objectValue(raw.run ?? raw.execution ?? raw.testRun ?? raw.test_run ?? raw);
454
565
  const completion = objectValue(source.completion ?? source.completion_result ?? source.completionResult);
455
- const artifacts = source.artifacts ?? source.artifact_refs ?? source.artifactRefs;
566
+ const artifact = objectValue(source.artifact);
567
+ const artifacts = source.artifacts ?? source.artifact_refs ?? source.artifactRefs
568
+ ?? (artifact.saveReceipt ? [artifact.saveReceipt] : []);
456
569
  const evidence = source.evidence ?? source.references ?? [];
457
570
  const stages = Array.isArray(source.stages) ? source.stages : [];
458
571
  const status = normalizeStatus(source.status ?? source.state);
@@ -465,19 +578,24 @@ export function normalizeRun(value) {
465
578
  const trigger = typeof triggerSource === 'string'
466
579
  ? triggerSource
467
580
  : firstPresent(objectValue(triggerSource).type, objectValue(triggerSource).triggerType, objectValue(triggerSource).trigger_type, source.triggerType, source.trigger_type, source.triggerReason, source.trigger_reason, source.reason) ?? null;
468
- const artifactReadback = source.artifactReadback ?? source.artifact_readback ?? source.readback;
581
+ const artifactReadback = source.artifactReadback ?? source.artifact_readback ?? source.readback ?? artifact.readback;
469
582
  const judgment = source.judgment ?? source.judgment_result ?? source.judgmentResult;
470
583
  const judgmentReason = firstPresent(
471
584
  source.judgmentReason, source.judgment_reason,
472
585
  objectValue(judgment).reason, source.reason_detail,
473
586
  ) ?? null;
587
+ const preflightSource = objectValue(source.preflight ?? raw.preflight);
588
+ const preflightChecks = Array.isArray(preflightSource.checks) ? preflightSource.checks : [];
474
589
  return {
475
590
  raw: value,
476
591
  id: stringValue(firstPresent(source.runId, source.run_id, source.id), ''),
477
592
  contractId: stringValue(firstPresent(source.contractId, source.contract_id), ''),
478
593
  contractVersion: integerValue(source.contractVersion ?? source.contract_version, null),
594
+ profileId: stringValue(firstPresent(source.profileId, source.profile_id), ''),
479
595
  mode: normalizeStatus(source.mode),
480
596
  status: RUN_STATUSES.has(status) ? status : status ?? null,
597
+ errorCode: stringValue(firstPresent(source.errorCode, source.error_code), ''),
598
+ errorMessage: stringValue(firstPresent(source.errorMessage, source.error_message), ''),
481
599
  safeTest: {
482
600
  configSnapshot: safeTestSource.configSnapshot ?? safeTestSource.config_snapshot ?? null,
483
601
  configHash: stringValue(firstPresent(safeTestSource.configHash, safeTestSource.config_hash), ''),
@@ -491,6 +609,19 @@ export function normalizeRun(value) {
491
609
  artifactReadback: artifactReadback === undefined ? null : artifactReadback,
492
610
  judgment: judgment === undefined ? null : judgment,
493
611
  judgmentReason,
612
+ preflight: {
613
+ available: typeof preflightSource.available === 'boolean' ? preflightSource.available : null,
614
+ checks: preflightChecks.map((value, index) => {
615
+ const check = objectValue(value);
616
+ return {
617
+ id: stringValue(firstPresent(check.id, check.checkId, check.check_id), `check-${index + 1}`),
618
+ status: normalizeStatus(check.status ?? check.state),
619
+ reason: stringValue(firstPresent(check.reason, check.reason_code), ''),
620
+ action: stringValue(firstPresent(check.action, check.required_action), ''),
621
+ resumeFrom: stringValue(firstPresent(check.resumeFrom, check.resume_from), ''),
622
+ };
623
+ }),
624
+ },
494
625
  stage: stringValue(firstPresent(source.stage, source.step, source.phase), ''),
495
626
  stages: stages.map((value) => {
496
627
  const stage = objectValue(value);
@@ -509,7 +640,7 @@ export function normalizeRun(value) {
509
640
  artifacts: Array.isArray(artifacts) ? artifacts : [],
510
641
  completion: {
511
642
  state: normalizeStatus(firstPresent(completion.state, completion.status, source.completion_state)),
512
- criteria: Array.isArray(completion.criteria) ? completion.criteria : [],
643
+ criteria: Array.isArray(completion.criteria) ? completion.criteria : Array.isArray(source.criteria) ? source.criteria : [],
513
644
  reason: stringValue(firstPresent(completion.reason, completion.error), ''),
514
645
  },
515
646
  pendingApproval: normalizeApprovalTarget(source.pendingApproval ?? source.pending_approval, integerValue(source.contractVersion ?? source.contract_version, null)),
@@ -518,6 +649,30 @@ export function normalizeRun(value) {
518
649
  };
519
650
  }
520
651
 
652
+ /**
653
+ * Readiness is only known from a preflight bound to this exact saved profile version.
654
+ * `profiles` is the list offered by the host (defaults to MANA_EXECUTION_PROFILES).
655
+ */
656
+ export function executionProfileReadiness(profileId, contractValue, runValues = [], profiles = MANA_EXECUTION_PROFILES) {
657
+ const profile = (Array.isArray(profiles) ? profiles : []).find((item) => item.id === profileId);
658
+ const contract = normalizeContract(contractValue);
659
+ if (!profile || !contract.contractId || !Number.isInteger(contract.version) || contract.profileId !== profileId) {
660
+ return { status: 'unknown', label: '事前確認: 未確認', checks: [] };
661
+ }
662
+ for (const value of runValues) {
663
+ if (!value) continue;
664
+ const run = normalizeRun(value);
665
+ if (run.contractId !== contract.contractId || run.contractVersion !== contract.version || run.profileId !== profileId) continue;
666
+ if (typeof run.preflight.available !== 'boolean') continue;
667
+ return {
668
+ status: run.preflight.available ? 'available' : 'unavailable',
669
+ label: run.preflight.available ? '事前確認: 実行可能' : '事前確認: 実行不可',
670
+ checks: run.preflight.checks,
671
+ };
672
+ }
673
+ return { status: 'unknown', label: '事前確認: 未確認', checks: [] };
674
+ }
675
+
521
676
  function safeTestStatus(value, contract) {
522
677
  const run = normalizeRun(value);
523
678
  const current = objectValue(contract);
@@ -584,12 +739,16 @@ function parseCriteriaLines(value) {
584
739
 
585
740
  function contractDraftFromForm(fields) {
586
741
  const criterionText = fields.completionCriteria.value;
742
+ // The profile and resource pickers exist only when the host offers
743
+ // execution profiles. The free-text form keeps its original request body.
587
744
  const body = {
745
+ ...(fields.profileId ? { profileId: fields.profileId.value } : {}),
588
746
  outcome: fields.outcome.value,
589
747
  scope: fields.scope.value,
590
748
  inputRefs: parseReferenceLines(fields.inputRefs.value),
591
749
  knowledgeRefs: parseReferenceLines(fields.knowledgeRefs.value),
592
750
  judgmentRefs: parseReferenceLines(fields.judgmentRefs.value),
751
+ ...(fields.allowedResources ? { allowedResources: stringValue(fields.allowedResources.value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean) } : {}),
593
752
  artifactDestination: {
594
753
  adapterId: fields.adapterId.value,
595
754
  location: fields.location.value,
@@ -699,7 +858,7 @@ function stateMessage(document, status, options = {}) {
699
858
  const item = createElement(document, 'div', { className: `outcome-mana-state outcome-mana-error outcome-mana-error-${state}`, attrs: { role: 'alert' } });
700
859
  item.append(createElement(document, 'strong', { text: state === 'unknown' ? '状態を確認できません' : state === 'conflict' ? '別の更新を確認しました' : '読み込みを確認できません' }));
701
860
  if (options.message) item.append(createElement(document, 'p', { text: options.message }));
702
- if (options.retry) item.append(button(document, '再試行', options.retry, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
861
+ if (options.retry) item.append(button(document, options.retryLabel ?? '再試行', options.retry, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
703
862
  return item;
704
863
  }
705
864
  return null;
@@ -753,6 +912,21 @@ function evidenceList(document, title, values) {
753
912
  return section;
754
913
  }
755
914
 
915
+ function criterionSummaries(criteria) {
916
+ return (Array.isArray(criteria) ? criteria : []).map((value) => {
917
+ const criterion = objectValue(value);
918
+ return [stringValue(criterion.id, UNKNOWN), statusLabel(criterion.status), stringValue(criterion.reason, '')]
919
+ .filter(Boolean).join(' / ');
920
+ });
921
+ }
922
+
923
+ function artifactReadbackSummary(value) {
924
+ const readback = objectValue(value);
925
+ if (!Object.keys(readback).length) return UNKNOWN;
926
+ return readback.verified === true ? '検証済み'
927
+ : readback.verified === false ? '検証失敗' : '検証状態は未確認';
928
+ }
929
+
756
930
  function stageList(document, stages) {
757
931
  const section = createElement(document, 'div', { className: 'outcome-mana-evidence' });
758
932
  section.append(createElement(document, 'h4', { text: '処理段階' }));
@@ -827,11 +1001,13 @@ function canManage(session) {
827
1001
  function contractFormValues(contract, draft) {
828
1002
  const source = draft ?? contract ?? normalizeContract({});
829
1003
  return {
1004
+ profileId: source.profileId ?? '',
830
1005
  outcome: source.outcome ?? '',
831
1006
  scope: source.scope ?? '',
832
1007
  inputRefs: (source.inputRefs ?? []).map((item) => `${item.id}${item.version ? `@${item.version}` : ''}`).join('\n'),
833
1008
  knowledgeRefs: (source.knowledgeRefs ?? []).map((item) => `${item.id}${item.version ? `@${item.version}` : ''}`).join('\n'),
834
1009
  judgmentRefs: (source.judgmentRefs ?? []).map((item) => `${item.id}${item.version ? `@${item.version}` : ''}`).join('\n'),
1010
+ allowedResources: (source.allowedResources ?? []).map((item) => typeof item === 'string' ? item : firstPresent(item.id, item.resourceId, item.resource_id)).filter(Boolean).join('\n'),
835
1011
  adapterId: source.artifactDestination?.adapterId ?? '',
836
1012
  location: source.artifactDestination?.location ?? '',
837
1013
  environment: source.artifactDestination?.environment ?? 'isolated',
@@ -845,11 +1021,13 @@ function contractFormValues(contract, draft) {
845
1021
 
846
1022
  function contractInput(contract, overrides = {}) {
847
1023
  return {
1024
+ profileId: contract.profileId ?? '',
848
1025
  outcome: contract.outcome,
849
1026
  scope: contract.scope,
850
1027
  inputRefs: contract.inputRefs,
851
1028
  knowledgeRefs: contract.knowledgeRefs,
852
1029
  judgmentRefs: contract.judgmentRefs,
1030
+ allowedResources: contract.allowedResources ?? [],
853
1031
  artifactDestination: contract.artifactDestination,
854
1032
  completionCriteria: contract.completionCriteria,
855
1033
  owner: contract.owner,
@@ -867,6 +1045,74 @@ function triggerFromContract(contract, draft) {
867
1045
  return normalizeTrigger(draft?.trigger ?? contract?.trigger);
868
1046
  }
869
1047
 
1048
+ /**
1049
+ * @typedef {object} ManaConnectorDefinition
1050
+ * @property {string} id Stable connector id used by contracts and profiles.
1051
+ * @property {string} label Display name.
1052
+ * @property {string} description One-line purpose.
1053
+ * @property {string} icon Icon URL. The UI always takes the icon from here,
1054
+ * never from an API response.
1055
+ */
1056
+
1057
+ /**
1058
+ * @typedef {object} ManaConnectionStatus
1059
+ * @property {string} id Connector id.
1060
+ * @property {string} [status] e.g. `connected`, `unconfirmed`, `failed`,
1061
+ * `permission_denied`, `authorization_required`. Missing means `unconfirmed`.
1062
+ * @property {string|null} [account] Account that authenticated the connection.
1063
+ * @property {string|null} [checkedAt] Time the host last verified it.
1064
+ * @property {string|null} [reason] Reason code shown when not connected.
1065
+ */
1066
+
1067
+ /**
1068
+ * @typedef {object} ManaConnectionCatalog
1069
+ * @property {string[]} ids Connector ids whose authentication the host
1070
+ * manages. They are listed in `connectors` order. Project resources and
1071
+ * Brainbase/Mana foundations stay in the separate project-resource list.
1072
+ * @property {(context: { project: object, session: object }) => Promise<ManaConnectionStatus[]|{ connections: ManaConnectionStatus[] }>} load
1073
+ * Reads the host's current connection status. A rejection keeps every
1074
+ * connection unconfirmed with the error code as its reason.
1075
+ * @property {Record<string, string>} [labels] Display name per connector id
1076
+ * in the connection list (e.g. the installed app name).
1077
+ * @property {(connectorId: string) => string} [sourceLabel] Where the
1078
+ * project resources of a connector come from. Defaults to the connector label.
1079
+ * @property {Record<string, string>} [reasonLabels] Labels for host-specific
1080
+ * reason codes. They take precedence over the built-in labels.
1081
+ * @property {Partial<typeof CONNECTION_CATALOG_COPY>} [copy] Replaces the
1082
+ * neutral wording of the connection view and the resource picker.
1083
+ */
1084
+
1085
+ /**
1086
+ * Creates the Mana outcome-delegation workspace.
1087
+ *
1088
+ * Besides `document`, `root`, `api`, `apiMutation`, `project`, `session`,
1089
+ * `paths`, `pathFor`, `basePath`, `confirm`, `onChange`, and `autoLoad`, the
1090
+ * host may pass these options. Without them the workspace behaves as the
1091
+ * single-owner default: the built-in connector list, foundation checks
1092
+ * through `paths.foundationCheck`, and a free-text contract form.
1093
+ *
1094
+ * @param {object} options
1095
+ * @param {ManaConnectorDefinition[]} [options.connectors] Replaces the
1096
+ * default connector list (GitHub, Google Drive, Slack, Brainbase Graph,
1097
+ * Mana runtime) and its order.
1098
+ * @param {ManaConnectionCatalog} [options.connectionCatalog] Reads connection
1099
+ * status from the host instead of foundation checks, and shows connections,
1100
+ * project resources, and the delegation's allowed range as three sections.
1101
+ * @param {(connectorId: string) => void} [options.onManageConnection] Opens
1102
+ * the host's connection management for the selected connector. The button
1103
+ * appears only with `connectionCatalog`.
1104
+ * @param {Array<typeof MANA_EXECUTION_PROFILES[number]>} [options.executionProfiles]
1105
+ * Execution profiles offered by the contract form. When non-empty, the form
1106
+ * asks for a profile and picks input references, allowed resources, and the
1107
+ * artifact destination from loaded connection resources, and it loads
1108
+ * connections before a delegation is edited. Pass MANA_EXECUTION_PROFILES
1109
+ * for the Mana runtime profiles.
1110
+ * @param {Storage|null} [options.sessionStorage] Keeps the accepted safe-test
1111
+ * run ID per actor, project, and contract. Defaults to the browser's
1112
+ * sessionStorage; `null` disables it.
1113
+ * @param {typeof setTimeout} [options.setTimeout] Scheduler for safe-test readback.
1114
+ * @param {typeof clearTimeout} [options.clearTimeout] Cancels a scheduled readback.
1115
+ */
870
1116
  export function createManaOutcomeUI(options = {}) {
871
1117
  const document = options.document ?? globalThis.document;
872
1118
  if (!document) throw new Error('outcome_mana_document_unavailable');
@@ -877,6 +1123,16 @@ export function createManaOutcomeUI(options = {}) {
877
1123
  const pathBuilder = typeof options.pathFor === 'function' ? options.pathFor : null;
878
1124
  const projectInput = options.project;
879
1125
  const sessionInput = options.session;
1126
+ const connectorDefinitions = Array.isArray(options.connectors) && options.connectors.length
1127
+ ? options.connectors
1128
+ : CONNECTOR_DEFINITIONS;
1129
+ const connectionCatalog = options.connectionCatalog && typeof options.connectionCatalog.load === 'function'
1130
+ ? options.connectionCatalog
1131
+ : null;
1132
+ const catalogIds = Array.isArray(connectionCatalog?.ids) ? connectionCatalog.ids : [];
1133
+ const catalogCopy = { ...CONNECTION_CATALOG_COPY, ...objectValue(connectionCatalog?.copy) };
1134
+ const executionProfiles = Array.isArray(options.executionProfiles) ? options.executionProfiles : [];
1135
+ const profilePicker = executionProfiles.length > 0;
880
1136
  const state = {
881
1137
  // null means no user choice has been made yet. render() derives a useful
882
1138
  // first step from the state so existing deep links/readbacks land on the
@@ -887,13 +1143,56 @@ export function createManaOutcomeUI(options = {}) {
887
1143
  contract: { status: 'loading', records: [], selectedId: null, detail: null, error: null, formDraft: null, pendingSave: null },
888
1144
  authority: { status: 'idle', matrix: [], raw: null, error: null, draft: null },
889
1145
  triggers: { status: 'idle', trigger: null, error: null, draft: null, catalogStatus: 'idle', schedules: [], subscriptions: [], catalogError: null },
890
- safeTest: { status: 'idle', result: null, error: null, formDraft: null },
1146
+ safeTest: { status: 'idle', result: null, runId: null, contractId: null, error: null, formDraft: null },
891
1147
  activation: { status: 'idle', error: null, message: '' },
892
1148
  runs: { status: 'idle', records: [], selectedId: null, detail: null, error: null },
893
1149
  runAction: { status: 'idle', error: null, draft: null },
894
1150
  connections: { status: 'unknown', records: [], selectedId: null, detail: null, error: null },
895
1151
  manaSettings: { status: 'unknown', data: null, error: null, draft: null, saveStatus: 'idle', saveError: null },
896
1152
  };
1153
+ let contractDetailRequestGeneration = 0;
1154
+ let authorityRequestGeneration = 0;
1155
+ let safeTestTimer = null;
1156
+ let safeTestReadCount = 0;
1157
+ const safeTestReadLimit = 450;
1158
+ const scheduleTimeout = options.setTimeout ?? globalThis.setTimeout;
1159
+ const cancelTimeout = options.clearTimeout ?? globalThis.clearTimeout;
1160
+ let tabStorage = options.sessionStorage;
1161
+ if (tabStorage === undefined) {
1162
+ try { tabStorage = globalThis.sessionStorage; } catch { /* storage is optional */ }
1163
+ }
1164
+
1165
+ function safeTestStorageKey(id) {
1166
+ const projectCode = currentProject().code;
1167
+ const actorId = currentSession().actorId;
1168
+ return projectCode && actorId && id ? `mana-safe-test-run:${actorId}:${projectCode}:${id}` : null;
1169
+ }
1170
+
1171
+ function rememberSafeTestRun(id, runId) {
1172
+ const key = safeTestStorageKey(id);
1173
+ if (!key || !runId) return;
1174
+ try { tabStorage?.setItem(key, runId); } catch { /* storage is optional */ }
1175
+ }
1176
+
1177
+ function stopSafeTestReadback() {
1178
+ if (safeTestTimer !== null) cancelTimeout(safeTestTimer);
1179
+ safeTestTimer = null;
1180
+ }
1181
+
1182
+ function scheduleSafeTestReadback(runId) {
1183
+ stopSafeTestReadback();
1184
+ if (safeTestReadCount >= safeTestReadLimit) {
1185
+ state.safeTest.status = 'unknown';
1186
+ return;
1187
+ }
1188
+ safeTestTimer = scheduleTimeout(async () => {
1189
+ safeTestTimer = null;
1190
+ if (state.safeTest.runId !== runId) return;
1191
+ safeTestReadCount += 1;
1192
+ await refreshSafeTestReadback();
1193
+ }, 2000);
1194
+ safeTestTimer?.unref?.();
1195
+ }
897
1196
 
898
1197
  const viewIds = new Set([...MANA_VIEWS.map((view) => view.id), 'delegation_settings']);
899
1198
 
@@ -956,9 +1255,12 @@ export function createManaOutcomeUI(options = {}) {
956
1255
  function currentProject() { return projectValue(projectInput); }
957
1256
  function currentSession() { return sessionValue(sessionInput); }
958
1257
  function currentContract() {
959
- return state.contract.detail ?? state.contract.records.find((record) => record.contractId === state.contract.selectedId) ?? null;
1258
+ const selectedId = state.contract.selectedId;
1259
+ if (selectedId && state.contract.detail?.contractId === selectedId) return state.contract.detail;
1260
+ if (selectedId) return state.contract.records.find((record) => record.contractId === selectedId) ?? null;
1261
+ return state.contract.detail ?? null;
960
1262
  }
961
- function contractId() { return currentContract()?.contractId || null; }
1263
+ function contractId() { return state.contract.selectedId || currentContract()?.contractId || null; }
962
1264
  function currentPaths() {
963
1265
  const generated = defaultPathMap(options.basePath ?? '/api/outcome-delegations', currentProject().code);
964
1266
  return { ...generated, ...paths };
@@ -972,6 +1274,10 @@ export function createManaOutcomeUI(options = {}) {
972
1274
  function connectorSource(project, id) {
973
1275
  const foundations = objectValue(project.foundation_values);
974
1276
  const resources = objectValue(project.resources);
1277
+ // Drive has a primary project folder and an optional artifact destination.
1278
+ // The resource collection is authoritative for contract choices, while
1279
+ // foundation_values.drive remains the single primary-folder edit value.
1280
+ if (id === 'drive' && hasOwn(resources, 'drive')) return resources.drive;
975
1281
  const aliases = {
976
1282
  brainbase: ['brainbase', 'knowledge'],
977
1283
  drive: ['drive', 'google_drive', 'googleDrive'],
@@ -987,15 +1293,64 @@ export function createManaOutcomeUI(options = {}) {
987
1293
  return null;
988
1294
  }
989
1295
 
1296
+ function projectResourceId(connectorId, value) {
1297
+ const source = objectValue(value);
1298
+ const explicit = firstPresent(source.id, source.resourceId, source.resource_id);
1299
+ if (explicit) return stringValue(explicit).trim();
1300
+ const identifier = firstPresent(
1301
+ source.identifier,
1302
+ source.folder_id,
1303
+ source.folderId,
1304
+ source.channel_id,
1305
+ source.channelId,
1306
+ typeof value === 'string' || typeof value === 'number' ? value : null,
1307
+ );
1308
+ if (!identifier) return '';
1309
+ const identifierText = stringValue(identifier).trim();
1310
+ if (identifierText.startsWith(`${connectorId}:`)) return identifierText;
1311
+ const prefixes = {
1312
+ github: 'github:repo:',
1313
+ drive: 'drive:folder:',
1314
+ slack: 'slack:channel:',
1315
+ brainbase: 'brainbase:knowledge:',
1316
+ mana: 'mana:runtime:',
1317
+ };
1318
+ return `${prefixes[connectorId] ?? `${connectorId}:resource:`}${identifierText}`;
1319
+ }
1320
+
1321
+ function projectResources(connectorId, source) {
1322
+ if (source === null || source === undefined) return [];
1323
+ const record = objectValue(source);
1324
+ const values = Array.isArray(source)
1325
+ ? source
1326
+ : Array.isArray(record.items) ? record.items : [source];
1327
+ return values.map((value) => {
1328
+ const resource = objectValue(value);
1329
+ const id = projectResourceId(connectorId, value);
1330
+ if (!id) return null;
1331
+ return {
1332
+ ...resource,
1333
+ id,
1334
+ label: firstPresent(
1335
+ resource.label,
1336
+ resource.name,
1337
+ resource.title,
1338
+ resource.identifier,
1339
+ typeof value === 'string' || typeof value === 'number' ? value : null,
1340
+ id,
1341
+ ),
1342
+ };
1343
+ }).filter(Boolean);
1344
+ }
1345
+
990
1346
  function connectorRecordsFromProject() {
991
1347
  const project = currentProject();
992
- return CONNECTOR_DEFINITIONS.map((definition) => {
1348
+ return connectorDefinitions.map((definition) => {
993
1349
  const source = connectorSource(project, definition.id);
994
- // GitHub foundations can be an array of repositories. Keep every item
995
- // as a permitted resource while leaving the connection state unknown.
996
- const value = Array.isArray(source)
997
- ? { resources: source }
998
- : (source !== null && source !== undefined && typeof source !== 'object' ? { account: source } : source);
1350
+ // Project registration and connection authentication are distinct.
1351
+ // Normalize the former into stable contract resource IDs without
1352
+ // inventing a connector status or account from the project payload.
1353
+ const value = { resources: projectResources(definition.id, source) };
999
1354
  const normalized = normalizeConnector(value, definition.id);
1000
1355
  return {
1001
1356
  ...normalized,
@@ -1009,16 +1364,26 @@ export function createManaOutcomeUI(options = {}) {
1009
1364
  function connectionStatusLabel(status) {
1010
1365
  const normalized = normalizeStatus(status);
1011
1366
  if (['connected', 'verified', 'readback_verified'].includes(normalized)) return '接続確認済み';
1012
- if (['active', 'enabled', 'ready', 'available'].includes(normalized)) return '利用可能';
1367
+ if (['active', 'enabled', 'ready', 'available'].includes(normalized)) return '登録済み(接続未確認)';
1013
1368
  if (['failed', 'error', 'unavailable'].includes(normalized)) return '確認失敗';
1014
1369
  if (['unregistered', 'not_registered'].includes(normalized)) return '未登録';
1015
1370
  if (['unconfigured', 'misconfigured'].includes(normalized)) return '確認設定なし';
1016
1371
  if (['permission_denied', 'forbidden', 'unauthorized'].includes(normalized)) return '権限不足';
1372
+ if (['authorization_required', 'required_scope_missing'].includes(normalized)) return '追加権限が必要';
1017
1373
  if (['timeout', 'timed_out'].includes(normalized)) return 'タイムアウト';
1018
1374
  if (['rejected', 'unconfirmed', 'readback_mismatch'].includes(normalized)) return '確認できません';
1019
1375
  return UNKNOWN;
1020
1376
  }
1021
1377
 
1378
+ function projectResourceStatusLabel(connector) {
1379
+ return connector.resources?.length || connector.account ? '登録済み' : '未登録';
1380
+ }
1381
+
1382
+ function connectionSourceLabel(id) {
1383
+ const hosted = typeof connectionCatalog?.sourceLabel === 'function' ? connectionCatalog.sourceLabel(id) : null;
1384
+ return firstPresent(hosted, connectorDefinitions.find((item) => item.id === id)?.label, id);
1385
+ }
1386
+
1022
1387
  function connectionReasonLabel(reason) {
1023
1388
  const value = stringValue(reason).trim();
1024
1389
  const normalized = normalizeStatus(value);
@@ -1040,7 +1405,12 @@ export function createManaOutcomeUI(options = {}) {
1040
1405
  provider_rejected: '接続先が確認要求を拒否しました',
1041
1406
  provider_readback_mismatch: '登録内容と確認結果が一致しません',
1042
1407
  local_preview_only: 'ローカルプレビューでは実接続を確認できません',
1408
+ not_implemented: 'このアプリの接続は準備中です',
1043
1409
  };
1410
+ const hosted = objectValue(connectionCatalog?.reasonLabels);
1411
+ if (hasOwn(hosted, value)) return stringValue(hosted[value]);
1412
+ if (normalized && hasOwn(hosted, normalized)) return stringValue(hosted[normalized]);
1413
+ if (/^upstream_http_\d+$/u.test(normalized)) return `接続状態APIが応答しました(HTTP ${normalized.slice(-3)})`;
1044
1414
  return labels[normalized] ?? (value || UNKNOWN);
1045
1415
  }
1046
1416
 
@@ -1061,7 +1431,7 @@ export function createManaOutcomeUI(options = {}) {
1061
1431
  function normalizeConnectorPayload(payload) {
1062
1432
  const records = collectionFrom(payload, ['connectors', 'connections', 'items', 'records']);
1063
1433
  if (!records) return { state: 'unknown', records: [], raw: payload };
1064
- return { state: records.length ? 'ready' : 'empty', records: records.map((item) => normalizeConnector(item)), raw: payload };
1434
+ return { state: records.length ? 'ready' : 'empty', records: records.map((item) => withConnectorDefinition(item, '', connectorDefinitions)), raw: payload };
1065
1435
  }
1066
1436
 
1067
1437
  function connectorFromFoundationCheck(definition, payload, error, attemptedAt) {
@@ -1084,10 +1454,10 @@ export function createManaOutcomeUI(options = {}) {
1084
1454
  }, definition.id);
1085
1455
  }
1086
1456
 
1087
- async function checkConnections() {
1457
+ async function checkFoundationConnections() {
1088
1458
  const attemptedAt = new Date().toISOString();
1089
- return Promise.all(CONNECTOR_DEFINITIONS.map(async (definition) => {
1090
- const kind = CONNECTOR_FOUNDATION_KINDS[definition.id];
1459
+ return Promise.all(connectorDefinitions.map(async (definition) => {
1460
+ const kind = CONNECTOR_FOUNDATION_KINDS[definition.id] ?? definition.id;
1091
1461
  const target = path('foundationCheck', { kind });
1092
1462
  if (!target || typeof apiMutation !== 'function') return connectorFromFoundationCheck(definition, {}, null, null);
1093
1463
  try {
@@ -1103,13 +1473,60 @@ export function createManaOutcomeUI(options = {}) {
1103
1473
  }));
1104
1474
  }
1105
1475
 
1476
+ function catalogDefinitions() {
1477
+ return connectorDefinitions.filter((item) => catalogIds.includes(item.id));
1478
+ }
1479
+
1480
+ function catalogLabel(definition) {
1481
+ return firstPresent(objectValue(connectionCatalog?.labels)[definition.id], definition.label);
1482
+ }
1483
+
1484
+ // Host-managed authentication and project registration stay separate:
1485
+ // the host reports only connection status, and the resources come from the
1486
+ // project payload.
1487
+ async function checkCatalogConnections() {
1488
+ const payload = await connectionCatalog.load({ project: currentProject(), session: currentSession() });
1489
+ const loaded = collectionFrom(payload, ['connections', 'connectors', 'items', 'records']) ?? [];
1490
+ const byId = new Map(loaded.map((item) => normalizeConnector(item)).map((item) => [item.id, item]));
1491
+ const projectRecords = new Map(connectorRecordsFromProject().map((item) => [item.id, item]));
1492
+ return catalogDefinitions().map((definition) => {
1493
+ const hosted = byId.get(definition.id);
1494
+ return withConnectorDefinition({
1495
+ id: definition.id,
1496
+ label: catalogLabel(definition),
1497
+ description: definition.description,
1498
+ status: hosted?.status ?? 'unconfirmed',
1499
+ account: hosted?.account ?? null,
1500
+ resources: projectRecords.get(definition.id)?.resources ?? [],
1501
+ checkedAt: hosted?.checkedAt ?? null,
1502
+ reason: hosted?.reason ?? null,
1503
+ }, definition.id, connectorDefinitions);
1504
+ });
1505
+ }
1506
+
1507
+ function catalogPlaceholders(error = null) {
1508
+ const normalizedError = error ? normalizeError(error) : null;
1509
+ return catalogDefinitions().map((definition) => withConnectorDefinition({
1510
+ id: definition.id,
1511
+ label: catalogLabel(definition),
1512
+ description: definition.description,
1513
+ status: error ? (normalizedError.status === 403 ? 'permission_denied' : 'failed') : 'unconfirmed',
1514
+ account: null,
1515
+ checkedAt: null,
1516
+ reason: normalizedError?.code ?? null,
1517
+ }, definition.id, connectorDefinitions));
1518
+ }
1519
+
1106
1520
  async function loadConnections() {
1107
1521
  const checkTarget = path('foundationCheck', { kind: 'github' });
1108
1522
  const target = path('connectors') ?? path('connections');
1109
1523
  state.connections.status = 'loading'; state.connections.error = null; render();
1110
1524
  try {
1111
- if (checkTarget && typeof apiMutation === 'function') {
1112
- const records = await checkConnections();
1525
+ if (connectionCatalog) {
1526
+ const records = await checkCatalogConnections();
1527
+ state.connections = { ...state.connections, status: 'ready', records, error: null };
1528
+ } else if (checkTarget && typeof apiMutation === 'function') {
1529
+ const records = await checkFoundationConnections();
1113
1530
  state.connections = { ...state.connections, status: 'ready', records, error: null };
1114
1531
  } else if (!target) {
1115
1532
  const records = projectConnectors();
@@ -1120,10 +1537,30 @@ export function createManaOutcomeUI(options = {}) {
1120
1537
  }
1121
1538
  if (!state.connections.selectedId && state.connections.records.length) state.connections.selectedId = state.connections.records[0].id;
1122
1539
  state.connections.detail = state.connections.records.find((item) => item.id === state.connections.selectedId) ?? null;
1123
- } catch (error) { setError(state.connections, error); }
1540
+ } catch (error) {
1541
+ if (connectionCatalog) {
1542
+ // Keep every host connection visible as unconfirmed rather than
1543
+ // hiding the list or inventing a verification time.
1544
+ state.connections = {
1545
+ ...state.connections,
1546
+ status: 'error',
1547
+ records: catalogPlaceholders(error),
1548
+ error: normalizeError(error),
1549
+ };
1550
+ if (!state.connections.selectedId) state.connections.selectedId = state.connections.records[0]?.id ?? null;
1551
+ state.connections.detail = state.connections.records.find((item) => item.id === state.connections.selectedId) ?? null;
1552
+ } else setError(state.connections, error);
1553
+ }
1124
1554
  render(); notifyChange(); return state.connections;
1125
1555
  }
1126
1556
 
1557
+ // The profile pickers choose from loaded connection resources, so load
1558
+ // them once before a delegation is edited. The free-text form needs none.
1559
+ async function ensureConnectionsForDelegation() {
1560
+ if (!profilePicker || state.connections.status !== 'unknown') return state.connections;
1561
+ return loadConnections();
1562
+ }
1563
+
1127
1564
  async function loadManaSettings() {
1128
1565
  const target = path('settings') ?? path('manaSettings');
1129
1566
  const mana = objectValue(currentProject().mana);
@@ -1270,21 +1707,47 @@ export function createManaOutcomeUI(options = {}) {
1270
1707
 
1271
1708
  async function loadContractDetail(id = contractId(), { renderAfter = true } = {}) {
1272
1709
  if (!id) return null;
1710
+ const requestedId = id;
1711
+ const requestGeneration = ++contractDetailRequestGeneration;
1712
+ // A contract refresh changes the version-bound authority context even when
1713
+ // the user returns to the same contract before an older request settles.
1714
+ authorityRequestGeneration += 1;
1715
+ // Authority is version-bound. A detail refresh may return a newer version
1716
+ // for the same contract id, so never carry the previous version's matrix
1717
+ // across that boundary. An embedded authority payload below can replace
1718
+ // this idle state; otherwise the authority stage reloads it explicitly.
1719
+ state.authority = { status: 'idle', matrix: [], error: null, draft: null };
1720
+ state.contract.selectedId = requestedId;
1273
1721
  state.contract.status = 'loading'; state.contract.error = null;
1274
1722
  if (renderAfter) render();
1275
1723
  try {
1276
- const payload = await callApi(path('contractDetail', { contractId: id }));
1724
+ const payload = await callApi(path('contractDetail', { contractId: requestedId }));
1725
+ if (state.contract.selectedId !== requestedId || contractDetailRequestGeneration !== requestGeneration) return null;
1277
1726
  const contract = normalizeContract(payload);
1278
- state.contract.detail = contract.contractId ? contract : { ...contract, contractId: id };
1279
- state.contract.selectedId = id;
1727
+ state.contract.detail = contract.contractId ? contract : { ...contract, contractId: requestedId };
1280
1728
  state.contract.status = 'ready';
1729
+ if (state.safeTest.contractId && state.safeTest.contractId !== requestedId) {
1730
+ stopSafeTestReadback();
1731
+ state.safeTest = { status: 'idle', result: null, runId: null, contractId: null, error: null, formDraft: null };
1732
+ }
1733
+ if (!state.safeTest.runId) {
1734
+ let storedRunId = null;
1735
+ const key = safeTestStorageKey(requestedId);
1736
+ try { storedRunId = key ? tabStorage?.getItem(key) : null; } catch { /* storage is optional */ }
1737
+ if (storedRunId) {
1738
+ state.safeTest.runId = storedRunId;
1739
+ state.safeTest.contractId = requestedId;
1740
+ state.safeTest.result = { runId: storedRunId, status: 'queued' };
1741
+ await refreshSafeTestReadback();
1742
+ }
1743
+ }
1281
1744
  const authorityPayload = objectValue(payload).authority ?? objectValue(payload).authorityMatrix;
1282
1745
  if (authorityPayload !== undefined) state.authority = { ...normalizeAuthority(authorityPayload), error: null };
1283
1746
  const triggerPayload = objectValue(payload).trigger ?? objectValue(payload).triggers;
1284
1747
  if (triggerPayload !== undefined) state.triggers = { ...state.triggers, status: 'ready', trigger: normalizeTrigger(triggerPayload), error: null, draft: null };
1285
1748
  await loadTriggerCatalog(state.contract.detail.trigger?.type ?? 'manual', { renderAfter: false, enterDetail: false });
1286
1749
  } catch (error) {
1287
- setError(state.contract, error);
1750
+ if (state.contract.selectedId === requestedId && contractDetailRequestGeneration === requestGeneration) setError(state.contract, error);
1288
1751
  }
1289
1752
  if (renderAfter) { render(); notifyChange(); }
1290
1753
  return state.contract.detail;
@@ -1439,11 +1902,21 @@ export function createManaOutcomeUI(options = {}) {
1439
1902
  async function loadAuthority(id = contractId()) {
1440
1903
  setActiveStage('authority');
1441
1904
  if (!id) { state.authority.status = 'unknown'; render(); return state.authority; }
1905
+ const requestedId = id;
1906
+ if (state.contract.status === 'loading' || state.contract.selectedId !== requestedId || currentContract()?.contractId !== requestedId) {
1907
+ render();
1908
+ return state.authority;
1909
+ }
1910
+ const requestedVersion = currentContract()?.version;
1911
+ const requestGeneration = ++authorityRequestGeneration;
1442
1912
  state.authority.status = 'loading'; state.authority.error = null; render();
1443
1913
  try {
1444
- const payload = await callApi(path('authority', { contractId: id, contractVersion: currentContract()?.version }));
1914
+ const payload = await callApi(path('authority', { contractId: requestedId, contractVersion: requestedVersion }));
1915
+ if (state.contract.selectedId !== requestedId || authorityRequestGeneration !== requestGeneration || currentContract()?.version !== requestedVersion) return state.authority;
1445
1916
  state.authority = { ...state.authority, ...normalizeAuthority(payload), error: null };
1446
- } catch (error) { setError(state.authority, error); }
1917
+ } catch (error) {
1918
+ if (state.contract.selectedId === requestedId && authorityRequestGeneration === requestGeneration && currentContract()?.version === requestedVersion) setError(state.authority, error);
1919
+ }
1447
1920
  render(); notifyChange(); return state.authority;
1448
1921
  }
1449
1922
 
@@ -1509,33 +1982,43 @@ export function createManaOutcomeUI(options = {}) {
1509
1982
  external_send: false,
1510
1983
  production_writes: false,
1511
1984
  };
1985
+ stopSafeTestReadback();
1986
+ safeTestReadCount = 0;
1987
+ state.safeTest.runId = null;
1988
+ state.safeTest.contractId = id;
1512
1989
  state.safeTest.formDraft = body; state.safeTest.status = 'loading'; state.safeTest.error = null; render();
1513
1990
  try {
1514
1991
  const payload = await callMutation(path('tests', { contractId: id }), { body, key: idempotencyKey('mana-safe-test') });
1515
1992
  const accepted = normalizeRun(payload);
1516
- let readback = null;
1517
- if (accepted.id) {
1518
- try { readback = normalizeRun(await callApi(path('runDetail', { runId: accepted.id }))); } catch { /* accepted is not completion */ }
1519
- }
1520
- const result = readback?.id ? readback : payload;
1521
- state.safeTest.result = result;
1993
+ const acceptedId = accepted.id;
1994
+ state.safeTest.runId = acceptedId || null;
1995
+ state.safeTest.result = payload;
1996
+ rememberSafeTestRun(id, acceptedId);
1522
1997
  state.safeTest.formDraft = null;
1523
- state.safeTest.status = safeTestStatus(result, currentContract());
1998
+ state.safeTest.status = safeTestStatus(payload, currentContract());
1999
+ if (acceptedId) await refreshSafeTestReadback();
1524
2000
  } catch (error) { setError(state.safeTest, error); }
1525
2001
  render(); notifyChange(); return state.safeTest;
1526
2002
  }
1527
2003
 
1528
2004
  async function refreshSafeTestReadback() {
1529
- const runId = normalizeRun(state.safeTest.result).id;
2005
+ const runId = state.safeTest.runId ?? normalizeRun(state.safeTest.result).id;
1530
2006
  if (!runId) return state.safeTest;
2007
+ stopSafeTestReadback();
1531
2008
  try {
1532
2009
  const result = normalizeRun(await callApi(path('runDetail', { runId })));
2010
+ if (state.safeTest.runId !== runId) return state.safeTest;
2011
+ if (result.id !== runId || (result.contractId && result.contractId !== contractId())) throw new Error('safe_test_readback_identity_mismatch');
1533
2012
  state.safeTest.result = result;
1534
2013
  state.safeTest.status = safeTestStatus(result, currentContract());
1535
2014
  state.safeTest.error = null;
1536
- } catch {
2015
+ if (result.status === 'queued' || result.status === 'running') scheduleSafeTestReadback(runId);
2016
+ } catch (error) {
2017
+ if (state.safeTest.runId !== runId) return state.safeTest;
2018
+ state.safeTest.error = normalizeError(error);
1537
2019
  state.safeTest.status = 'unknown';
1538
2020
  }
2021
+ render(); notifyChange();
1539
2022
  return state.safeTest;
1540
2023
  }
1541
2024
 
@@ -1588,12 +2071,25 @@ export function createManaOutcomeUI(options = {}) {
1588
2071
  state.runs.status = 'loading'; state.runs.error = null; render();
1589
2072
  try {
1590
2073
  const normalized = normalizeRuns(await callApi(path('runList', { contractId: id })));
2074
+ // The runtime defines run-list recency by creation time. A long-running old
2075
+ // run may be updated after a newer run and must not become "latest" again.
2076
+ normalized.records.sort((left, right) => new Date(right.startedAt ?? 0).getTime() - new Date(left.startedAt ?? 0).getTime());
1591
2077
  state.runs = { ...state.runs, ...normalized, status: normalized.state, error: null };
1592
2078
  const latestSafeTest = normalized.records.find((run) => run.mode === 'safe_test' && run.contractId === id);
1593
2079
  if (latestSafeTest) {
2080
+ stopSafeTestReadback();
2081
+ state.safeTest.runId = latestSafeTest.id;
2082
+ state.safeTest.contractId = id;
2083
+ rememberSafeTestRun(id, latestSafeTest.id);
1594
2084
  state.safeTest.result = latestSafeTest;
1595
2085
  state.safeTest.status = safeTestStatus(latestSafeTest, currentContract());
1596
2086
  state.safeTest.error = null;
2087
+ if (latestSafeTest.status === 'queued' || latestSafeTest.status === 'running') scheduleSafeTestReadback(latestSafeTest.id);
2088
+ }
2089
+ const latestRun = normalized.records[0];
2090
+ if (latestRun?.id) {
2091
+ state.runs.selectedId = latestRun.id;
2092
+ await loadRunDetail(latestRun.id, { renderAfter: false });
1597
2093
  }
1598
2094
  }
1599
2095
  catch (error) { setError(state.runs, error); }
@@ -1643,6 +2139,280 @@ export function createManaOutcomeUI(options = {}) {
1643
2139
  render(); notifyChange(); return state.runAction;
1644
2140
  }
1645
2141
 
2142
+ // Profile, input, allowed-resource, and destination pickers. They exist
2143
+ // only when the host offers execution profiles; choices come from the
2144
+ // loaded connection resources, and references that are not listed now are
2145
+ // preserved instead of dropped.
2146
+ function contractProfilePickers(values, contract, fields) {
2147
+ const profileInput = createElement(document, 'input', { attrs: { name: 'profileId', type: 'hidden' } });
2148
+ profileInput.value = values.profileId;
2149
+ fields.profileId = profileInput;
2150
+ const knownProfile = (profileId) => executionProfiles.find((profile) => profile.id === profileId) ?? null;
2151
+ let selectedProfileId = values.profileId;
2152
+ const connectorById = (connectorId) => state.connections.records.find((item) => item.id === connectorId) ?? null;
2153
+ const resourceIsRegistered = (value) => {
2154
+ const resource = objectValue(value);
2155
+ const id = stringValue(firstPresent(resource.id, resource.resourceId, resource.resource_id, typeof value === 'string' ? value : null)).trim();
2156
+ const status = normalizeStatus(firstPresent(resource.status, resource.state, resource.connectionStatus, resource.connection_status));
2157
+ return Boolean(id) && !['failed', 'error', 'unavailable', 'permission_denied', 'forbidden', 'unauthorized'].includes(status);
2158
+ };
2159
+ const connectorHasRegisteredResource = (connector) => (connector?.resources ?? []).some(resourceIsRegistered);
2160
+ const profileConnectionLabel = (connector) => {
2161
+ if (['connected', 'verified', 'readback_verified'].includes(normalizeStatus(connector?.status))) return '接続確認済み';
2162
+ if (connectorHasRegisteredResource(connector)) return 'プロジェクト資源登録済み・接続未確認';
2163
+ if (!connector || ['unregistered', 'not_registered'].includes(normalizeStatus(connector.status))) return '未登録のため利用不可';
2164
+ return '接続未確認のため利用不可';
2165
+ };
2166
+ const currentReadiness = (profileId) => executionProfileReadiness(profileId, contract, [
2167
+ state.safeTest.result, state.runs.detail, ...state.runs.records,
2168
+ ], executionProfiles);
2169
+ const profileField = createElement(document, 'fieldset', { className: 'outcome-mana-profile-picker' });
2170
+ profileField.append(createElement(document, 'legend', { text: '委任する仕事' }));
2171
+ profileField.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: '仕事の種類を選ぶと、入力資料・生成内容・保存先が決まります。アプリの接続状態と、実行できるかの事前確認は別に表示します。' }));
2172
+ const profileRadios = [];
2173
+ executionProfiles.forEach((profile) => {
2174
+ const inputConnectors = profile.inputConnectorIds.map(connectorById);
2175
+ const destinationConnector = connectorById(profile.destinationConnectorId);
2176
+ const readiness = currentReadiness(profile.id);
2177
+ const card = createElement(document, 'label', { className: `outcome-mana-profile-card${selectedProfileId === profile.id ? ' is-selected' : ''}` });
2178
+ const radio = createElement(document, 'input', { attrs: { type: 'radio', name: 'manaExecutionProfile', value: profile.id, 'data-profile-id': profile.id } });
2179
+ radio.checked = selectedProfileId === profile.id;
2180
+ const copy = createElement(document, 'span', { className: 'outcome-mana-profile-copy' });
2181
+ copy.append(
2182
+ createElement(document, 'strong', { text: profile.label }),
2183
+ createElement(document, 'small', { text: `入力: ${profile.input} → Manaが生成: ${profile.output} → 保存先: ${profile.destination}` }),
2184
+ );
2185
+ const statuses = createElement(document, 'span', { className: 'outcome-mana-profile-statuses' });
2186
+ statuses.append(
2187
+ createElement(document, 'small', { text: `入力 ${profile.inputConnectorLabel}: ${inputConnectors.map(profileConnectionLabel).join('、')}`, attrs: { 'data-profile-input-connection': profile.id } }),
2188
+ createElement(document, 'small', { text: `保存先 ${profile.destinationConnectorLabel}: ${profileConnectionLabel(destinationConnector)}`, attrs: { 'data-profile-destination-connection': profile.id } }),
2189
+ createElement(document, 'small', { text: readiness.label, attrs: { 'data-profile-readiness': profile.id, 'data-readiness': readiness.status } }),
2190
+ );
2191
+ card.append(radio, copy, statuses);
2192
+ listen(radio, 'change', () => {
2193
+ if (!radio.checked) return;
2194
+ selectedProfileId = profile.id;
2195
+ profileInput.value = profile.id;
2196
+ profileRadios.forEach(({ card: candidate, radio: candidateRadio }) => {
2197
+ candidate.className = `outcome-mana-profile-card${candidateRadio === radio ? ' is-selected' : ''}`;
2198
+ });
2199
+ inputChoiceLabels.forEach(({ choice, resource, input }) => {
2200
+ choice.hidden = !profile.inputConnectorIds.includes(resource.connectorId);
2201
+ if (!profile.inputConnectorIds.includes(resource.connectorId)) input.checked = false;
2202
+ });
2203
+ inputRefsInput.value = inputChoiceLabels
2204
+ .filter(({ resource, input }) => input.checked && profile.inputConnectorIds.includes(resource.connectorId))
2205
+ .map(({ resource }) => `${resource.id}${resource.version ? `@${resource.version}` : ''}`)
2206
+ .join('\n');
2207
+ const allowedIds = [];
2208
+ allowedChoiceLabels.forEach(({ choice, resource, input }) => {
2209
+ const usable = profile.inputConnectorIds.includes(resource.connectorId) || resource.connectorId === profile.destinationConnectorId;
2210
+ choice.hidden = !usable;
2211
+ if (!usable) input.checked = false;
2212
+ else if (input.checked) allowedIds.push(resource.id);
2213
+ });
2214
+ allowedResourcesInput.value = [...new Set([...allowedIds, ...preservedUnknownIds])].join('\n');
2215
+ destinationChoiceLabels.forEach(({ choice, resource, input }) => {
2216
+ choice.hidden = resource.connectorId !== profile.destinationConnectorId;
2217
+ if (resource.connectorId !== profile.destinationConnectorId) input.checked = false;
2218
+ });
2219
+ if (destinationResource?.connectorId !== profile.destinationConnectorId) {
2220
+ destinationResource = null;
2221
+ adapterIdInput.value = '';
2222
+ locationInput.value = '';
2223
+ }
2224
+ });
2225
+ profileRadios.push({ card, radio });
2226
+ profileField.append(card);
2227
+ });
2228
+ const resourceIsSelectable = (value) => {
2229
+ const resource = objectValue(value);
2230
+ const status = normalizeStatus(firstPresent(resource.status, resource.state, resource.connectionStatus, resource.connection_status));
2231
+ return !['failed', 'error', 'unavailable', 'permission_denied', 'forbidden', 'unauthorized'].includes(status);
2232
+ };
2233
+ const selectableResources = state.connections.records
2234
+ .flatMap((connector) => (connector.resources ?? []).map((value) => {
2235
+ const resource = objectValue(value);
2236
+ const id = stringValue(firstPresent(resource.id, resource.resourceId, resource.resource_id, typeof value === 'string' ? value : null)).trim();
2237
+ const connectorVerified = ['connected', 'verified'].includes(normalizeStatus(connector.status));
2238
+ if (!id || !resourceIsSelectable(value) || (!connectorVerified && !resourceIsRegistered(value))) return null;
2239
+ return {
2240
+ id,
2241
+ label: resourceLabel(value),
2242
+ connectorId: connector.id,
2243
+ connector: connector.label,
2244
+ adapterId: stringValue(firstPresent(resource.adapterId, resource.adapter_id, connector.id)).trim(),
2245
+ version: stringValue(firstPresent(resource.version, resource.commit, resource.sha, resource.revision)).trim() || null,
2246
+ };
2247
+ }).filter(Boolean));
2248
+ const selectableIds = new Set(selectableResources.map((resource) => resource.id));
2249
+ const inputReferenceValues = parseReferenceLines(values.inputRefs);
2250
+ const inputReferenceIds = inputReferenceValues.map((reference) => reference.id);
2251
+ const preservedInputReferences = inputReferenceValues.filter((reference) => !selectableIds.has(reference.id));
2252
+ const inputField = createElement(document, 'fieldset', { className: 'outcome-mana-resource-picker' });
2253
+ inputField.append(createElement(document, 'legend', { text: '入力資料' }));
2254
+ const inputRefsInput = createElement(document, 'textarea', {
2255
+ className: 'outcome-mana-resource-picker-value',
2256
+ attrs: { name: 'inputRefs', hidden: 'hidden', 'aria-hidden': 'true' },
2257
+ });
2258
+ inputRefsInput.value = values.inputRefs;
2259
+ fields.inputRefs = inputRefsInput;
2260
+ inputField.append(inputRefsInput);
2261
+ const inputChoiceLabels = [];
2262
+ if (selectableResources.length) {
2263
+ const choices = createElement(document, 'div', { className: 'outcome-mana-resource-choices' });
2264
+ const syncInputReferences = () => {
2265
+ const selectedProfile = knownProfile(selectedProfileId);
2266
+ const selected = selectableResources.filter((candidate) => candidate.inputRef?.checked
2267
+ && selectedProfile?.inputConnectorIds.includes(candidate.connectorId));
2268
+ inputRefsInput.value = [
2269
+ ...selected.map((candidate) => {
2270
+ const version = stringValue(candidate.versionInput?.value ?? candidate.version).trim();
2271
+ return `${candidate.id}${version ? `@${version}` : ''}`;
2272
+ }),
2273
+ ...preservedInputReferences.map((reference) => `${reference.id}${reference.version ? `@${reference.version}` : ''}`),
2274
+ ].join('\n');
2275
+ };
2276
+ selectableResources.forEach((resource) => {
2277
+ const input = createElement(document, 'input', { attrs: { type: 'checkbox', value: resource.id, 'data-picker': 'input-reference' } });
2278
+ input.checked = inputReferenceIds.includes(resource.id);
2279
+ listen(input, 'change', syncInputReferences);
2280
+ resource.inputRef = input;
2281
+ const choice = createElement(document, 'label', { className: 'outcome-mana-resource-choice' });
2282
+ choice.hidden = !knownProfile(selectedProfileId)?.inputConnectorIds.includes(resource.connectorId);
2283
+ inputChoiceLabels.push({ choice, resource, input });
2284
+ choice.append(input, createElement(document, 'span', { text: resource.label }), createElement(document, 'small', { text: `${resource.connector} · ${resource.id}${resource.version ? ` @ ${resource.version}` : ' · 版未確認'}` }));
2285
+ if (resource.connectorId === 'github') {
2286
+ const existingVersion = inputReferenceValues.find((reference) => reference.id === resource.id)?.version ?? resource.version ?? '';
2287
+ const versionInput = createElement(document, 'input', {
2288
+ className: 'outcome-mana-resource-version',
2289
+ attrs: {
2290
+ type: 'text',
2291
+ value: existingVersion,
2292
+ placeholder: '40文字のコミットSHA',
2293
+ maxlength: '40',
2294
+ 'aria-label': `${resource.label}のコミットSHA`,
2295
+ },
2296
+ });
2297
+ versionInput.value = existingVersion;
2298
+ resource.versionInput = versionInput;
2299
+ listen(versionInput, 'input', syncInputReferences);
2300
+ listen(versionInput, 'change', syncInputReferences);
2301
+ choice.append(versionInput);
2302
+ }
2303
+ choices.append(choice);
2304
+ });
2305
+ inputField.append(choices);
2306
+ } else {
2307
+ inputField.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: '入力に使える接続済み資源がありません。' }));
2308
+ }
2309
+ if (preservedInputReferences.length) inputField.append(createElement(document, 'p', { className: 'outcome-mana-resource-preserved', text: `既存の入力参照を保持(現在は未確認): ${preservedInputReferences.map((reference) => reference.id).join('、')}` }));
2310
+ const allowedResourceIds = values.allowedResources.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
2311
+ const preservedUnknownIds = allowedResourceIds.filter((id) => !selectableIds.has(id));
2312
+ const resourceField = createElement(document, 'fieldset', { className: 'outcome-mana-resource-picker' });
2313
+ resourceField.append(createElement(document, 'legend', { text: 'この委任で使う接続資源' }));
2314
+ const allowedResourcesInput = createElement(document, 'textarea', {
2315
+ className: 'outcome-mana-resource-picker-value',
2316
+ attrs: { name: 'allowedResources', hidden: 'hidden', 'aria-hidden': 'true' },
2317
+ });
2318
+ allowedResourcesInput.value = allowedResourceIds.join('\n');
2319
+ fields.allowedResources = allowedResourcesInput;
2320
+ resourceField.append(allowedResourcesInput);
2321
+ const allowedChoiceLabels = [];
2322
+ if (selectableResources.length) {
2323
+ const choices = createElement(document, 'div', { className: 'outcome-mana-resource-choices' });
2324
+ selectableResources.forEach((resource) => {
2325
+ const input = createElement(document, 'input', { attrs: { type: 'checkbox', value: resource.id, 'data-picker': 'allowed-resource' } });
2326
+ input.checked = allowedResourceIds.includes(resource.id);
2327
+ listen(input, 'change', () => {
2328
+ const selectedProfile = knownProfile(selectedProfileId);
2329
+ const selected = selectableResources.filter((candidate) => candidate.input.checked
2330
+ && (selectedProfile?.inputConnectorIds.includes(candidate.connectorId) || candidate.connectorId === selectedProfile?.destinationConnectorId)).map((candidate) => candidate.id);
2331
+ allowedResourcesInput.value = [...new Set([...selected, ...preservedUnknownIds])].join('\n');
2332
+ if (!input.checked && destinationResource?.id === resource.id) {
2333
+ destinationResource = null;
2334
+ adapterIdInput.value = '';
2335
+ locationInput.value = '';
2336
+ const destinationRadio = destinationChoiceLabels.find((item) => item.resource.id === resource.id)?.input;
2337
+ if (destinationRadio) destinationRadio.checked = false;
2338
+ }
2339
+ });
2340
+ resource.input = input;
2341
+ const choice = createElement(document, 'label', { className: 'outcome-mana-resource-choice' });
2342
+ choice.hidden = !(knownProfile(selectedProfileId)?.inputConnectorIds.includes(resource.connectorId)
2343
+ || resource.connectorId === knownProfile(selectedProfileId)?.destinationConnectorId);
2344
+ allowedChoiceLabels.push({ choice, resource, input });
2345
+ choice.append(input, createElement(document, 'span', { text: resource.label }), createElement(document, 'small', { text: `${resource.connector} · ${resource.id}` }));
2346
+ choices.append(choice);
2347
+ });
2348
+ resourceField.append(choices);
2349
+ } else {
2350
+ resourceField.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: catalogCopy.noSelectableResources }));
2351
+ }
2352
+ if (preservedUnknownIds.length) {
2353
+ resourceField.append(createElement(document, 'p', { className: 'outcome-mana-resource-preserved', text: `既存参照を保持(現在は未確認): ${preservedUnknownIds.join('、')}` }));
2354
+ }
2355
+ const destinationField = createElement(document, 'fieldset', { className: 'outcome-mana-resource-picker' });
2356
+ destinationField.append(createElement(document, 'legend', { text: '成果物の保存先' }));
2357
+ const adapterIdInput = createElement(document, 'input', { attrs: { name: 'adapterId', type: 'hidden' } });
2358
+ const locationInput = createElement(document, 'input', { attrs: { name: 'location', type: 'hidden' } });
2359
+ adapterIdInput.value = values.adapterId;
2360
+ locationInput.value = values.location;
2361
+ let destinationResource = selectableResources.find((resource) => resource.id === values.location && resource.adapterId === values.adapterId) ?? null;
2362
+ fields.adapterId = adapterIdInput;
2363
+ fields.location = locationInput;
2364
+ destinationField.append(adapterIdInput, locationInput);
2365
+ const destinationChoiceLabels = [];
2366
+ if (selectableResources.length) {
2367
+ const choices = createElement(document, 'div', { className: 'outcome-mana-resource-choices' });
2368
+ selectableResources.forEach((resource) => {
2369
+ const input = createElement(document, 'input', { attrs: { type: 'radio', name: 'artifactDestinationChoice', value: resource.id, 'data-picker': 'artifact-destination' } });
2370
+ input.checked = values.location === resource.id && values.adapterId === resource.adapterId;
2371
+ const choice = createElement(document, 'label', { className: 'outcome-mana-resource-choice' });
2372
+ choice.hidden = resource.connectorId !== knownProfile(selectedProfileId)?.destinationConnectorId;
2373
+ listen(input, 'change', () => {
2374
+ if (!input.checked) return;
2375
+ destinationResource = resource;
2376
+ adapterIdInput.value = resource.adapterId;
2377
+ locationInput.value = resource.id;
2378
+ const allowedInput = allowedChoiceLabels.find((item) => item.resource.id === resource.id)?.input;
2379
+ if (allowedInput) allowedInput.checked = true;
2380
+ const selectedProfile = knownProfile(selectedProfileId);
2381
+ const selected = allowedChoiceLabels.filter((item) => item.input.checked
2382
+ && (selectedProfile?.inputConnectorIds.includes(item.resource.connectorId)
2383
+ || item.resource.connectorId === selectedProfile?.destinationConnectorId)).map((item) => item.resource.id);
2384
+ allowedResourcesInput.value = [...new Set([...selected, ...preservedUnknownIds])].join('\n');
2385
+ });
2386
+ destinationChoiceLabels.push({ choice, resource, input });
2387
+ choice.append(input, createElement(document, 'span', { text: resource.label }), createElement(document, 'small', { text: `${resource.connector} · ${resource.id}` }));
2388
+ choices.append(choice);
2389
+ });
2390
+ destinationField.append(choices);
2391
+ } else {
2392
+ destinationField.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: '保存先に使える接続済み資源がありません。' }));
2393
+ }
2394
+ if (values.location && !selectableIds.has(values.location)) destinationField.append(createElement(document, 'p', { className: 'outcome-mana-resource-preserved', text: `既存の保存先を保持(現在は未確認): ${values.location}` }));
2395
+ function validate(isExisting) {
2396
+ const selected = knownProfile(profileInput.value);
2397
+ const inputReferences = parseReferenceLines(inputRefsInput.value);
2398
+ const allowedIds = allowedResourcesInput.value.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
2399
+ const reasons = profileInputValidationReasons(selected, inputReferences, selectableResources);
2400
+ const originalDestination = contract.artifactDestination;
2401
+ const preservingUnlistedDestination = isExisting && !destinationResource
2402
+ && Boolean(originalDestination?.adapterId && originalDestination?.location)
2403
+ && adapterIdInput.value === originalDestination.adapterId
2404
+ && locationInput.value === originalDestination.location
2405
+ && fields.environment.value === originalDestination.environment
2406
+ && profileInput.value === contract.profileId;
2407
+ if (selected && !destinationResource && !preservingUnlistedDestination) reasons.push(`${selected.destinationConnectorLabel}の保存先を選んでください。`);
2408
+ if (destinationResource && destinationResource.connectorId !== selected?.destinationConnectorId) reasons.push('保存先アプリが選んだ仕事と一致していません。');
2409
+ if (destinationResource && !allowedIds.includes(destinationResource.id)) reasons.push('保存先を「この委任で使う接続資源」に含めてください。');
2410
+ if (!allowedIds.some((id) => selectableIds.has(id))) reasons.push('この委任で使う接続資源を1つ以上選んでください。');
2411
+ return reasons;
2412
+ }
2413
+ return { profileInput, profileField, inputField, resourceField, destinationField, validate };
2414
+ }
2415
+
1646
2416
  function renderContractSection() {
1647
2417
  const section = createElement(document, 'section', { className: 'outcome-mana-section outcome-mana-contract', attrs: { 'aria-labelledby': 'outcome-mana-contract-heading' } });
1648
2418
  section.append(sectionHeading(document, '成果契約', state.contract.status, '期待する成果、対象範囲、根拠、保存先、完了条件を版付きで管理します。保存と有効化は別操作です。', 'outcome-mana-contract-heading'));
@@ -1667,13 +2437,18 @@ export function createManaOutcomeUI(options = {}) {
1667
2437
  const form = createElement(document, 'form', { className: 'outcome-mana-form', attrs: { 'aria-label': '成果契約の編集' } });
1668
2438
  const fields = {};
1669
2439
  const addField = (name, label, type, options = {}) => { const result = field(document, label, type, name, values[name], options); fields[name] = result.input; form.append(result.label); };
2440
+ const pickers = profilePicker ? contractProfilePickers(values, contract, fields) : null;
2441
+ if (pickers) form.append(pickers.profileInput, pickers.profileField);
1670
2442
  addField('outcome', '期待する成果', 'textarea', { attrs: { rows: '4', required: 'required', maxlength: '2000' }, helper: '手順ではなく、受け取りたい状態を具体的に書きます。' });
1671
2443
  addField('scope', '対象範囲', 'textarea', { attrs: { rows: '3', required: 'required', maxlength: '2000' }, helper: '対象プロジェクトや資料の範囲。サーバーでも検証します。' });
1672
- addField('inputRefs', '入力資料(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', required: 'required', maxlength: '4000' } });
2444
+ if (!pickers) addField('inputRefs', '入力資料(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', required: 'required', maxlength: '4000' } });
1673
2445
  addField('knowledgeRefs', '参照する知識(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', maxlength: '4000' } });
1674
2446
  addField('judgmentRefs', '参照する判断(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', maxlength: '4000' } });
1675
- addField('adapterId', '成果物アダプター', 'input', { attrs: { required: 'required', maxlength: '128' } });
1676
- addField('location', '成果物の保存先', 'input', { attrs: { required: 'required', maxlength: '2000' } });
2447
+ if (pickers) form.append(pickers.inputField, pickers.resourceField, pickers.destinationField);
2448
+ else {
2449
+ addField('adapterId', '成果物アダプター', 'input', { attrs: { required: 'required', maxlength: '128' } });
2450
+ addField('location', '成果物の保存先', 'input', { attrs: { required: 'required', maxlength: '2000' } });
2451
+ }
1677
2452
  addField('environment', '保存環境', 'select', { options: [{ value: 'isolated', label: '隔離保存先' }, { value: 'production', label: '本番保存先' }], helper: '隔離試験は本番保存先へ書き込みません。' });
1678
2453
  addField('completionCriteria', '完了条件(id・説明・kind・expected、タブ区切り)', 'textarea', { attrs: { rows: '5', required: 'required', maxlength: '8000' }, helper: '例: criteria-1[Tab]本文を含む[Tab]content_includes[Tab]報告' });
1679
2454
  addField('ownerActorId', '責任者の主体ID', 'input', { attrs: { required: 'required', maxlength: '128' }, helper: '主体はサーバーで現在の組織権限と照合します。' });
@@ -1693,6 +2468,13 @@ export function createManaOutcomeUI(options = {}) {
1693
2468
  const isExisting = Boolean(contract.contractId);
1694
2469
  const save = button(document, isExisting ? '下書きを保存' : '下書きを作成', async () => {
1695
2470
  if (typeof form.reportValidity === 'function' && !form.reportValidity()) return;
2471
+ const reasons = pickers ? pickers.validate(isExisting) : [];
2472
+ if (reasons.length) {
2473
+ let error = Array.from(form.children).find((child) => child.hasAttribute?.('data-profile-form-error'));
2474
+ if (!error) { error = createElement(document, 'p', { className: 'outcome-mana-inline-error', attrs: { role: 'alert', 'data-profile-form-error': 'true' } }); form.append(error); }
2475
+ error.textContent = reasons.join(' ');
2476
+ return;
2477
+ }
1696
2478
  await saveContractDraft(contractDraftFromForm(fields), { create: !isExisting });
1697
2479
  }, { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !manage || state.contract.status === 'saving', attrs: { 'aria-label': isExisting ? '成果契約の下書きを保存' : '成果契約の下書きを作成' } });
1698
2480
  actions.append(save);
@@ -1713,6 +2495,19 @@ export function createManaOutcomeUI(options = {}) {
1713
2495
  else if (['loading', 'empty', 'permission_denied', 'error_retryable', 'conflict', 'unknown'].includes(state.authority.status)) {
1714
2496
  const message = stateMessage(document, state.authority.status, { emptyTitle: '権限設定は未登録です', emptyText: '上流の権限設定を確認してから追加します。', message: state.authority.error?.message, retry: () => loadAuthority() });
1715
2497
  if (message) content.append(message);
2498
+ if (state.authority.status === 'empty' && canManage(currentSession())) {
2499
+ const form = createElement(document, 'form', { className: 'outcome-mana-form', attrs: { 'aria-label': '最初の権限設定を追加' } });
2500
+ const operation = field(document, '許可する操作', 'input', 'authorityOperation', '', { attrs: { required: 'required', maxlength: '128' }, helper: '例: github.contents.read。実行時の操作IDと一致させます。' });
2501
+ const resource = field(document, '対象資源ID', 'input', 'authorityResource', '', { attrs: { required: 'required', maxlength: '256' }, helper: '接続資源の安定IDを指定します。表示名やURLでは判定しません。' });
2502
+ const policy = field(document, '実行方針', 'select', 'authorityPolicy', 'approval', { options: Object.entries(AUTHORITY_LABELS).map(([value, label]) => ({ value, label })) });
2503
+ form.append(operation.label, resource.label, policy.label);
2504
+ form.append(button(document, '最初の権限を追加', () => {
2505
+ if (typeof form.reportValidity === 'function' && !form.reportValidity()) return;
2506
+ saveAuthority([{ operation: operation.input.value.trim(), resource: resource.input.value.trim(), policy: policy.input.value }]);
2507
+ }, { className: 'outcome-mana-button outcome-mana-button-primary' }));
2508
+ listen(form, 'submit', (event) => event.preventDefault?.());
2509
+ content.append(form);
2510
+ }
1716
2511
  } else {
1717
2512
  const matrix = state.authority.draft ?? state.authority.matrix;
1718
2513
  const table = createElement(document, 'div', { className: 'outcome-mana-authority-table', attrs: { role: 'table', 'aria-label': '操作権限マトリクス' } });
@@ -1787,9 +2582,30 @@ export function createManaOutcomeUI(options = {}) {
1787
2582
  form.append(actions); listen(form, 'submit', (event) => event.preventDefault?.()); content.append(form); section.append(content); return section;
1788
2583
  }
1789
2584
 
2585
+ function renderPreflightChecks(preflight) {
2586
+ if (!preflight?.checks?.length) return null;
2587
+ const section = createElement(document, 'section');
2588
+ section.append(createElement(document, 'h4', { text: '実行前チェック' }));
2589
+ const checks = createElement(document, 'div', { className: 'outcome-mana-preflight-list', attrs: { role: 'list', 'aria-label': '実行前チェック' } });
2590
+ preflight.checks.forEach((check) => {
2591
+ const item = createElement(document, 'article', { className: 'outcome-mana-preflight-item', attrs: { role: 'listitem' } });
2592
+ item.append(createElement(document, 'strong', { text: check.id }), statusBadge(document, check.status));
2593
+ item.append(definitionList(document, [
2594
+ ['不足理由', PREFLIGHT_REASON_LABELS[check.reason] ?? check.reason],
2595
+ ['必要な対応', PREFLIGHT_ACTION_LABELS[check.action] ?? check.action],
2596
+ ['対応後の再開地点', STAGE_LABELS[check.resumeFrom] ?? check.resumeFrom],
2597
+ ]));
2598
+ checks.append(item);
2599
+ });
2600
+ section.append(checks);
2601
+ return section;
2602
+ }
2603
+
1790
2604
  function renderSafeTestSection() {
1791
2605
  const section = createElement(document, 'section', { className: 'outcome-mana-section', attrs: { 'aria-labelledby': 'outcome-mana-safe-test-heading' } });
1792
2606
  const contract = currentContract();
2607
+ const inFlight = Boolean(state.safeTest.runId && state.safeTest.status !== 'unknown'
2608
+ && ['queued', 'running'].includes(normalizeRun(state.safeTest.result).status));
1793
2609
  const effectiveTestStatus = state.safeTest.status === 'loading' ? 'loading' : state.safeTest.result
1794
2610
  ? safeTestStatus(state.safeTest.result, contract) : state.safeTest.status;
1795
2611
  section.append(sectionHeading(document, '隔離試験', effectiveTestStatus, '契約版とサンプル入力で試験します。外部送信と本番書き込みはリクエストでも遮断し、未実行の操作を成功とは表示しません。', 'outcome-mana-safe-test-heading'));
@@ -1802,25 +2618,50 @@ export function createManaOutcomeUI(options = {}) {
1802
2618
  form.append(version.label, sample.label);
1803
2619
  const safety = createElement(document, 'p', { className: 'outcome-mana-safe-note', text: '隔離モード: 外部送信なし / 本番書き込みなし / 成果物は隔離保存先のみ' }); form.append(safety);
1804
2620
  const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
1805
- actions.append(button(document, '隔離試験を実行', () => runSafeTest({ contractVersion: Number(version.input.value), sampleInput: sample.input.value }), { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) || state.safeTest.status === 'loading' }));
2621
+ actions.append(button(document, inFlight ? '隔離試験を実行中' : '新しい隔離試験を実行', () => runSafeTest({ contractVersion: Number(version.input.value), sampleInput: sample.input.value }), { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) || state.safeTest.status === 'loading' || inFlight }));
1806
2622
  form.append(actions); listen(form, 'submit', (event) => event.preventDefault?.()); content.append(form);
1807
- if (effectiveTestStatus === 'unexecuted') content.append(stateMessage(document, 'unknown', { message: '試験要求は受理されましたが、実行結果を確認できていません。成功とは扱いません。', retry: () => runSafeTest({ contractVersion: Number(version.input.value), sampleInput: sample.input.value }) }));
1808
- if (effectiveTestStatus === 'executed') content.append(stateMessage(document, 'unknown', { message: '試験は実行済みですが、完了条件の検証済み結果を確認できていません。', retry: () => runSafeTest({ contractVersion: Number(version.input.value), sampleInput: sample.input.value }) }));
2623
+ if (inFlight) {
2624
+ content.append(createElement(document, 'p', { text: '隔離試験を実行中です。同じ試験IDの結果を追跡しています。', attrs: { role: 'status' } }));
2625
+ content.append(button(document, '結果を再取得', refreshSafeTestReadback, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
2626
+ }
2627
+ if (!inFlight && effectiveTestStatus === 'unexecuted') content.append(stateMessage(document, 'unknown', { message: '試験要求は受理されましたが、実行結果を確認できていません。成功とは扱いません。', retry: state.safeTest.runId ? refreshSafeTestReadback : null, retryLabel: '結果を再取得' }));
2628
+ if (!inFlight && state.safeTest.status === 'unknown' && state.safeTest.runId) content.append(stateMessage(document, 'unknown', { message: '結果の取得を確認できません。同じ試験IDを再取得してください。', retry: refreshSafeTestReadback, retryLabel: '結果を再取得' }));
1809
2629
  if (effectiveTestStatus === 'verified') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-success', text: 'この設定で試験済み', attrs: { role: 'status' } }));
1810
2630
  if (effectiveTestStatus === 'stale') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '設定変更のため再試験が必要', attrs: { role: 'status' } }));
1811
- if (effectiveTestStatus === 'completion_unverified') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '試験は実行済みですが、完了条件の検証結果を確認できていません。', attrs: { role: 'status' } }));
2631
+ if (!inFlight && state.safeTest.status !== 'unknown' && effectiveTestStatus === 'completion_unverified') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '試験は実行済みですが、完了条件の検証結果を確認できていません。', attrs: { role: 'status' } }));
1812
2632
  if (effectiveTestStatus === 'configuration_snapshot_missing') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '試験時の設定記録が不足しているため、現在の設定と一致するか確認できません。再試験が必要です。', attrs: { role: 'status' } }));
1813
- if (effectiveTestStatus === 'failed') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '隔離試験が失敗しました。外部への副作用は成功扱いにしていません。', attrs: { role: 'alert' } }));
1814
- if (state.safeTest.error) content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: state.safeTest.error.message, attrs: { role: 'alert' } }));
2633
+ if (effectiveTestStatus === 'failed') {
2634
+ content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '隔離試験が失敗しました。外部への副作用は成功扱いにしていません。', attrs: { role: 'alert' } }));
2635
+ }
2636
+ if (state.safeTest.error) {
2637
+ const error = state.safeTest.error;
2638
+ content.append(definitionList(document, [
2639
+ ['エラーコード', error.code || UNKNOWN],
2640
+ ['HTTP状態', error.status == null ? UNKNOWN : String(error.status)],
2641
+ ['理由', error.message || UNKNOWN],
2642
+ ]));
2643
+ }
1815
2644
  if (state.safeTest.result) {
1816
2645
  const result = objectValue(state.safeTest.result);
1817
2646
  const execution = objectValue(result.execution ?? result.testRun ?? result.run);
1818
- content.append(definitionList(document, [['実行状態', statusLabel(completionState(state.safeTest.result))], ['試験ID', firstPresent(result.test_id, result.testId, result.id, execution.id)], ['対象版', firstPresent(result.contract_version, result.contractVersion, contract.version)]]));
2647
+ const run = normalizeRun(state.safeTest.result);
2648
+ content.append(definitionList(document, [
2649
+ ['完了判定', statusLabel(completionState(state.safeTest.result))],
2650
+ ['Mana実行状態', statusLabel(run.status)],
2651
+ ['試験ID', firstPresent(state.safeTest.runId, run.id, result.test_id, result.testId, result.id, execution.id)],
2652
+ ['対象版', firstPresent(run.contractVersion, result.contract_version, result.contractVersion, contract.version)],
2653
+ ...(run.stage ? [['現在の段階', STAGE_LABELS[run.stage] ?? run.stage]] : []),
2654
+ ...(run.errorCode ? [['エラーコード', run.errorCode]] : []),
2655
+ ...(run.errorMessage ? [['理由', run.errorMessage]] : []),
2656
+ ['隔離成果物の読戻し', artifactReadbackSummary(run.artifactReadback)],
2657
+ ]));
2658
+ const preflight = renderPreflightChecks(run.preflight);
2659
+ if (preflight) content.append(preflight);
1819
2660
  content.append(evidenceList(document, '予定した操作', result.operations ?? execution.operations));
1820
2661
  content.append(evidenceList(document, '必要な承認', result.approvals ?? execution.approvals));
1821
2662
  content.append(evidenceList(document, '参照した根拠', result.evidence ?? execution.evidence));
1822
- content.append(evidenceList(document, '隔離成果物', result.artifacts ?? execution.artifacts));
1823
- content.append(evidenceList(document, '完了条件の評価', result.completion?.criteria ?? result.completion ?? execution.completion?.criteria ?? execution.completion));
2663
+ content.append(evidenceList(document, '隔離成果物', run.artifacts));
2664
+ content.append(evidenceList(document, '完了条件の評価', criterionSummaries(run.completion.criteria)));
1824
2665
  }
1825
2666
  section.append(content); return section;
1826
2667
  }
@@ -1844,6 +2685,8 @@ export function createManaOutcomeUI(options = {}) {
1844
2685
  ['更新', dateLabel(detail.updatedAt)],
1845
2686
  ]));
1846
2687
  section.append(stageList(document, detail.stages));
2688
+ const preflight = renderPreflightChecks(detail.preflight);
2689
+ if (preflight) section.append(preflight);
1847
2690
  section.append(evidenceList(document, '参照した根拠', detail.evidence), evidenceList(document, '成果物', detail.artifacts), evidenceList(document, '完了条件', detail.completion.criteria));
1848
2691
  if (detail.completion.reason) section.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: detail.completion.reason, attrs: { role: 'alert' } }));
1849
2692
  const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
@@ -1919,12 +2762,13 @@ export function createManaOutcomeUI(options = {}) {
1919
2762
 
1920
2763
  function renderOverviewView() {
1921
2764
  const view = createElement(document, 'section', { className: 'outcome-mana-overview', attrs: { 'aria-labelledby': 'outcome-mana-overview-heading' } });
1922
- function startNewDelegation() {
2765
+ async function startNewDelegation() {
1923
2766
  state.contract.detail = null;
1924
2767
  state.contract.selectedId = null;
1925
2768
  state.contract.formDraft = {};
1926
2769
  setActiveStage('contract');
1927
2770
  render();
2771
+ await ensureConnectionsForDelegation();
1928
2772
  }
1929
2773
  const heading = createElement(document, 'div', { className: 'outcome-mana-view-heading' });
1930
2774
  heading.append(group(document, 'div', {}, [
@@ -2019,7 +2863,11 @@ export function createManaOutcomeUI(options = {}) {
2019
2863
  ['責任者', present(selected.owner?.actorId)], ['対象範囲', present(selected.scope)], ['起動条件', triggerLabel(selected.trigger?.type)],
2020
2864
  ['契約版', selected.version === null ? UNKNOWN : `v${selected.version}`], ['最終実行', state.runs.records[0] ? dateLabel(state.runs.records[0].updatedAt ?? state.runs.records[0].startedAt) : UNKNOWN],
2021
2865
  ]));
2022
- inspector.append(button(document, '委任設定を開く', () => { setActiveView('delegation_settings'); render(); }, { className: 'outcome-mana-button outcome-mana-button-primary' }));
2866
+ inspector.append(button(document, '委任設定を開く', async () => {
2867
+ setActiveView('delegation_settings');
2868
+ render();
2869
+ await ensureConnectionsForDelegation();
2870
+ }, { className: 'outcome-mana-button outcome-mana-button-primary' }));
2023
2871
  }
2024
2872
  layout.append(list, inspector); view.append(layout); return view;
2025
2873
  }
@@ -2030,8 +2878,10 @@ export function createManaOutcomeUI(options = {}) {
2030
2878
  const values = Array.isArray(source) ? source : Object.entries(objectValue(source)).map(([id, value]) => ({ id, ...objectValue(value) }));
2031
2879
  const byId = new Map(values.map((item) => [normalizeConnector(item).id.toLowerCase(), normalizeConnector(item)]));
2032
2880
  const registeredById = new Map(connectorRecordsFromProject().map((item) => [item.id.toLowerCase(), item]));
2033
- const loadedById = new Map(state.connections.records.map((item) => [item.id.toLowerCase(), item]));
2034
- return CONNECTOR_DEFINITIONS.map((definition) => ({
2881
+ // With a host connection catalog this is the project-resource list only;
2882
+ // host connection status never makes a project resource look registered.
2883
+ const loadedById = new Map(connectionCatalog ? [] : state.connections.records.map((item) => [item.id.toLowerCase(), item]));
2884
+ return connectorDefinitions.map((definition) => ({
2035
2885
  ...definition,
2036
2886
  ...(registeredById.get(definition.id) ?? {}),
2037
2887
  ...(byId.get(definition.id) ?? {}),
@@ -2047,12 +2897,21 @@ export function createManaOutcomeUI(options = {}) {
2047
2897
  const headingActions = createElement(document, 'div', { className: 'outcome-mana-toolbar' });
2048
2898
  headingActions.append(button(document, state.connections.status === 'loading' ? '確認中…' : '接続を再確認', () => loadConnections(), { className: 'outcome-mana-button outcome-mana-button-secondary', disabled: state.connections.status === 'loading' }));
2049
2899
  view.append(group(document, 'div', { className: 'outcome-mana-view-heading' }, [headingCopy, headingActions]));
2050
- const connectors = projectConnectors();
2900
+ // A host connection catalog separates host-authenticated connections,
2901
+ // this project's registered resources, and the delegation's allowed range.
2902
+ const catalogMode = Boolean(connectionCatalog);
2903
+ const connectors = !catalogMode
2904
+ ? projectConnectors()
2905
+ : state.connections.records.length ? state.connections.records : catalogPlaceholders();
2051
2906
  if (!state.connections.selectedId && connectors.length) state.connections.selectedId = connectors[0].id;
2052
2907
  const selectedConnector = connectors.find((item) => item.id === state.connections.selectedId) ?? null;
2908
+ if (catalogMode) {
2909
+ view.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: catalogCopy.connectionsTitle }));
2910
+ view.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: catalogCopy.connectionsDescription }));
2911
+ }
2053
2912
  const table = createElement(document, 'div', { className: 'outcome-mana-connection-table', attrs: { role: 'table', 'aria-label': 'Mana組織接続一覧' } });
2054
2913
  const header = createElement(document, 'div', { className: 'outcome-mana-connection-row outcome-mana-connection-header', attrs: { role: 'row' } });
2055
- ['接続', '状態', 'アカウント', '登録資源', '最終確認'].forEach((label) => header.append(createElement(document, 'span', { text: label, attrs: { role: 'columnheader' } })));
2914
+ ['接続', '状態', 'アカウント', catalogMode ? 'このプロジェクトで利用' : '登録資源', '最終確認'].forEach((label) => header.append(createElement(document, 'span', { text: label, attrs: { role: 'columnheader' } })));
2056
2915
  table.append(header);
2057
2916
  for (const connector of connectors) {
2058
2917
  const selected = connector.id === state.connections.selectedId;
@@ -2065,28 +2924,58 @@ export function createManaOutcomeUI(options = {}) {
2065
2924
  connectorIdentity(document, connector, { attrs: { role: 'cell' } }),
2066
2925
  group(document, 'span', { attrs: { role: 'cell' } }, [statusBadge(document, connector.status ?? 'unknown', connectionStatusLabel(connector.status))]),
2067
2926
  createElement(document, 'span', { text: present(connector.account), attrs: { role: 'cell' } }),
2068
- createElement(document, 'span', { text: connector.resources?.length ? connector.resources.map(resourceLabel).join('、') : UNKNOWN, attrs: { role: 'cell' } }),
2927
+ createElement(document, 'span', {
2928
+ text: catalogMode
2929
+ ? projectResourceStatusLabel(projectConnectors().find((item) => item.id === connector.id) ?? {})
2930
+ : connector.resources?.length ? connector.resources.map(resourceLabel).join('、') : UNKNOWN,
2931
+ attrs: { role: 'cell' },
2932
+ }),
2069
2933
  createElement(document, 'span', { text: dateLabel(connector.checkedAt), attrs: { role: 'cell' } }),
2070
2934
  );
2071
2935
  table.append(row);
2072
2936
  }
2937
+ if (catalogMode) {
2938
+ view.append(table);
2939
+ const resourcesSection = createElement(document, 'section', { className: 'outcome-mana-resource-section', attrs: { 'aria-label': 'プロジェクト資源' } });
2940
+ resourcesSection.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '2. このプロジェクトで使う資源' }));
2941
+ resourcesSection.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: catalogCopy.resourcesDescription }));
2942
+ const resourceGrid = createElement(document, 'div', { className: 'outcome-mana-project-resource-grid' });
2943
+ for (const connector of projectConnectors()) {
2944
+ const card = createElement(document, 'article', { className: 'outcome-mana-project-resource-card' });
2945
+ card.append(connectorIdentity(document, connector));
2946
+ card.append(definitionList(document, [
2947
+ ['状態', projectResourceStatusLabel(connector)],
2948
+ ['接続元', connectionSourceLabel(connector.id)],
2949
+ ]));
2950
+ card.append(evidenceList(document, '登録資源', connector.resources?.length ? connector.resources : (connector.account ? [connector.account] : [])));
2951
+ resourceGrid.append(card);
2952
+ }
2953
+ resourcesSection.append(resourceGrid);
2954
+ view.append(resourcesSection);
2955
+ }
2073
2956
  const contract = currentContract();
2074
2957
  const inspector = createElement(document, 'aside', { className: 'outcome-mana-connection-inspector', attrs: { 'aria-label': '選択した接続の詳細' } });
2075
2958
  inspector.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '選択中の接続' }));
2076
2959
  if (selectedConnector) {
2077
2960
  inspector.append(connectorIdentity(document, selectedConnector, { large: true }));
2078
2961
  inspector.append(definitionList(document, [['接続状態', connectionStatusLabel(selectedConnector.status)], ['接続先', selectedConnector.account], ['確認日時', dateLabel(selectedConnector.checkedAt)], ['確認理由', connectionReasonLabel(selectedConnector.reason)]]));
2079
- inspector.append(evidenceList(document, '登録資源', selectedConnector.resources));
2962
+ if (catalogMode) {
2963
+ inspector.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: catalogCopy.manageNote }));
2964
+ if (typeof options.onManageConnection === 'function') {
2965
+ inspector.append(button(document, catalogCopy.manageAction, () => options.onManageConnection(selectedConnector.id), { className: 'outcome-mana-button outcome-mana-button-primary' }));
2966
+ }
2967
+ } else inspector.append(evidenceList(document, '登録資源', selectedConnector.resources));
2080
2968
  } else inspector.append(stateMessage(document, 'unknown', { message: '接続を選ぶと登録資源と確認状態を表示します。' }));
2081
2969
  const allowed = createElement(document, 'aside', { className: 'outcome-mana-resource-panel' });
2082
- allowed.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '委任単位の許可範囲' }), createElement(document, 'h3', { text: 'この委任が利用できる資源' }));
2970
+ allowed.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: catalogMode ? '3. 委任単位の許可範囲' : '委任単位の許可範囲' }), createElement(document, 'h3', { text: 'この委任が利用できる資源' }));
2083
2971
  if (!contract) allowed.append(stateMessage(document, 'unknown', { message: '委任を選ぶと、契約版に固定された参照先と保存先を確認できます。' }));
2084
2972
  else {
2085
2973
  const permitted = permittedResources(contract);
2086
2974
  allowed.append(evidenceList(document, '許可リソース', permitted));
2087
2975
  allowed.append(evidenceList(document, '入力', contract.inputRefs), evidenceList(document, '知識', contract.knowledgeRefs), evidenceList(document, '判断', contract.judgmentRefs), definitionList(document, [['保存先', present(contract.artifactDestination?.location)], ['接続ID', present(contract.artifactDestination?.adapterId)], ['環境', present(contract.artifactDestination?.environment)]]));
2088
2976
  }
2089
- view.append(table, group(document, 'div', { className: 'outcome-mana-connection-detail-layout' }, [inspector, allowed])); return view;
2977
+ if (!catalogMode) view.append(table);
2978
+ view.append(group(document, 'div', { className: 'outcome-mana-connection-detail-layout' }, [inspector, allowed])); return view;
2090
2979
  }
2091
2980
 
2092
2981
  function renderDelegationSettings() {
@@ -2214,7 +3103,14 @@ export function createManaOutcomeUI(options = {}) {
2214
3103
  for (const [index, stage] of WORKFLOW_STAGES.entries()) {
2215
3104
  const item = createElement(document, 'li', { className: `outcome-mana-stage-item${activeStage === stage.id ? ' is-active' : ''}` });
2216
3105
  const status = stageStatus(stage.id);
2217
- const stageButton = button(document, '', () => { setActiveStage(stage.id); render(); }, {
3106
+ const stageButton = button(document, '', () => {
3107
+ if (stage.id === 'authority' && state.authority.status === 'idle' && state.contract.status === 'ready' && currentContract()?.contractId === state.contract.selectedId) {
3108
+ void loadAuthority();
3109
+ return;
3110
+ }
3111
+ setActiveStage(stage.id);
3112
+ render();
3113
+ }, {
2218
3114
  className: 'outcome-mana-stage-button',
2219
3115
  attrs: {
2220
3116
  'aria-current': activeStage === stage.id ? 'step' : 'false',
@@ -2297,6 +3193,7 @@ export function createManaOutcomeUI(options = {}) {
2297
3193
  saveAuthority,
2298
3194
  saveTriggers,
2299
3195
  runSafeTest,
3196
+ refreshSafeTestReadback,
2300
3197
  activate,
2301
3198
  stopActivation,
2302
3199
  loadRuns,