@unson/brainbase-mcp 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/local-web-host.js +6 -0
- package/package.json +12 -7
- package/ui/graph-projects-view.css +66 -0
- package/ui/graph-projects-view.js +126 -10
- package/ui/project-graph-entry.js +290 -0
- package/ui/project-graph-vendor.LICENSE.txt +109 -0
- package/ui/project-graph-vendor.js +312 -0
- package/ui/project-graph.js +263 -0
- package/ui/project-workspace.css +491 -0
- package/ui/project-workspace.js +722 -0
- package/ui/icons/mana/LICENSE.tabler.txt +0 -21
- package/ui/icons/mana/affiliate.svg +0 -20
- package/ui/icons/mana/brand-github.svg +0 -15
- package/ui/icons/mana/brand-google-drive.svg +0 -17
- package/ui/icons/mana/brand-slack.svg +0 -18
- package/ui/icons/mana/cpu.svg +0 -24
- package/ui/outcome-mana.css +0 -332
- package/ui/outcome-mana.js +0 -3222
package/ui/outcome-mana.js
DELETED
|
@@ -1,3222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mana outcome-delegation UI.
|
|
3
|
-
*
|
|
4
|
-
* This module deliberately has no knowledge of fetch, tenant tokens, or the
|
|
5
|
-
* Mana runtime URL. The host injects the same-origin `api` and `apiMutation`
|
|
6
|
-
* functions used by the organization shell. The BFF remains responsible for
|
|
7
|
-
* deriving tenant, actor, and project access from the authenticated session.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const OUTCOME_DELEGATION_CONTRACT_VERSION = 'mana.outcome-delegation.v1';
|
|
11
|
-
export const MANA_STORY_IDS = Object.freeze([
|
|
12
|
-
'story-brainbase-outcome-mana-contract',
|
|
13
|
-
'story-brainbase-outcome-mana-authority',
|
|
14
|
-
'story-brainbase-outcome-mana-triggers',
|
|
15
|
-
'story-brainbase-outcome-mana-deliver-outcome',
|
|
16
|
-
'story-brainbase-outcome-mana-safe-test',
|
|
17
|
-
'story-brainbase-outcome-mana-run-control',
|
|
18
|
-
]);
|
|
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
|
-
|
|
69
|
-
const UNKNOWN = '未確認';
|
|
70
|
-
const EMPTY = 'empty';
|
|
71
|
-
const STOPPED_RUN_RESTART_UNAVAILABLE = '停止した実行は再開できません。';
|
|
72
|
-
|
|
73
|
-
const STATUS_LABELS = Object.freeze({
|
|
74
|
-
loading: '読み込み中',
|
|
75
|
-
idle: '待機中',
|
|
76
|
-
ready: '取得済み',
|
|
77
|
-
draft: '下書き',
|
|
78
|
-
active: '有効',
|
|
79
|
-
stopped: '停止',
|
|
80
|
-
misconfigured: '設定不備',
|
|
81
|
-
retired: '廃止',
|
|
82
|
-
queued: '待機中',
|
|
83
|
-
pending: '待機中',
|
|
84
|
-
running: '実行中',
|
|
85
|
-
waiting_approval: '承認待ち',
|
|
86
|
-
failed: '失敗',
|
|
87
|
-
completion_unverified: '完了未確認',
|
|
88
|
-
configuration_snapshot_missing: '設定記録不足',
|
|
89
|
-
completed_verified: '成果検証済み',
|
|
90
|
-
completed: '完了',
|
|
91
|
-
not_executed: '未実行',
|
|
92
|
-
unexecuted: '未実行',
|
|
93
|
-
executed: '実行済み・完了未確認',
|
|
94
|
-
verified: '検証済み',
|
|
95
|
-
saved_unverified: '保存済み・確認待ち',
|
|
96
|
-
stale: '再試験が必要',
|
|
97
|
-
permission_denied: '権限不足',
|
|
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: '実行権限が設定されていません',
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const AUTHORITY_LABELS = Object.freeze({
|
|
133
|
-
auto: '自動許可',
|
|
134
|
-
approval: '承認が必要',
|
|
135
|
-
deny: '禁止',
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const TRIGGER_LABELS = Object.freeze({
|
|
139
|
-
manual: '手動',
|
|
140
|
-
schedule: '定期実行',
|
|
141
|
-
document: '文書追加',
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const RUN_STATUSES = new Set([
|
|
145
|
-
'queued', 'running', 'waiting_approval', 'failed', 'stopped',
|
|
146
|
-
'completion_unverified', 'completed_verified',
|
|
147
|
-
]);
|
|
148
|
-
|
|
149
|
-
const STAGE_LABELS = Object.freeze({
|
|
150
|
-
preflight: '事前確認',
|
|
151
|
-
knowledge_retrieval: '知識取得',
|
|
152
|
-
input_retrieval: '入力取得',
|
|
153
|
-
generate: '成果生成',
|
|
154
|
-
effect_execution: '外部操作',
|
|
155
|
-
artifact_save: '成果物保存',
|
|
156
|
-
artifact_readback: '保存結果確認',
|
|
157
|
-
completion_verification: '完了条件確認',
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// The runtime still exposes the same contract, authority, trigger, test, and
|
|
161
|
-
// run resources. The UI presents those resources as one operational workflow
|
|
162
|
-
// so people can see the current step without expanding five long cards at once.
|
|
163
|
-
const WORKFLOW_STAGES = Object.freeze([
|
|
164
|
-
{ id: 'contract', label: '成果契約', description: '成果・根拠・保存先・完了条件' },
|
|
165
|
-
{ id: 'authority', label: '権限', description: '操作・対象資源・主体ごとの許可' },
|
|
166
|
-
{ id: 'triggers', label: '起動条件', description: '起動方式と明示的な有効化' },
|
|
167
|
-
{ id: 'safe_test', label: '安全試験', description: '隔離実行と完了条件の検証' },
|
|
168
|
-
{ id: 'runs', label: '実行履歴', description: '起動理由・段階・成果物・再開地点' },
|
|
169
|
-
]);
|
|
170
|
-
|
|
171
|
-
const READBACK_STATES = new Set(['verified', 'active', 'completed_verified', 'readback_verified']);
|
|
172
|
-
|
|
173
|
-
// These are the operational surfaces of the Mana workspace. Codex and
|
|
174
|
-
// Claude Code are intentionally not listed here: their own applications are
|
|
175
|
-
// the source of truth for their plug-in connections. Mana needs the shared
|
|
176
|
-
// inventory because its runtime authority is only visible from this module.
|
|
177
|
-
const MANA_VIEWS = Object.freeze([
|
|
178
|
-
{ id: 'overview', label: '委任一覧', description: '委任ごとの状態と概要' },
|
|
179
|
-
{ id: 'connections', label: '接続状態', description: '組織全体の接続と許可リソース' },
|
|
180
|
-
{ id: 'runs', label: '実行履歴', description: '実行工程とreadback' },
|
|
181
|
-
{ id: 'settings', label: 'Mana設定', description: 'runtime既定値と監査' },
|
|
182
|
-
]);
|
|
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).
|
|
186
|
-
const CONNECTOR_DEFINITIONS = Object.freeze([
|
|
187
|
-
{ id: 'github', label: 'GitHub', description: 'コード・ドキュメントの参照', icon: '/icons/mana/brand-github.svg' },
|
|
188
|
-
{ id: 'drive', label: 'Google Drive', description: '資料・成果物の保存', icon: '/icons/mana/brand-google-drive.svg' },
|
|
189
|
-
{ id: 'slack', label: 'Slack', description: '通知・承認・文書イベント', icon: '/icons/mana/brand-slack.svg' },
|
|
190
|
-
{ id: 'brainbase', label: 'Brainbase Graph', description: '知識・判断の参照', icon: '/icons/mana/affiliate.svg' },
|
|
191
|
-
{ id: 'mana', label: 'Mana runtime', description: '委任の実行基盤', icon: '/icons/mana/cpu.svg' },
|
|
192
|
-
]);
|
|
193
|
-
|
|
194
|
-
const CONNECTOR_FOUNDATION_KINDS = Object.freeze({
|
|
195
|
-
github: 'github',
|
|
196
|
-
drive: 'drive',
|
|
197
|
-
slack: 'slack',
|
|
198
|
-
brainbase: 'knowledge',
|
|
199
|
-
mana: 'mana',
|
|
200
|
-
});
|
|
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
|
-
|
|
213
|
-
function hasOwn(value, key) {
|
|
214
|
-
return Boolean(value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, key));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function objectValue(value) {
|
|
218
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function resolveInjected(value) {
|
|
222
|
-
return typeof value === 'function' ? value() : value;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function firstPresent(...values) {
|
|
226
|
-
return values.find((value) => value !== null && value !== undefined && String(value).trim() !== '');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function present(value, fallback = UNKNOWN) {
|
|
230
|
-
if (Array.isArray(value)) {
|
|
231
|
-
const items = value.filter((item) => item !== null && item !== undefined && String(item).trim() !== '');
|
|
232
|
-
return items.length ? items.join(', ') : fallback;
|
|
233
|
-
}
|
|
234
|
-
return value === null || value === undefined || String(value).trim() === '' ? fallback : String(value);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function stringValue(value, fallback = '') {
|
|
238
|
-
return value === null || value === undefined ? fallback : String(value);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function integerValue(value, fallback = null) {
|
|
242
|
-
return Number.isInteger(value) ? value : Number.isInteger(Number(value)) ? Number(value) : fallback;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function projectValue(input) {
|
|
246
|
-
const project = objectValue(resolveInjected(input));
|
|
247
|
-
return {
|
|
248
|
-
// Keep the canonical project payload intact. The UI only reads a small
|
|
249
|
-
// subset today, but foundation values/resources are needed to render the
|
|
250
|
-
// connection inventory without guessing a connection is healthy.
|
|
251
|
-
...project,
|
|
252
|
-
code: firstPresent(project.code, project.project_code, project.projectCode) ?? null,
|
|
253
|
-
name: firstPresent(project.name, project.display_name, project.displayName) ?? null,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function sessionValue(input) {
|
|
258
|
-
const session = objectValue(resolveInjected(input));
|
|
259
|
-
const permissions = objectValue(session.permissions);
|
|
260
|
-
return {
|
|
261
|
-
role: firstPresent(session.role, session.project_role, permissions.role) ?? null,
|
|
262
|
-
actorId: firstPresent(session.actor_id, session.actorId, session.person_id, session.personId, session.slack_user_id) ?? null,
|
|
263
|
-
displayName: firstPresent(session.display_name, session.displayName, session.name) ?? null,
|
|
264
|
-
outcomePermission: firstPresent(session.outcome_permission, permissions.outcomes, permissions.outcome_delegation) ?? null,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function normalizeStatus(value) {
|
|
269
|
-
const status = stringValue(value).trim().toLowerCase().replace(/[ -]+/g, '_');
|
|
270
|
-
return status || null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function normalizeConnector(value, fallbackId = '') {
|
|
274
|
-
const raw = value;
|
|
275
|
-
const source = objectValue(value);
|
|
276
|
-
const resources = Array.isArray(source.resources)
|
|
277
|
-
? source.resources
|
|
278
|
-
: Array.isArray(source.allowedResources)
|
|
279
|
-
? source.allowedResources
|
|
280
|
-
: Array.isArray(source.allowed_resources) ? source.allowed_resources : [];
|
|
281
|
-
return {
|
|
282
|
-
raw,
|
|
283
|
-
id: stringValue(firstPresent(source.id, source.connectorId, source.connector_id, fallbackId), fallbackId),
|
|
284
|
-
label: stringValue(firstPresent(source.label, source.name, source.provider, fallbackId), fallbackId),
|
|
285
|
-
description: stringValue(source.description, ''),
|
|
286
|
-
// A registered resource may have no status. Keep that distinction so
|
|
287
|
-
// the screen can show 未確認 instead of inferring a live connection.
|
|
288
|
-
status: normalizeStatus(firstPresent(source.status, source.state, source.connectionState, source.connection_state)),
|
|
289
|
-
account: firstPresent(source.account, source.accountId, source.account_id, source.identifier, source.url) ?? null,
|
|
290
|
-
resourceCount: integerValue(firstPresent(source.resourceCount, source.resource_count), resources.length || null),
|
|
291
|
-
resources,
|
|
292
|
-
checkedAt: firstPresent(source.checkedAt, source.checked_at, source.updatedAt, source.updated_at) ?? null,
|
|
293
|
-
reason: firstPresent(source.reason, source.error, source.connectionReason, source.connection_reason) ?? null,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
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
|
-
|
|
311
|
-
export function normalizeApprovalTarget(value, fallbackContractVersion = null) {
|
|
312
|
-
const source = objectValue(value);
|
|
313
|
-
const effect = objectValue(source.effect);
|
|
314
|
-
const payload = effect.payload;
|
|
315
|
-
const payloadObject = objectValue(payload);
|
|
316
|
-
const operation = firstPresent(source.operation, effect.operation);
|
|
317
|
-
const resource = firstPresent(source.resource, effect.resource);
|
|
318
|
-
const destination = firstPresent(
|
|
319
|
-
source.destination, source.external_destination, source.externalDestination,
|
|
320
|
-
payloadObject.destination, payloadObject.external_destination, payloadObject.externalDestination,
|
|
321
|
-
resource,
|
|
322
|
-
);
|
|
323
|
-
const diff = firstPresent(
|
|
324
|
-
source.diff, source.change_summary, source.changeSummary, source.changes,
|
|
325
|
-
payloadObject.diff, payloadObject.change_summary, payloadObject.changeSummary, payloadObject.changes,
|
|
326
|
-
payload,
|
|
327
|
-
);
|
|
328
|
-
const target = {
|
|
329
|
-
approvalId: stringValue(firstPresent(source.approvalId, source.approval_id), ''),
|
|
330
|
-
operation: stringValue(operation, ''),
|
|
331
|
-
resource: stringValue(resource, ''),
|
|
332
|
-
payloadHash: stringValue(firstPresent(source.payloadHash, source.payload_hash), ''),
|
|
333
|
-
destination: destination ?? null,
|
|
334
|
-
contractVersion: integerValue(firstPresent(source.contractVersion, source.contract_version, fallbackContractVersion), null),
|
|
335
|
-
expiresAt: firstPresent(source.expiresAt, source.expires_at) ?? null,
|
|
336
|
-
diff: diff ?? null,
|
|
337
|
-
};
|
|
338
|
-
return {
|
|
339
|
-
...target,
|
|
340
|
-
complete: Boolean(target.approvalId && target.operation && target.resource && target.payloadHash
|
|
341
|
-
&& target.destination && target.contractVersion !== null && target.expiresAt && target.diff),
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function approvalDisplayValue(value) {
|
|
346
|
-
if (value === null || value === undefined || value === '') return UNKNOWN;
|
|
347
|
-
if (typeof value === 'object') {
|
|
348
|
-
try { return JSON.stringify(value); } catch { return UNKNOWN; }
|
|
349
|
-
}
|
|
350
|
-
return String(value);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function statusLabel(value) {
|
|
354
|
-
const status = normalizeStatus(value);
|
|
355
|
-
return STATUS_LABELS[status] ?? (status ? `状態: ${status}` : UNKNOWN);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function authorityLabel(value) {
|
|
359
|
-
return AUTHORITY_LABELS[normalizeStatus(value)] ?? UNKNOWN;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function triggerLabel(value) {
|
|
363
|
-
const source = objectValue(value);
|
|
364
|
-
const normalized = normalizeStatus(typeof value === 'object' ? firstPresent(source.type, source.triggerType, source.trigger_type) : value);
|
|
365
|
-
return TRIGGER_LABELS[normalized] ?? UNKNOWN;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function runReasonLabel(reason, trigger) {
|
|
369
|
-
const normalized = normalizeStatus(reason);
|
|
370
|
-
if (TRIGGER_LABELS[normalized]) return TRIGGER_LABELS[normalized];
|
|
371
|
-
if (typeof reason === 'string' && reason.trim() && !/^[a-z0-9_]+$/i.test(reason.trim())) return reason.trim();
|
|
372
|
-
return triggerLabel(trigger);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function dateLabel(value) {
|
|
376
|
-
if (!value) return UNKNOWN;
|
|
377
|
-
const date = new Date(value);
|
|
378
|
-
if (Number.isNaN(date.getTime())) return UNKNOWN;
|
|
379
|
-
return new Intl.DateTimeFormat('ja-JP', {
|
|
380
|
-
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
381
|
-
}).format(date);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function idempotencyKey(prefix) {
|
|
385
|
-
const generated = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
386
|
-
return `${prefix}-${generated}`;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function normalizeError(value) {
|
|
390
|
-
const source = value instanceof Error ? value : objectValue(value);
|
|
391
|
-
const nested = objectValue(source.error);
|
|
392
|
-
const status = integerValue(source.status ?? source.statusCode ?? source.response?.status, null);
|
|
393
|
-
const code = stringValue(firstPresent(source.code, nested.code, typeof source.error === 'string' ? source.error : null, status ? `http_${status}` : null), 'unknown_error');
|
|
394
|
-
const message = stringValue(firstPresent(source.message, nested.message), code);
|
|
395
|
-
const lower = `${code} ${message}`.toLowerCase();
|
|
396
|
-
const permission = status === 401 || status === 403 || /permission|forbidden|not_allowed|scope_mismatch|unauthori/.test(lower);
|
|
397
|
-
const conflict = status === 409 || /conflict|version|stale/.test(lower);
|
|
398
|
-
return {
|
|
399
|
-
code,
|
|
400
|
-
message,
|
|
401
|
-
status,
|
|
402
|
-
kind: permission ? 'permission_denied' : conflict ? 'conflict' : 'error_retryable',
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function collectionFrom(payload, keys = ['items', 'records', 'contracts', 'delegations', 'runs']) {
|
|
407
|
-
if (Array.isArray(payload)) return payload;
|
|
408
|
-
const object = objectValue(payload);
|
|
409
|
-
for (const key of keys) if (Array.isArray(object[key])) return object[key];
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function explicitEmpty(payload, keys) {
|
|
414
|
-
if (Array.isArray(payload)) return payload.length === 0;
|
|
415
|
-
const object = objectValue(payload);
|
|
416
|
-
if (object.state === EMPTY || object.state === 'empty') return true;
|
|
417
|
-
const collection = collectionFrom(payload, keys);
|
|
418
|
-
return Array.isArray(collection) && collection.length === 0;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function isReadbackVerified(payload) {
|
|
422
|
-
const object = objectValue(payload);
|
|
423
|
-
const readback = objectValue(object.readback ?? object.verification);
|
|
424
|
-
const contract = objectValue(object.contract ?? object.delegation);
|
|
425
|
-
const run = objectValue(object.run ?? object.testRun);
|
|
426
|
-
return object.verified === true || object.readback_verified === true || readback.verified === true
|
|
427
|
-
|| READBACK_STATES.has(normalizeStatus(object.state)) || READBACK_STATES.has(normalizeStatus(readback.state))
|
|
428
|
-
|| READBACK_STATES.has(normalizeStatus(contract.status)) || READBACK_STATES.has(normalizeStatus(run.status));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function contractMatchesExpected(contract, { id, version = null, fields = null, status = null } = {}) {
|
|
432
|
-
if (!contract?.contractId || contract.contractId !== id) return false;
|
|
433
|
-
if (version !== null && contract.version !== version) return false;
|
|
434
|
-
if (status && contract.status !== status) return false;
|
|
435
|
-
if (!fields) return true;
|
|
436
|
-
const expected = normalizeContract(fields);
|
|
437
|
-
const source = objectValue(fields);
|
|
438
|
-
const comparisons = [
|
|
439
|
-
['project', 'project_code', 'projectCode'], ['profileId', 'profile_id'], ['outcome'], ['scope'],
|
|
440
|
-
['inputRefs', 'input_refs'], ['knowledgeRefs', 'knowledge_refs'], ['judgmentRefs', 'judgment_refs'],
|
|
441
|
-
['allowedResources', 'allowed_resources'],
|
|
442
|
-
['artifactDestination', 'artifact_destination'], ['completionCriteria', 'completion_criteria'],
|
|
443
|
-
['owner'], ['limits'], ['trigger'],
|
|
444
|
-
];
|
|
445
|
-
return comparisons.every((keys) => {
|
|
446
|
-
if (!keys.some((key) => hasOwn(source, key))) return true;
|
|
447
|
-
const normalizedKey = keys[0];
|
|
448
|
-
return JSON.stringify(contract[normalizedKey]) === JSON.stringify(expected[normalizedKey]);
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function normalizeReference(value) {
|
|
453
|
-
const reference = objectValue(value);
|
|
454
|
-
return {
|
|
455
|
-
id: stringValue(firstPresent(reference.id, reference.referenceId, reference.reference_id)),
|
|
456
|
-
version: reference.version === null ? null : stringValue(firstPresent(reference.version, reference.revision), ''),
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function normalizeCriterion(value, index = 0) {
|
|
461
|
-
const criterion = objectValue(value);
|
|
462
|
-
return {
|
|
463
|
-
id: stringValue(firstPresent(criterion.id, criterion.criterionId), `criterion-${index + 1}`),
|
|
464
|
-
description: stringValue(firstPresent(criterion.description, criterion.label), ''),
|
|
465
|
-
kind: stringValue(firstPresent(criterion.kind), 'content_includes'),
|
|
466
|
-
expected: criterion.expected,
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function normalizeTrigger(value) {
|
|
471
|
-
const trigger = objectValue(value);
|
|
472
|
-
const type = normalizeStatus(firstPresent(trigger.type, trigger.trigger_type));
|
|
473
|
-
return {
|
|
474
|
-
type: ['manual', 'schedule', 'document'].includes(type) ? type : null,
|
|
475
|
-
scheduleId: stringValue(firstPresent(trigger.scheduleId, trigger.schedule_id), ''),
|
|
476
|
-
subscriptionId: stringValue(firstPresent(trigger.subscriptionId, trigger.subscription_id), ''),
|
|
477
|
-
provider: stringValue(trigger.provider, ''),
|
|
478
|
-
state: normalizeStatus(firstPresent(trigger.state, trigger.status, trigger.connection_state)) ?? null,
|
|
479
|
-
reason: stringValue(firstPresent(trigger.reason, trigger.error, trigger.connection_reason), ''),
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function normalizeContract(value) {
|
|
484
|
-
const raw = objectValue(value);
|
|
485
|
-
const source = objectValue(raw.contract ?? raw.delegation ?? raw);
|
|
486
|
-
const destination = objectValue(source.artifactDestination ?? source.artifact_destination);
|
|
487
|
-
const owner = objectValue(source.owner);
|
|
488
|
-
const limits = objectValue(source.limits);
|
|
489
|
-
const references = (key, snakeKey) => {
|
|
490
|
-
const refs = source[key] ?? source[snakeKey];
|
|
491
|
-
return Array.isArray(refs) ? refs.map(normalizeReference) : [];
|
|
492
|
-
};
|
|
493
|
-
const criteria = source.completionCriteria ?? source.completion_criteria;
|
|
494
|
-
const allowedResources = source.allowedResources ?? source.allowed_resources;
|
|
495
|
-
return {
|
|
496
|
-
raw: value,
|
|
497
|
-
schemaVersion: stringValue(firstPresent(source.schemaVersion, source.schema_version), OUTCOME_DELEGATION_CONTRACT_VERSION),
|
|
498
|
-
contractId: stringValue(firstPresent(source.contractId, source.contract_id, source.id), ''),
|
|
499
|
-
version: integerValue(source.version, null),
|
|
500
|
-
tenantId: null,
|
|
501
|
-
project: stringValue(firstPresent(source.project, source.project_code, source.projectCode), ''),
|
|
502
|
-
status: normalizeStatus(source.status),
|
|
503
|
-
nextAction: stringValue(firstPresent(source.nextAction, source.next_action), ''),
|
|
504
|
-
profileId: stringValue(firstPresent(source.profileId, source.profile_id), ''),
|
|
505
|
-
outcome: stringValue(source.outcome, ''),
|
|
506
|
-
scope: stringValue(source.scope, ''),
|
|
507
|
-
inputRefs: references('inputRefs', 'input_refs'),
|
|
508
|
-
knowledgeRefs: references('knowledgeRefs', 'knowledge_refs'),
|
|
509
|
-
judgmentRefs: references('judgmentRefs', 'judgment_refs'),
|
|
510
|
-
allowedResources: Array.isArray(allowedResources) ? allowedResources : [],
|
|
511
|
-
artifactDestination: {
|
|
512
|
-
adapterId: stringValue(firstPresent(destination.adapterId, destination.adapter_id), ''),
|
|
513
|
-
location: stringValue(destination.location, ''),
|
|
514
|
-
environment: ['production', 'isolated'].includes(stringValue(destination.environment)) ? destination.environment : null,
|
|
515
|
-
},
|
|
516
|
-
completionCriteria: Array.isArray(criteria) ? criteria.map(normalizeCriterion) : [],
|
|
517
|
-
owner: { actorId: stringValue(firstPresent(owner.actorId, owner.actor_id), '') },
|
|
518
|
-
trigger: normalizeTrigger(source.trigger),
|
|
519
|
-
limits: {
|
|
520
|
-
maxAttempts: integerValue(limits.maxAttempts ?? limits.max_attempts, null),
|
|
521
|
-
timeoutMs: integerValue(limits.timeoutMs ?? limits.timeout_ms, null),
|
|
522
|
-
budgetUnits: integerValue(limits.budgetUnits ?? limits.budget_units, null),
|
|
523
|
-
},
|
|
524
|
-
createdBy: stringValue(firstPresent(source.createdBy, source.created_by), ''),
|
|
525
|
-
createdAt: source.createdAt ?? source.created_at ?? null,
|
|
526
|
-
updatedAt: source.updatedAt ?? source.updated_at ?? null,
|
|
527
|
-
supersedesVersion: integerValue(source.supersedesVersion ?? source.supersedes_version, null),
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
export function normalizeContracts(payload) {
|
|
532
|
-
const records = collectionFrom(payload, ['items', 'records', 'contracts', 'delegations']);
|
|
533
|
-
if (!records) return { state: 'unknown', records: [], raw: payload };
|
|
534
|
-
return { state: records.length ? 'ready' : 'empty', records: records.map(normalizeContract), raw: payload };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function normalizeAuthority(value) {
|
|
538
|
-
const source = objectValue(value);
|
|
539
|
-
const entries = collectionFrom(source, ['matrix', 'rules', 'items', 'entries', 'authorities']);
|
|
540
|
-
if (!entries) return { state: 'unknown', status: 'unknown', matrix: [], raw: value };
|
|
541
|
-
const status = entries.length ? 'ready' : 'empty';
|
|
542
|
-
return {
|
|
543
|
-
state: status,
|
|
544
|
-
status,
|
|
545
|
-
matrix: entries.map((entry, index) => {
|
|
546
|
-
const item = objectValue(entry);
|
|
547
|
-
return {
|
|
548
|
-
id: stringValue(firstPresent(item.id, item.operationId, item.operation_id), `operation-${index + 1}`),
|
|
549
|
-
operation: stringValue(firstPresent(item.operation, item.action, item.name), UNKNOWN),
|
|
550
|
-
target: stringValue(firstPresent(item.target, item.resource, item.resource_id), UNKNOWN),
|
|
551
|
-
project: stringValue(firstPresent(item.project, item.project_code, item.projectCode), UNKNOWN),
|
|
552
|
-
actor: stringValue(firstPresent(item.actor, item.actor_id, item.actorId), UNKNOWN),
|
|
553
|
-
policy: ['auto', 'approval', 'deny'].includes(normalizeStatus(item.policy ?? item.mode ?? item.effect)) ? normalizeStatus(item.policy ?? item.mode ?? item.effect) : null,
|
|
554
|
-
reason: stringValue(firstPresent(item.reason, item.explanation), ''),
|
|
555
|
-
state: normalizeStatus(item.state),
|
|
556
|
-
};
|
|
557
|
-
}),
|
|
558
|
-
raw: value,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
export function normalizeRun(value) {
|
|
563
|
-
const raw = objectValue(value);
|
|
564
|
-
const source = objectValue(raw.run ?? raw.execution ?? raw.testRun ?? raw.test_run ?? raw);
|
|
565
|
-
const completion = objectValue(source.completion ?? source.completion_result ?? source.completionResult);
|
|
566
|
-
const artifact = objectValue(source.artifact);
|
|
567
|
-
const artifacts = source.artifacts ?? source.artifact_refs ?? source.artifactRefs
|
|
568
|
-
?? (artifact.saveReceipt ? [artifact.saveReceipt] : []);
|
|
569
|
-
const evidence = source.evidence ?? source.references ?? [];
|
|
570
|
-
const stages = Array.isArray(source.stages) ? source.stages : [];
|
|
571
|
-
const status = normalizeStatus(source.status ?? source.state);
|
|
572
|
-
const safeTestSource = objectValue(source.safeTest ?? source.safe_test ?? raw.safeTest ?? raw.safe_test);
|
|
573
|
-
const configHash = stringValue(firstPresent(
|
|
574
|
-
source.configHash, source.config_hash,
|
|
575
|
-
safeTestSource.configHash, safeTestSource.config_hash,
|
|
576
|
-
), '');
|
|
577
|
-
const triggerSource = source.trigger;
|
|
578
|
-
const trigger = typeof triggerSource === 'string'
|
|
579
|
-
? triggerSource
|
|
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;
|
|
581
|
-
const artifactReadback = source.artifactReadback ?? source.artifact_readback ?? source.readback ?? artifact.readback;
|
|
582
|
-
const judgment = source.judgment ?? source.judgment_result ?? source.judgmentResult;
|
|
583
|
-
const judgmentReason = firstPresent(
|
|
584
|
-
source.judgmentReason, source.judgment_reason,
|
|
585
|
-
objectValue(judgment).reason, source.reason_detail,
|
|
586
|
-
) ?? null;
|
|
587
|
-
const preflightSource = objectValue(source.preflight ?? raw.preflight);
|
|
588
|
-
const preflightChecks = Array.isArray(preflightSource.checks) ? preflightSource.checks : [];
|
|
589
|
-
return {
|
|
590
|
-
raw: value,
|
|
591
|
-
id: stringValue(firstPresent(source.runId, source.run_id, source.id), ''),
|
|
592
|
-
contractId: stringValue(firstPresent(source.contractId, source.contract_id), ''),
|
|
593
|
-
contractVersion: integerValue(source.contractVersion ?? source.contract_version, null),
|
|
594
|
-
profileId: stringValue(firstPresent(source.profileId, source.profile_id), ''),
|
|
595
|
-
mode: normalizeStatus(source.mode),
|
|
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), ''),
|
|
599
|
-
safeTest: {
|
|
600
|
-
configSnapshot: safeTestSource.configSnapshot ?? safeTestSource.config_snapshot ?? null,
|
|
601
|
-
configHash: stringValue(firstPresent(safeTestSource.configHash, safeTestSource.config_hash), ''),
|
|
602
|
-
currentConfigHash: stringValue(firstPresent(safeTestSource.currentConfigHash, safeTestSource.current_config_hash), ''),
|
|
603
|
-
retestRequired: safeTestSource.retestRequired === true || safeTestSource.retest_required === true,
|
|
604
|
-
retestReason: stringValue(firstPresent(safeTestSource.retestReason, safeTestSource.retest_reason), ''),
|
|
605
|
-
},
|
|
606
|
-
configHash,
|
|
607
|
-
trigger,
|
|
608
|
-
reason: stringValue(firstPresent(source.triggerReason, source.trigger_reason, source.reason), ''),
|
|
609
|
-
artifactReadback: artifactReadback === undefined ? null : artifactReadback,
|
|
610
|
-
judgment: judgment === undefined ? null : judgment,
|
|
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
|
-
},
|
|
625
|
-
stage: stringValue(firstPresent(source.stage, source.step, source.phase), ''),
|
|
626
|
-
stages: stages.map((value) => {
|
|
627
|
-
const stage = objectValue(value);
|
|
628
|
-
return {
|
|
629
|
-
id: stringValue(firstPresent(stage.id, stage.stageId, stage.stage_id), ''),
|
|
630
|
-
status: normalizeStatus(stage.status ?? stage.state),
|
|
631
|
-
attempt: integerValue(stage.attempt, null),
|
|
632
|
-
startedAt: stage.startedAt ?? stage.started_at ?? null,
|
|
633
|
-
completedAt: stage.completedAt ?? stage.completed_at ?? null,
|
|
634
|
-
errorCode: stringValue(firstPresent(stage.errorCode, stage.error_code), ''),
|
|
635
|
-
receipt: stage.receipt,
|
|
636
|
-
};
|
|
637
|
-
}),
|
|
638
|
-
resumePoint: stringValue(firstPresent(source.resumePoint, source.resume_point, source.restart_from), ''),
|
|
639
|
-
evidence: Array.isArray(evidence) ? evidence : [],
|
|
640
|
-
artifacts: Array.isArray(artifacts) ? artifacts : [],
|
|
641
|
-
completion: {
|
|
642
|
-
state: normalizeStatus(firstPresent(completion.state, completion.status, source.completion_state)),
|
|
643
|
-
criteria: Array.isArray(completion.criteria) ? completion.criteria : Array.isArray(source.criteria) ? source.criteria : [],
|
|
644
|
-
reason: stringValue(firstPresent(completion.reason, completion.error), ''),
|
|
645
|
-
},
|
|
646
|
-
pendingApproval: normalizeApprovalTarget(source.pendingApproval ?? source.pending_approval, integerValue(source.contractVersion ?? source.contract_version, null)),
|
|
647
|
-
startedAt: source.startedAt ?? source.started_at ?? source.createdAt ?? source.created_at ?? null,
|
|
648
|
-
updatedAt: source.updatedAt ?? source.updated_at ?? null,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
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
|
-
|
|
676
|
-
function safeTestStatus(value, contract) {
|
|
677
|
-
const run = normalizeRun(value);
|
|
678
|
-
const current = objectValue(contract);
|
|
679
|
-
const resultState = completionState(value);
|
|
680
|
-
if (resultState === 'unexecuted') return 'unexecuted';
|
|
681
|
-
if (resultState === 'failed') return 'failed';
|
|
682
|
-
if (resultState !== 'completed_verified') return 'completion_unverified';
|
|
683
|
-
if (run.mode !== 'safe_test' || !run.contractId || run.contractId !== current.contractId) return 'unknown';
|
|
684
|
-
const { configHash, currentConfigHash, retestRequired, retestReason } = run.safeTest;
|
|
685
|
-
if (retestReason === 'configuration_snapshot_missing') return 'configuration_snapshot_missing';
|
|
686
|
-
if (retestRequired || (configHash && currentConfigHash && configHash !== currentConfigHash)) return 'stale';
|
|
687
|
-
if (!configHash || !currentConfigHash) return 'configuration_snapshot_missing';
|
|
688
|
-
return configHash === currentConfigHash ? 'verified' : 'stale';
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
export function normalizeRuns(payload) {
|
|
692
|
-
const records = collectionFrom(payload, ['runs', 'items', 'records']);
|
|
693
|
-
if (!records) return { state: 'unknown', records: [], raw: payload };
|
|
694
|
-
return { state: records.length ? 'ready' : 'empty', records: records.map(normalizeRun), raw: payload };
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
export function completionState(value) {
|
|
698
|
-
const source = objectValue(value);
|
|
699
|
-
const run = objectValue(source.run ?? source.testRun ?? source);
|
|
700
|
-
const state = normalizeStatus(firstPresent(source.completion_state, source.completionState, run.completion_state, run.completionState, run.status, source.status));
|
|
701
|
-
const executed = source.executed === true || source.execution_state === 'executed' || Boolean(firstPresent(source.run_id, source.runId, run.id, run.run_id))
|
|
702
|
-
|| ['queued', 'running', 'waiting_approval', 'failed', 'stopped', 'completion_unverified', 'completed_verified'].includes(state);
|
|
703
|
-
const verified = source.completion_verified === true || source.completionVerified === true
|
|
704
|
-
|| state === 'completed_verified' || objectValue(source.completion).state === 'verified' || objectValue(run.completion).state === 'verified';
|
|
705
|
-
if (!executed) return 'unexecuted';
|
|
706
|
-
if (verified) return 'completed_verified';
|
|
707
|
-
return state === 'failed' ? 'failed' : state === 'stopped' ? 'stopped' : 'completion_unverified';
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function criterionLines(criteria) {
|
|
711
|
-
return (Array.isArray(criteria) ? criteria : []).map((criterion) => {
|
|
712
|
-
const value = normalizeCriterion(criterion);
|
|
713
|
-
const expected = value.expected === undefined ? '' : `\t${String(value.expected)}`;
|
|
714
|
-
return `${value.id}\t${value.description}\t${value.kind}${expected}`;
|
|
715
|
-
}).join('\n');
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function parseReferenceLines(value) {
|
|
719
|
-
return stringValue(value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
720
|
-
const [id, version = ''] = line.split(/\s*(?:@|\t|\|)\s*/, 2);
|
|
721
|
-
return { id: id.trim(), version: version.trim() || null };
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function parseCriteriaLines(value) {
|
|
726
|
-
const text = stringValue(value).trim();
|
|
727
|
-
if (!text) return [];
|
|
728
|
-
try {
|
|
729
|
-
const parsed = JSON.parse(text);
|
|
730
|
-
if (Array.isArray(parsed)) return parsed.map(normalizeCriterion);
|
|
731
|
-
} catch {
|
|
732
|
-
// The line format keeps the form usable for people who do not want to edit JSON.
|
|
733
|
-
}
|
|
734
|
-
return text.split(/\r?\n/).map((line, index) => {
|
|
735
|
-
const [id, description, kind = 'content_includes', expected = ''] = line.split(/\s*(?:\t|\|)\s*/);
|
|
736
|
-
return normalizeCriterion({ id: id?.trim() || `criterion-${index + 1}`, description: description?.trim() || line.trim(), kind: kind?.trim() || 'content_includes', expected: expected?.trim() || undefined }, index);
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function contractDraftFromForm(fields) {
|
|
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.
|
|
744
|
-
const body = {
|
|
745
|
-
...(fields.profileId ? { profileId: fields.profileId.value } : {}),
|
|
746
|
-
outcome: fields.outcome.value,
|
|
747
|
-
scope: fields.scope.value,
|
|
748
|
-
inputRefs: parseReferenceLines(fields.inputRefs.value),
|
|
749
|
-
knowledgeRefs: parseReferenceLines(fields.knowledgeRefs.value),
|
|
750
|
-
judgmentRefs: parseReferenceLines(fields.judgmentRefs.value),
|
|
751
|
-
...(fields.allowedResources ? { allowedResources: stringValue(fields.allowedResources.value).split(/\r?\n/).map((line) => line.trim()).filter(Boolean) } : {}),
|
|
752
|
-
artifactDestination: {
|
|
753
|
-
adapterId: fields.adapterId.value,
|
|
754
|
-
location: fields.location.value,
|
|
755
|
-
environment: fields.environment.value,
|
|
756
|
-
},
|
|
757
|
-
completionCriteria: parseCriteriaLines(criterionText),
|
|
758
|
-
owner: { actorId: fields.ownerActorId.value },
|
|
759
|
-
trigger: { type: 'manual' },
|
|
760
|
-
limits: {
|
|
761
|
-
maxAttempts: Number(fields.maxAttempts.value),
|
|
762
|
-
timeoutMs: Number(fields.timeoutMs.value),
|
|
763
|
-
budgetUnits: Number(fields.budgetUnits.value),
|
|
764
|
-
},
|
|
765
|
-
};
|
|
766
|
-
return body;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function createElement(document, tag, options = {}) {
|
|
770
|
-
const element = document.createElement(tag);
|
|
771
|
-
if (options.className) element.className = options.className;
|
|
772
|
-
if (options.text !== undefined) element.textContent = String(options.text);
|
|
773
|
-
for (const [name, value] of Object.entries(options.attrs ?? {})) {
|
|
774
|
-
if (value === undefined || value === null) continue;
|
|
775
|
-
element.setAttribute(name, String(value));
|
|
776
|
-
}
|
|
777
|
-
return element;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function clearElement(element) {
|
|
781
|
-
if (!element) return;
|
|
782
|
-
if (typeof element.replaceChildren === 'function') element.replaceChildren();
|
|
783
|
-
else if (Array.isArray(element.children)) element.children.length = 0;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function listen(element, type, handler) {
|
|
787
|
-
if (element && typeof element.addEventListener === 'function') element.addEventListener(type, handler);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function focusElement(element) {
|
|
791
|
-
if (element && typeof element.focus === 'function') element.focus();
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function button(document, text, handler, options = {}) {
|
|
795
|
-
const item = createElement(document, 'button', {
|
|
796
|
-
className: options.className ?? 'outcome-mana-button',
|
|
797
|
-
text,
|
|
798
|
-
attrs: { type: 'button', ...(options.attrs ?? {}) },
|
|
799
|
-
});
|
|
800
|
-
if (options.disabled) item.disabled = true;
|
|
801
|
-
listen(item, 'click', (event) => { event?.preventDefault?.(); handler?.(item, event); });
|
|
802
|
-
return item;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function group(document, tag, options = {}, children = []) {
|
|
806
|
-
const element = createElement(document, tag, options);
|
|
807
|
-
element.append(...children.filter(Boolean));
|
|
808
|
-
return element;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function connectorIdentity(document, connector, options = {}) {
|
|
812
|
-
const identity = createElement(document, options.tag ?? 'span', {
|
|
813
|
-
className: `outcome-mana-connector-identity outcome-mana-connector-${connector.id}${options.large ? ' is-large' : ''}`,
|
|
814
|
-
attrs: options.attrs,
|
|
815
|
-
});
|
|
816
|
-
identity.append(
|
|
817
|
-
group(document, 'span', { className: 'outcome-mana-connector-icon-frame', attrs: { 'aria-hidden': 'true' } }, [
|
|
818
|
-
createElement(document, 'img', {
|
|
819
|
-
className: 'outcome-mana-connector-icon',
|
|
820
|
-
attrs: { src: connector.icon, alt: '', width: options.large ? '24' : '20', height: options.large ? '24' : '20' },
|
|
821
|
-
}),
|
|
822
|
-
]),
|
|
823
|
-
group(document, 'span', { className: 'outcome-mana-connector-copy' }, [
|
|
824
|
-
createElement(document, options.large ? 'h3' : 'strong', { text: connector.label }),
|
|
825
|
-
options.large ? createElement(document, 'small', { text: connector.description }) : null,
|
|
826
|
-
]),
|
|
827
|
-
);
|
|
828
|
-
return identity;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function statusBadge(document, status, label = statusLabel(status)) {
|
|
832
|
-
const normalized = normalizeStatus(status) ?? 'unknown';
|
|
833
|
-
return createElement(document, 'span', {
|
|
834
|
-
className: `outcome-mana-badge outcome-mana-badge-${normalized}`,
|
|
835
|
-
text: label,
|
|
836
|
-
attrs: { 'data-state': normalized },
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function stateMessage(document, status, options = {}) {
|
|
841
|
-
const state = status ?? 'unknown';
|
|
842
|
-
if (state === 'loading') {
|
|
843
|
-
const item = createElement(document, 'div', { className: 'outcome-mana-state outcome-mana-loading', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
844
|
-
item.append(createElement(document, 'span', { className: 'outcome-mana-skeleton', attrs: { 'aria-hidden': 'true' } }), createElement(document, 'span', { text: options.loadingText ?? '読み込んでいます…' }));
|
|
845
|
-
return item;
|
|
846
|
-
}
|
|
847
|
-
if (state === 'empty') {
|
|
848
|
-
const item = createElement(document, 'div', { className: 'outcome-mana-state outcome-mana-empty', attrs: { role: 'status' } });
|
|
849
|
-
item.append(createElement(document, 'strong', { text: options.emptyTitle ?? '登録された内容はありません' }), createElement(document, 'p', { text: options.emptyText ?? 'ここから登録できます。' }));
|
|
850
|
-
return item;
|
|
851
|
-
}
|
|
852
|
-
if (state === 'permission_denied') {
|
|
853
|
-
const item = createElement(document, 'div', { className: 'outcome-mana-state outcome-mana-permission', attrs: { role: 'alert' } });
|
|
854
|
-
item.append(createElement(document, 'strong', { text: '権限がありません' }), createElement(document, 'p', { text: options.permissionText ?? 'このプロジェクトの委任設定を確認する権限がありません。現在の権限を確認してください。' }));
|
|
855
|
-
return item;
|
|
856
|
-
}
|
|
857
|
-
if (state === 'error_retryable' || state === 'conflict' || state === 'unknown') {
|
|
858
|
-
const item = createElement(document, 'div', { className: `outcome-mana-state outcome-mana-error outcome-mana-error-${state}`, attrs: { role: 'alert' } });
|
|
859
|
-
item.append(createElement(document, 'strong', { text: state === 'unknown' ? '状態を確認できません' : state === 'conflict' ? '別の更新を確認しました' : '読み込みを確認できません' }));
|
|
860
|
-
if (options.message) item.append(createElement(document, 'p', { text: options.message }));
|
|
861
|
-
if (options.retry) item.append(button(document, options.retryLabel ?? '再試行', options.retry, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
|
|
862
|
-
return item;
|
|
863
|
-
}
|
|
864
|
-
return null;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function definitionList(document, entries) {
|
|
868
|
-
const list = createElement(document, 'dl', { className: 'outcome-mana-definition' });
|
|
869
|
-
for (const [term, value] of entries) {
|
|
870
|
-
list.append(createElement(document, 'dt', { text: term }), createElement(document, 'dd', { text: present(value) }));
|
|
871
|
-
}
|
|
872
|
-
return list;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function field(document, labelText, type, name, value, options = {}) {
|
|
876
|
-
const label = createElement(document, 'label', { className: 'outcome-mana-field' });
|
|
877
|
-
label.append(createElement(document, 'span', { className: 'outcome-mana-label', text: labelText }));
|
|
878
|
-
const input = createElement(document, type === 'textarea' ? 'textarea' : type === 'select' ? 'select' : 'input', {
|
|
879
|
-
className: 'outcome-mana-input',
|
|
880
|
-
attrs: { name, id: options.id ?? `outcome-mana-${name}`, ...(options.attrs ?? {}) },
|
|
881
|
-
});
|
|
882
|
-
if (type === 'select') {
|
|
883
|
-
for (const option of options.options ?? []) input.append(createElement(document, 'option', { text: option.label, attrs: { value: option.value } }));
|
|
884
|
-
}
|
|
885
|
-
if (type === 'textarea') input.textContent = stringValue(value);
|
|
886
|
-
else input.value = stringValue(value);
|
|
887
|
-
if (options.helper) label.append(input, createElement(document, 'small', { className: 'outcome-mana-helper', text: options.helper }));
|
|
888
|
-
else label.append(input);
|
|
889
|
-
return { label, input };
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function textOf(value) {
|
|
893
|
-
if (value === null || value === undefined) return UNKNOWN;
|
|
894
|
-
if (typeof value === 'string') return value;
|
|
895
|
-
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
896
|
-
const id = firstPresent(value.label, value.name, value.title, value.identifier, value.url, value.id);
|
|
897
|
-
if (id) return value.version ? `${id} @ ${value.version}` : id;
|
|
898
|
-
}
|
|
899
|
-
try { return JSON.stringify(value, null, 2); } catch { return UNKNOWN; }
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function evidenceList(document, title, values) {
|
|
903
|
-
const section = createElement(document, 'div', { className: 'outcome-mana-evidence' });
|
|
904
|
-
section.append(createElement(document, 'h4', { text: title }));
|
|
905
|
-
if (!Array.isArray(values) || !values.length) {
|
|
906
|
-
section.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: UNKNOWN }));
|
|
907
|
-
return section;
|
|
908
|
-
}
|
|
909
|
-
const list = createElement(document, 'ul');
|
|
910
|
-
values.forEach((value) => list.append(createElement(document, 'li', { text: textOf(value) })));
|
|
911
|
-
section.append(list);
|
|
912
|
-
return section;
|
|
913
|
-
}
|
|
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
|
-
|
|
930
|
-
function stageList(document, stages) {
|
|
931
|
-
const section = createElement(document, 'div', { className: 'outcome-mana-evidence' });
|
|
932
|
-
section.append(createElement(document, 'h4', { text: '処理段階' }));
|
|
933
|
-
if (!Array.isArray(stages) || !stages.length) {
|
|
934
|
-
section.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: UNKNOWN }));
|
|
935
|
-
return section;
|
|
936
|
-
}
|
|
937
|
-
const rows = stages.map((stage) => {
|
|
938
|
-
const details = [statusLabel(stage.status)];
|
|
939
|
-
if (stage.attempt !== null) details.push(`試行 ${stage.attempt}`);
|
|
940
|
-
if (stage.errorCode) details.push(`エラー: ${stage.errorCode}`);
|
|
941
|
-
return [`${STAGE_LABELS[stage.id] ?? stage.id} (${stage.id || UNKNOWN})`, details.join(' / ')];
|
|
942
|
-
});
|
|
943
|
-
section.append(definitionList(document, rows));
|
|
944
|
-
return section;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function sectionHeading(document, title, status, description, id) {
|
|
948
|
-
const head = createElement(document, 'div', { className: 'outcome-mana-section-heading' });
|
|
949
|
-
const copy = createElement(document, 'div');
|
|
950
|
-
copy.append(createElement(document, 'h2', { text: title, attrs: id ? { id } : {} }), createElement(document, 'p', { className: 'outcome-mana-section-description', text: description }));
|
|
951
|
-
head.append(copy);
|
|
952
|
-
if (status) head.append(statusBadge(document, status));
|
|
953
|
-
return head;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function defaultPathMap(basePath, projectCode) {
|
|
957
|
-
const root = String(basePath || '/api/outcome-delegations').replace(/\/$/, '');
|
|
958
|
-
const query = projectCode ? `?project_code=${encodeURIComponent(projectCode)}` : '';
|
|
959
|
-
return {
|
|
960
|
-
settings: `${root}/settings${query}`,
|
|
961
|
-
contractList: `${root}${query}`,
|
|
962
|
-
triggerCatalog: (type) => {
|
|
963
|
-
const search = new URLSearchParams(projectCode ? { project_code: projectCode } : {});
|
|
964
|
-
if (type) search.set('type', type);
|
|
965
|
-
return `${root}/triggers${search.size ? `?${search}` : ''}`;
|
|
966
|
-
},
|
|
967
|
-
contractDetail: (contractId) => `${root}/${encodeURIComponent(contractId)}${query}`,
|
|
968
|
-
contractClone: (contractId) => `${root}/${encodeURIComponent(contractId)}/clone${query}`,
|
|
969
|
-
contractRetire: (contractId) => `${root}/${encodeURIComponent(contractId)}/retire${query}`,
|
|
970
|
-
authority: (contractId, params = {}) => {
|
|
971
|
-
const search = new URLSearchParams(projectCode ? { project_code: projectCode } : {});
|
|
972
|
-
if (Number.isInteger(params.contractVersion)) search.set('contractVersion', String(params.contractVersion));
|
|
973
|
-
return `${root}/${encodeURIComponent(contractId)}/authority${search.size ? `?${search}` : ''}`;
|
|
974
|
-
},
|
|
975
|
-
triggers: (contractId) => `${root}/${encodeURIComponent(contractId)}/triggers${query}`,
|
|
976
|
-
tests: (contractId) => `${root}/${encodeURIComponent(contractId)}/tests${query}`,
|
|
977
|
-
activation: (contractId) => `${root}/${encodeURIComponent(contractId)}/activation${query}`,
|
|
978
|
-
runList: (contractId) => `${root}/${encodeURIComponent(contractId)}/runs${query}`,
|
|
979
|
-
runDetail: (runId) => `${root}/runs/${encodeURIComponent(runId)}${query}`,
|
|
980
|
-
decisions: (runId) => `${root}/runs/${encodeURIComponent(runId)}/decisions${query}`,
|
|
981
|
-
resume: (runId) => `${root}/runs/${encodeURIComponent(runId)}/resume${query}`,
|
|
982
|
-
runStop: (runId) => `${root}/runs/${encodeURIComponent(runId)}/stop${query}`,
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
export function createManaPaths(options = {}) {
|
|
987
|
-
return defaultPathMap(options.basePath ?? '/api/outcome-delegations', options.projectCode ?? options.project_code ?? null);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function resolvePath(pathMap, name, params) {
|
|
991
|
-
const value = pathMap[name];
|
|
992
|
-
return typeof value === 'function' ? value(params?.id ?? params?.contractId ?? params?.runId, params) : value;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function canManage(session) {
|
|
996
|
-
const permission = normalizeStatus(session.outcomePermission);
|
|
997
|
-
if (['deny', 'denied', 'false', 'none'].includes(permission) || session.outcomePermission === false) return false;
|
|
998
|
-
return true;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function contractFormValues(contract, draft) {
|
|
1002
|
-
const source = draft ?? contract ?? normalizeContract({});
|
|
1003
|
-
return {
|
|
1004
|
-
profileId: source.profileId ?? '',
|
|
1005
|
-
outcome: source.outcome ?? '',
|
|
1006
|
-
scope: source.scope ?? '',
|
|
1007
|
-
inputRefs: (source.inputRefs ?? []).map((item) => `${item.id}${item.version ? `@${item.version}` : ''}`).join('\n'),
|
|
1008
|
-
knowledgeRefs: (source.knowledgeRefs ?? []).map((item) => `${item.id}${item.version ? `@${item.version}` : ''}`).join('\n'),
|
|
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'),
|
|
1011
|
-
adapterId: source.artifactDestination?.adapterId ?? '',
|
|
1012
|
-
location: source.artifactDestination?.location ?? '',
|
|
1013
|
-
environment: source.artifactDestination?.environment ?? 'isolated',
|
|
1014
|
-
completionCriteria: criterionLines(source.completionCriteria),
|
|
1015
|
-
ownerActorId: source.owner?.actorId ?? '',
|
|
1016
|
-
maxAttempts: source.limits?.maxAttempts ?? '',
|
|
1017
|
-
timeoutMs: source.limits?.timeoutMs ?? '',
|
|
1018
|
-
budgetUnits: source.limits?.budgetUnits ?? '',
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function contractInput(contract, overrides = {}) {
|
|
1023
|
-
return {
|
|
1024
|
-
profileId: contract.profileId ?? '',
|
|
1025
|
-
outcome: contract.outcome,
|
|
1026
|
-
scope: contract.scope,
|
|
1027
|
-
inputRefs: contract.inputRefs,
|
|
1028
|
-
knowledgeRefs: contract.knowledgeRefs,
|
|
1029
|
-
judgmentRefs: contract.judgmentRefs,
|
|
1030
|
-
allowedResources: contract.allowedResources ?? [],
|
|
1031
|
-
artifactDestination: contract.artifactDestination,
|
|
1032
|
-
completionCriteria: contract.completionCriteria,
|
|
1033
|
-
owner: contract.owner,
|
|
1034
|
-
trigger: contract.trigger?.type ? {
|
|
1035
|
-
type: contract.trigger.type,
|
|
1036
|
-
...(contract.trigger.type === 'schedule' ? { scheduleId: contract.trigger.scheduleId } : {}),
|
|
1037
|
-
...(contract.trigger.type === 'document' ? { subscriptionId: contract.trigger.subscriptionId, provider: contract.trigger.provider } : {}),
|
|
1038
|
-
} : { type: 'manual' },
|
|
1039
|
-
limits: contract.limits,
|
|
1040
|
-
...overrides,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function triggerFromContract(contract, draft) {
|
|
1045
|
-
return normalizeTrigger(draft?.trigger ?? contract?.trigger);
|
|
1046
|
-
}
|
|
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
|
-
*/
|
|
1116
|
-
export function createManaOutcomeUI(options = {}) {
|
|
1117
|
-
const document = options.document ?? globalThis.document;
|
|
1118
|
-
if (!document) throw new Error('outcome_mana_document_unavailable');
|
|
1119
|
-
const root = options.root ?? options.container ?? null;
|
|
1120
|
-
const api = options.api;
|
|
1121
|
-
const apiMutation = options.apiMutation;
|
|
1122
|
-
const paths = options.paths ?? {};
|
|
1123
|
-
const pathBuilder = typeof options.pathFor === 'function' ? options.pathFor : null;
|
|
1124
|
-
const projectInput = options.project;
|
|
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;
|
|
1136
|
-
const state = {
|
|
1137
|
-
// null means no user choice has been made yet. render() derives a useful
|
|
1138
|
-
// first step from the state so existing deep links/readbacks land on the
|
|
1139
|
-
// relevant workspace; clicks then remain the source of truth.
|
|
1140
|
-
activeView: null,
|
|
1141
|
-
delegationTab: 'settings',
|
|
1142
|
-
activeStage: null,
|
|
1143
|
-
contract: { status: 'loading', records: [], selectedId: null, detail: null, error: null, formDraft: null, pendingSave: null },
|
|
1144
|
-
authority: { status: 'idle', matrix: [], raw: null, error: null, draft: null },
|
|
1145
|
-
triggers: { status: 'idle', trigger: null, error: null, draft: null, catalogStatus: 'idle', schedules: [], subscriptions: [], catalogError: null },
|
|
1146
|
-
safeTest: { status: 'idle', result: null, runId: null, contractId: null, error: null, formDraft: null },
|
|
1147
|
-
activation: { status: 'idle', error: null, message: '' },
|
|
1148
|
-
runs: { status: 'idle', records: [], selectedId: null, detail: null, error: null },
|
|
1149
|
-
runAction: { status: 'idle', error: null, draft: null },
|
|
1150
|
-
connections: { status: 'unknown', records: [], selectedId: null, detail: null, error: null },
|
|
1151
|
-
manaSettings: { status: 'unknown', data: null, error: null, draft: null, saveStatus: 'idle', saveError: null },
|
|
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
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const viewIds = new Set([...MANA_VIEWS.map((view) => view.id), 'delegation_settings']);
|
|
1198
|
-
|
|
1199
|
-
function stageStatus(id) {
|
|
1200
|
-
if (id === 'contract') return state.contract.status;
|
|
1201
|
-
if (id === 'authority') return state.authority.status;
|
|
1202
|
-
if (id === 'triggers') {
|
|
1203
|
-
if (state.triggers.status !== 'idle') return state.triggers.status;
|
|
1204
|
-
if (state.triggers.catalogStatus !== 'idle') return state.triggers.catalogStatus;
|
|
1205
|
-
return currentContract()?.status ?? 'idle';
|
|
1206
|
-
}
|
|
1207
|
-
if (id === 'safe_test') {
|
|
1208
|
-
if (state.safeTest.status === 'loading') return 'loading';
|
|
1209
|
-
return state.safeTest.result ? safeTestStatus(state.safeTest.result, currentContract()) : state.safeTest.status;
|
|
1210
|
-
}
|
|
1211
|
-
if (id === 'runs') return state.runs.status;
|
|
1212
|
-
return 'unknown';
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function derivedActiveStage() {
|
|
1216
|
-
if (state.activeStage && WORKFLOW_STAGES.some((stage) => stage.id === state.activeStage)) return state.activeStage;
|
|
1217
|
-
if (state.triggers.status !== 'idle' || state.triggers.catalogStatus !== 'idle') return 'triggers';
|
|
1218
|
-
// The trigger workspace is where the final activation gates are shown.
|
|
1219
|
-
// Keep it as the landing step when a caller has already loaded the two
|
|
1220
|
-
// prerequisites but has not clicked a stage yet.
|
|
1221
|
-
if (state.authority.status !== 'idle' && state.safeTest.status !== 'idle') return 'triggers';
|
|
1222
|
-
if (state.runs.status !== 'idle') return 'runs';
|
|
1223
|
-
if (state.safeTest.status !== 'idle') return 'safe_test';
|
|
1224
|
-
if (state.authority.status !== 'idle') return 'authority';
|
|
1225
|
-
return 'contract';
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
function setActiveStage(id) {
|
|
1229
|
-
if (WORKFLOW_STAGES.some((stage) => stage.id === id)) {
|
|
1230
|
-
state.activeStage = id;
|
|
1231
|
-
state.delegationTab = 'settings';
|
|
1232
|
-
// The five existing workflow stages are the detail workspace. Keeping
|
|
1233
|
-
// this transition here preserves deep links and existing callers while
|
|
1234
|
-
// the top-level navigation remains focused on the five new surfaces.
|
|
1235
|
-
// Runs also has a top-level operational view. Keep that view selected
|
|
1236
|
-
// while it refreshes or opens a run; explicit workflow-stage navigation
|
|
1237
|
-
// still enters the delegation detail workspace.
|
|
1238
|
-
if (!(id === 'runs' && state.activeView === 'runs')) state.activeView = 'delegation_settings';
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
function setActiveView(id) {
|
|
1243
|
-
if (viewIds.has(id)) state.activeView = id;
|
|
1244
|
-
return state.activeView;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function derivedActiveView() {
|
|
1248
|
-
if (state.activeView && viewIds.has(state.activeView)) return state.activeView;
|
|
1249
|
-
// A detail-only readback is the existing deep-link shape. Render it in
|
|
1250
|
-
// the settings workspace until the user explicitly returns to overview.
|
|
1251
|
-
if (state.contract.detail && !state.contract.records.length) return 'delegation_settings';
|
|
1252
|
-
return 'overview';
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function currentProject() { return projectValue(projectInput); }
|
|
1256
|
-
function currentSession() { return sessionValue(sessionInput); }
|
|
1257
|
-
function currentContract() {
|
|
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;
|
|
1262
|
-
}
|
|
1263
|
-
function contractId() { return state.contract.selectedId || currentContract()?.contractId || null; }
|
|
1264
|
-
function currentPaths() {
|
|
1265
|
-
const generated = defaultPathMap(options.basePath ?? '/api/outcome-delegations', currentProject().code);
|
|
1266
|
-
return { ...generated, ...paths };
|
|
1267
|
-
}
|
|
1268
|
-
function path(name, params = {}) {
|
|
1269
|
-
const context = { ...params, projectCode: currentProject().code, project: currentProject() };
|
|
1270
|
-
if (pathBuilder) return pathBuilder(name, context);
|
|
1271
|
-
return resolvePath(currentPaths(), name, context);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
function connectorSource(project, id) {
|
|
1275
|
-
const foundations = objectValue(project.foundation_values);
|
|
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;
|
|
1281
|
-
const aliases = {
|
|
1282
|
-
brainbase: ['brainbase', 'knowledge'],
|
|
1283
|
-
drive: ['drive', 'google_drive', 'googleDrive'],
|
|
1284
|
-
github: ['github', 'gitHub'],
|
|
1285
|
-
slack: ['slack'],
|
|
1286
|
-
mana: ['mana', 'mana_runtime', 'manaRuntime'],
|
|
1287
|
-
};
|
|
1288
|
-
for (const key of aliases[id] ?? [id]) {
|
|
1289
|
-
if (hasOwn(foundations, key)) return foundations[key];
|
|
1290
|
-
if (hasOwn(resources, key)) return resources[key];
|
|
1291
|
-
if (hasOwn(project, key)) return project[key];
|
|
1292
|
-
}
|
|
1293
|
-
return null;
|
|
1294
|
-
}
|
|
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
|
-
|
|
1346
|
-
function connectorRecordsFromProject() {
|
|
1347
|
-
const project = currentProject();
|
|
1348
|
-
return connectorDefinitions.map((definition) => {
|
|
1349
|
-
const source = connectorSource(project, definition.id);
|
|
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) };
|
|
1354
|
-
const normalized = normalizeConnector(value, definition.id);
|
|
1355
|
-
return {
|
|
1356
|
-
...normalized,
|
|
1357
|
-
label: definition.label,
|
|
1358
|
-
description: definition.description,
|
|
1359
|
-
source,
|
|
1360
|
-
};
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function connectionStatusLabel(status) {
|
|
1365
|
-
const normalized = normalizeStatus(status);
|
|
1366
|
-
if (['connected', 'verified', 'readback_verified'].includes(normalized)) return '接続確認済み';
|
|
1367
|
-
if (['active', 'enabled', 'ready', 'available'].includes(normalized)) return '登録済み(接続未確認)';
|
|
1368
|
-
if (['failed', 'error', 'unavailable'].includes(normalized)) return '確認失敗';
|
|
1369
|
-
if (['unregistered', 'not_registered'].includes(normalized)) return '未登録';
|
|
1370
|
-
if (['unconfigured', 'misconfigured'].includes(normalized)) return '確認設定なし';
|
|
1371
|
-
if (['permission_denied', 'forbidden', 'unauthorized'].includes(normalized)) return '権限不足';
|
|
1372
|
-
if (['authorization_required', 'required_scope_missing'].includes(normalized)) return '追加権限が必要';
|
|
1373
|
-
if (['timeout', 'timed_out'].includes(normalized)) return 'タイムアウト';
|
|
1374
|
-
if (['rejected', 'unconfirmed', 'readback_mismatch'].includes(normalized)) return '確認できません';
|
|
1375
|
-
return UNKNOWN;
|
|
1376
|
-
}
|
|
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
|
-
|
|
1387
|
-
function connectionReasonLabel(reason) {
|
|
1388
|
-
const value = stringValue(reason).trim();
|
|
1389
|
-
const normalized = normalizeStatus(value);
|
|
1390
|
-
const labels = {
|
|
1391
|
-
connection_unregistered: '接続先が登録されていません',
|
|
1392
|
-
foundation_provider_check_unconfigured: '接続確認の設定が不足しています',
|
|
1393
|
-
foundation_provider_check_url_missing: '接続確認先のURLが設定されていません',
|
|
1394
|
-
provider_check_url_missing: '接続確認先のURLが設定されていません',
|
|
1395
|
-
foundation_provider_check_token_missing: '接続確認用の認証情報が設定されていません',
|
|
1396
|
-
provider_credentials_missing: '接続確認用の認証情報が設定されていません',
|
|
1397
|
-
provider_credentials_unavailable: '接続確認用の認証情報を読み取れません',
|
|
1398
|
-
provider_credentials_invalid: '接続確認用の認証情報が不正です',
|
|
1399
|
-
provider_permission_denied: '認証または権限が不足しています',
|
|
1400
|
-
permission_denied: '認証または権限が不足しています',
|
|
1401
|
-
provider_timeout: '接続先が時間内に応答しませんでした',
|
|
1402
|
-
upstream_timeout: '接続先が時間内に応答しませんでした',
|
|
1403
|
-
provider_request_failed: '接続先へ到達できませんでした',
|
|
1404
|
-
upstream_unavailable: '接続先へ到達できませんでした',
|
|
1405
|
-
provider_rejected: '接続先が確認要求を拒否しました',
|
|
1406
|
-
provider_readback_mismatch: '登録内容と確認結果が一致しません',
|
|
1407
|
-
local_preview_only: 'ローカルプレビューでは実接続を確認できません',
|
|
1408
|
-
not_implemented: 'このアプリの接続は準備中です',
|
|
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)})`;
|
|
1414
|
-
return labels[normalized] ?? (value || UNKNOWN);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
function resourceLabel(value) {
|
|
1418
|
-
const source = objectValue(value);
|
|
1419
|
-
return firstPresent(source.label, source.name, source.title, source.identifier, source.url, source.id) ?? textOf(value);
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function permittedResources(contract) {
|
|
1423
|
-
const source = objectValue(contract);
|
|
1424
|
-
const raw = source.allowedResources ?? source.allowed_resources ?? source.resources ?? source.scopeResources ?? source.scope_resources;
|
|
1425
|
-
if (Array.isArray(raw)) return raw;
|
|
1426
|
-
if (raw && typeof raw === 'object') return Object.entries(raw).flatMap(([kind, items]) => (Array.isArray(items) ? items : [items]).map((item) => ({ kind, value: item })));
|
|
1427
|
-
const refs = [source.inputRefs, source.knowledgeRefs, source.judgmentRefs].flatMap((items) => Array.isArray(items) ? items : []);
|
|
1428
|
-
return refs;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
function normalizeConnectorPayload(payload) {
|
|
1432
|
-
const records = collectionFrom(payload, ['connectors', 'connections', 'items', 'records']);
|
|
1433
|
-
if (!records) return { state: 'unknown', records: [], raw: payload };
|
|
1434
|
-
return { state: records.length ? 'ready' : 'empty', records: records.map((item) => withConnectorDefinition(item, '', connectorDefinitions)), raw: payload };
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
function connectorFromFoundationCheck(definition, payload, error, attemptedAt) {
|
|
1438
|
-
const source = objectValue(payload ?? error?.payload);
|
|
1439
|
-
const verification = objectValue(source.verification);
|
|
1440
|
-
const registered = connectorRecordsFromProject().find((item) => item.id === definition.id) ?? {};
|
|
1441
|
-
const status = firstPresent(source.status, verification.state, error?.status === 403 ? 'permission_denied' : null, error ? 'failed' : null);
|
|
1442
|
-
const reason = firstPresent(verification.reason, source.reason, source.error?.code, source.error, error?.message);
|
|
1443
|
-
const resources = Array.isArray(source.resources) ? source.resources : registered.resources;
|
|
1444
|
-
return normalizeConnector({
|
|
1445
|
-
...registered,
|
|
1446
|
-
id: definition.id,
|
|
1447
|
-
label: definition.label,
|
|
1448
|
-
description: definition.description,
|
|
1449
|
-
status,
|
|
1450
|
-
account: firstPresent(source.account, source.identifier, registered.account),
|
|
1451
|
-
resources,
|
|
1452
|
-
checkedAt: firstPresent(verification.checked_at, verification.checkedAt, source.checked_at, source.checkedAt, attemptedAt),
|
|
1453
|
-
reason,
|
|
1454
|
-
}, definition.id);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
async function checkFoundationConnections() {
|
|
1458
|
-
const attemptedAt = new Date().toISOString();
|
|
1459
|
-
return Promise.all(connectorDefinitions.map(async (definition) => {
|
|
1460
|
-
const kind = CONNECTOR_FOUNDATION_KINDS[definition.id] ?? definition.id;
|
|
1461
|
-
const target = path('foundationCheck', { kind });
|
|
1462
|
-
if (!target || typeof apiMutation !== 'function') return connectorFromFoundationCheck(definition, {}, null, null);
|
|
1463
|
-
try {
|
|
1464
|
-
const payload = await apiMutation(target, {
|
|
1465
|
-
method: 'POST',
|
|
1466
|
-
key: `mana-foundation-${kind}-check-${attemptedAt}`,
|
|
1467
|
-
body: {},
|
|
1468
|
-
});
|
|
1469
|
-
return connectorFromFoundationCheck(definition, payload, null, attemptedAt);
|
|
1470
|
-
} catch (error) {
|
|
1471
|
-
return connectorFromFoundationCheck(definition, null, error, attemptedAt);
|
|
1472
|
-
}
|
|
1473
|
-
}));
|
|
1474
|
-
}
|
|
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
|
-
|
|
1520
|
-
async function loadConnections() {
|
|
1521
|
-
const checkTarget = path('foundationCheck', { kind: 'github' });
|
|
1522
|
-
const target = path('connectors') ?? path('connections');
|
|
1523
|
-
state.connections.status = 'loading'; state.connections.error = null; render();
|
|
1524
|
-
try {
|
|
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();
|
|
1530
|
-
state.connections = { ...state.connections, status: 'ready', records, error: null };
|
|
1531
|
-
} else if (!target) {
|
|
1532
|
-
const records = projectConnectors();
|
|
1533
|
-
state.connections = { ...state.connections, status: records.some((item) => item.source !== null && item.source !== undefined) ? 'ready' : 'unknown', records, error: null };
|
|
1534
|
-
} else {
|
|
1535
|
-
const normalized = normalizeConnectorPayload(await callApi(target));
|
|
1536
|
-
state.connections = { ...state.connections, ...normalized, status: normalized.state, error: null };
|
|
1537
|
-
}
|
|
1538
|
-
if (!state.connections.selectedId && state.connections.records.length) state.connections.selectedId = state.connections.records[0].id;
|
|
1539
|
-
state.connections.detail = state.connections.records.find((item) => item.id === state.connections.selectedId) ?? null;
|
|
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
|
-
}
|
|
1554
|
-
render(); notifyChange(); return state.connections;
|
|
1555
|
-
}
|
|
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
|
-
|
|
1564
|
-
async function loadManaSettings() {
|
|
1565
|
-
const target = path('settings') ?? path('manaSettings');
|
|
1566
|
-
const mana = objectValue(currentProject().mana);
|
|
1567
|
-
const projected = objectValue(firstPresent(
|
|
1568
|
-
currentProject().manaSettings,
|
|
1569
|
-
currentProject().mana_settings,
|
|
1570
|
-
mana.settings,
|
|
1571
|
-
mana.runtimeSettings,
|
|
1572
|
-
mana.runtime_settings,
|
|
1573
|
-
));
|
|
1574
|
-
if (!target) {
|
|
1575
|
-
state.manaSettings = { ...state.manaSettings, status: Object.keys(projected).length ? 'ready' : 'unknown', data: Object.keys(projected).length ? projected : null, error: null };
|
|
1576
|
-
render(); notifyChange(); return state.manaSettings;
|
|
1577
|
-
}
|
|
1578
|
-
state.manaSettings.status = 'loading'; state.manaSettings.error = null; render();
|
|
1579
|
-
try {
|
|
1580
|
-
const payload = await callApi(target);
|
|
1581
|
-
const data = objectValue(payload.settings ?? payload.manaSettings ?? payload);
|
|
1582
|
-
state.manaSettings = { ...state.manaSettings, status: Object.keys(data).length ? 'ready' : 'unknown', data: Object.keys(data).length ? data : null, error: null };
|
|
1583
|
-
} catch (error) { setError(state.manaSettings, error); }
|
|
1584
|
-
render(); notifyChange(); return state.manaSettings;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function settingsSaveTarget() {
|
|
1588
|
-
// The read endpoint is intentionally not reused for writes. Hosts must
|
|
1589
|
-
// opt into a write route explicitly so an editable form cannot mutate a
|
|
1590
|
-
// runtime setting by accident.
|
|
1591
|
-
if (!hasOwn(paths, 'settingsSave') && !pathBuilder) return null;
|
|
1592
|
-
return path('settingsSave');
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
function settingSection(data, camelKey, snakeKey) {
|
|
1596
|
-
return objectValue(firstPresent(data[camelKey], data[snakeKey]));
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
function settingValue(section, ...keys) {
|
|
1600
|
-
return firstPresent(...keys.map((key) => section[key])) ?? '';
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
function scalarSettingValue(value) {
|
|
1604
|
-
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
1605
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return '';
|
|
1606
|
-
const nestedKeys = ['maxAttempts', 'max_attempts', 'max', 'limit', 'count', 'value'];
|
|
1607
|
-
const nestedKey = nestedKeys.find((key) => hasOwn(value, key));
|
|
1608
|
-
return nestedKey ? scalarSettingValue(value[nestedKey]) : '';
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
function updateAttemptSetting(current, nextValue) {
|
|
1612
|
-
if (current === undefined) return nextValue;
|
|
1613
|
-
if (typeof current === 'string' || typeof current === 'number') return nextValue;
|
|
1614
|
-
if (!current || typeof current !== 'object' || Array.isArray(current)) return current;
|
|
1615
|
-
const nestedKeys = ['maxAttempts', 'max_attempts', 'max', 'limit', 'count', 'value'];
|
|
1616
|
-
const nestedKey = nestedKeys.find((key) => hasOwn(current, key));
|
|
1617
|
-
if (!nestedKey) return current;
|
|
1618
|
-
return { ...current, [nestedKey]: updateAttemptSetting(current[nestedKey], nextValue) };
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function settingsDraftFromFields(data, controls) {
|
|
1622
|
-
const source = objectValue(data);
|
|
1623
|
-
const draft = JSON.parse(JSON.stringify(source));
|
|
1624
|
-
const defaultsKey = hasOwn(source, 'runtime_defaults') ? 'runtime_defaults' : hasOwn(source, 'runtimeDefaults') ? 'runtimeDefaults' : 'defaults';
|
|
1625
|
-
const safetyKey = hasOwn(source, 'safety_limits') ? 'safety_limits' : hasOwn(source, 'safetyLimits') ? 'safetyLimits' : 'safety_limits';
|
|
1626
|
-
const notificationKey = hasOwn(source, 'notifications') ? 'notifications' : 'notifications';
|
|
1627
|
-
const auditKey = hasOwn(source, 'audit') ? 'audit' : 'audit';
|
|
1628
|
-
const defaults = objectValue(source[defaultsKey]);
|
|
1629
|
-
const safety = objectValue(source[safetyKey]);
|
|
1630
|
-
const attemptKeys = ['maxAttempts', 'max_attempts', 'attempts'];
|
|
1631
|
-
const safetyAttemptKey = attemptKeys.find((key) => hasOwn(safety, key));
|
|
1632
|
-
const defaultsAttemptKey = attemptKeys.find((key) => hasOwn(defaults, key));
|
|
1633
|
-
const attemptsSectionKey = safetyAttemptKey ? safetyKey : defaultsAttemptKey ? defaultsKey : safetyKey;
|
|
1634
|
-
const attemptsPropertyKey = safetyAttemptKey ?? defaultsAttemptKey ?? (attemptsSectionKey === safetyKey ? 'attempts' : 'maxAttempts');
|
|
1635
|
-
draft[defaultsKey] = { ...defaults, timeout: controls.timeout.value, timeoutMs: controls.timeoutMs.value, budgetUnits: controls.budgetUnits.value };
|
|
1636
|
-
if (attemptsSectionKey === defaultsKey) draft[defaultsKey][attemptsPropertyKey] = updateAttemptSetting(defaults[attemptsPropertyKey], controls.maxAttempts.value);
|
|
1637
|
-
if (attemptsSectionKey === safetyKey) draft[safetyKey] = { ...safety, [attemptsPropertyKey]: updateAttemptSetting(safety[attemptsPropertyKey], controls.maxAttempts.value) };
|
|
1638
|
-
draft[safetyKey] = { ...objectValue(draft[safetyKey] ?? safety), safeTestRequired: controls.safeTestRequired.checked, externalEffectApproval: controls.externalEffectApproval.checked };
|
|
1639
|
-
draft[notificationKey] = { ...objectValue(source[notificationKey]), channel: controls.channel.value, events: controls.events.value };
|
|
1640
|
-
draft[auditKey] = { ...objectValue(source[auditKey]), retention: controls.retention.value, destination: controls.auditDestination.value };
|
|
1641
|
-
return draft;
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
async function saveManaSettings(draft) {
|
|
1645
|
-
const target = settingsSaveTarget();
|
|
1646
|
-
state.manaSettings.saveError = null;
|
|
1647
|
-
if (state.manaSettings.status !== 'ready' || !target) {
|
|
1648
|
-
state.manaSettings.draft = null;
|
|
1649
|
-
state.manaSettings.saveStatus = 'unknown';
|
|
1650
|
-
render(); notifyChange();
|
|
1651
|
-
return state.manaSettings;
|
|
1652
|
-
}
|
|
1653
|
-
state.manaSettings.draft = objectValue(draft);
|
|
1654
|
-
state.manaSettings.saveStatus = 'saving';
|
|
1655
|
-
render();
|
|
1656
|
-
try {
|
|
1657
|
-
const payload = await callMutation(target, { method: 'PUT', body: { settings: state.manaSettings.draft }, key: idempotencyKey('mana-settings-save') });
|
|
1658
|
-
const readback = objectValue(payload?.settings ?? payload?.manaSettings ?? payload);
|
|
1659
|
-
if (Object.keys(readback).length) {
|
|
1660
|
-
state.manaSettings = { ...state.manaSettings, status: 'ready', data: readback, draft: null, saveStatus: 'ready', saveError: null, error: null };
|
|
1661
|
-
} else {
|
|
1662
|
-
state.manaSettings.saveStatus = 'saved_unverified';
|
|
1663
|
-
}
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
state.manaSettings.saveStatus = 'error_retryable';
|
|
1666
|
-
state.manaSettings.saveError = normalizeError(error);
|
|
1667
|
-
}
|
|
1668
|
-
render(); notifyChange(); return state.manaSettings;
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
async function callApi(target, request = {}) {
|
|
1672
|
-
if (typeof api !== 'function') throw new Error('outcome_mana_api_unavailable');
|
|
1673
|
-
return api(target, { method: 'GET', headers: { accept: 'application/json' }, ...request });
|
|
1674
|
-
}
|
|
1675
|
-
async function callMutation(target, request = {}) {
|
|
1676
|
-
if (typeof apiMutation !== 'function') throw new Error('outcome_mana_api_mutation_unavailable');
|
|
1677
|
-
const method = request.method ?? 'POST';
|
|
1678
|
-
const key = request.key ?? idempotencyKey('mana-outcome');
|
|
1679
|
-
return apiMutation(target, { method, key, body: request.body ?? {} });
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function notifyChange() { if (typeof options.onChange === 'function') options.onChange(state); }
|
|
1683
|
-
function setError(target, error) {
|
|
1684
|
-
target.error = normalizeError(error);
|
|
1685
|
-
target.status = target.error.kind;
|
|
1686
|
-
}
|
|
1687
|
-
function confirmAction(message) {
|
|
1688
|
-
if (typeof options.confirm === 'function') return options.confirm(message);
|
|
1689
|
-
if (typeof globalThis.confirm === 'function') return globalThis.confirm(message);
|
|
1690
|
-
return true;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
async function loadContracts() {
|
|
1694
|
-
state.contract.status = 'loading'; state.contract.error = null; render();
|
|
1695
|
-
try {
|
|
1696
|
-
const payload = await callApi(path('contractList'));
|
|
1697
|
-
const normalized = normalizeContracts(payload);
|
|
1698
|
-
state.contract = { ...state.contract, ...normalized, status: normalized.state, error: null };
|
|
1699
|
-
if (state.contract.records.length && !state.contract.records.some((record) => record.contractId === state.contract.selectedId)) state.contract.selectedId = state.contract.records[0].contractId;
|
|
1700
|
-
if (state.contract.selectedId) await loadContractDetail(state.contract.selectedId, { renderAfter: false });
|
|
1701
|
-
} catch (error) {
|
|
1702
|
-
setError(state.contract, error);
|
|
1703
|
-
}
|
|
1704
|
-
render(); notifyChange();
|
|
1705
|
-
return state.contract;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
async function loadContractDetail(id = contractId(), { renderAfter = true } = {}) {
|
|
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;
|
|
1721
|
-
state.contract.status = 'loading'; state.contract.error = null;
|
|
1722
|
-
if (renderAfter) render();
|
|
1723
|
-
try {
|
|
1724
|
-
const payload = await callApi(path('contractDetail', { contractId: requestedId }));
|
|
1725
|
-
if (state.contract.selectedId !== requestedId || contractDetailRequestGeneration !== requestGeneration) return null;
|
|
1726
|
-
const contract = normalizeContract(payload);
|
|
1727
|
-
state.contract.detail = contract.contractId ? contract : { ...contract, contractId: requestedId };
|
|
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
|
-
}
|
|
1744
|
-
const authorityPayload = objectValue(payload).authority ?? objectValue(payload).authorityMatrix;
|
|
1745
|
-
if (authorityPayload !== undefined) state.authority = { ...normalizeAuthority(authorityPayload), error: null };
|
|
1746
|
-
const triggerPayload = objectValue(payload).trigger ?? objectValue(payload).triggers;
|
|
1747
|
-
if (triggerPayload !== undefined) state.triggers = { ...state.triggers, status: 'ready', trigger: normalizeTrigger(triggerPayload), error: null, draft: null };
|
|
1748
|
-
await loadTriggerCatalog(state.contract.detail.trigger?.type ?? 'manual', { renderAfter: false, enterDetail: false });
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
if (state.contract.selectedId === requestedId && contractDetailRequestGeneration === requestGeneration) setError(state.contract, error);
|
|
1751
|
-
}
|
|
1752
|
-
if (renderAfter) { render(); notifyChange(); }
|
|
1753
|
-
return state.contract.detail;
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
async function loadTriggerCatalog(type = triggerFromContract(currentContract(), state.triggers.draft).type, { renderAfter = true, enterDetail = true } = {}) {
|
|
1757
|
-
if (enterDetail) setActiveStage('triggers');
|
|
1758
|
-
if (type === 'manual') {
|
|
1759
|
-
state.triggers.catalogStatus = 'ready'; state.triggers.schedules = []; state.triggers.subscriptions = []; state.triggers.catalogError = null;
|
|
1760
|
-
if (renderAfter) { render(); notifyChange(); }
|
|
1761
|
-
return state.triggers;
|
|
1762
|
-
}
|
|
1763
|
-
state.triggers.catalogStatus = 'loading'; state.triggers.catalogError = null;
|
|
1764
|
-
if (renderAfter) render();
|
|
1765
|
-
try {
|
|
1766
|
-
const payload = objectValue(await callApi(path('triggerCatalog', { id: type })));
|
|
1767
|
-
state.triggers.schedules = Array.isArray(payload.schedules) ? payload.schedules.filter((item) => item && typeof item === 'object') : [];
|
|
1768
|
-
state.triggers.subscriptions = Array.isArray(payload.subscriptions) ? payload.subscriptions.filter((item) => item && typeof item === 'object') : [];
|
|
1769
|
-
state.triggers.catalogStatus = 'ready';
|
|
1770
|
-
} catch (error) {
|
|
1771
|
-
state.triggers.catalogError = normalizeError(error); state.triggers.catalogStatus = state.triggers.catalogError.kind;
|
|
1772
|
-
}
|
|
1773
|
-
if (renderAfter) { render(); notifyChange(); }
|
|
1774
|
-
return state.triggers;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function triggerAvailability(trigger) {
|
|
1778
|
-
if (trigger.type === 'manual') return { available: true, reason: '' };
|
|
1779
|
-
if (state.triggers.catalogStatus !== 'ready') return { available: false, reason: '接続候補を確認できていないため、この起動方式は利用できません。' };
|
|
1780
|
-
if (trigger.type === 'schedule') {
|
|
1781
|
-
const candidate = state.triggers.schedules.find((item) => item.id === trigger.scheduleId);
|
|
1782
|
-
if (!candidate) return { available: false, reason: '一致する定期実行の接続が見つからないため、定期実行は利用できません。' };
|
|
1783
|
-
if (!candidate.connected) return { available: false, reason: '定期実行の接続を確認できないため、定期実行は利用できません。' };
|
|
1784
|
-
if (!candidate.enabled) return { available: false, reason: '定期実行の接続が無効なため、定期実行は利用できません。' };
|
|
1785
|
-
return { available: true, reason: '' };
|
|
1786
|
-
}
|
|
1787
|
-
const candidate = state.triggers.subscriptions.find((item) => item.id === trigger.subscriptionId && item.provider === trigger.provider);
|
|
1788
|
-
const provider = trigger.provider ? `${trigger.provider === 'docusign' ? 'DocuSign' : trigger.provider}の` : '';
|
|
1789
|
-
if (!candidate) return { available: false, reason: `${provider}購読接続が見つからないため、文書追加は利用できません。` };
|
|
1790
|
-
if (!candidate.connected) return { available: false, reason: `${provider}購読接続を確認できないため、文書追加は利用できません。` };
|
|
1791
|
-
if (!candidate.enabled) return { available: false, reason: `${provider}購読接続が無効なため、文書追加は利用できません。` };
|
|
1792
|
-
if (candidate.available !== true) {
|
|
1793
|
-
const reasons = {
|
|
1794
|
-
document_event_authenticator_missing: `${provider}文書イベントの署名検証が未接続のため、文書追加は利用できません。`,
|
|
1795
|
-
document_subscription_unsubscriber_missing: `${provider}購読解除処理が未接続のため、文書追加は利用できません。`,
|
|
1796
|
-
document_subscription_scope_mismatch: 'この接続の組織・プロジェクト・実行者が現在の委任と一致しないため利用できません。委任に対応する接続を選択してください。',
|
|
1797
|
-
github_document_ingress_credentials_missing: 'GitHubの文書取り込み用Webhook認証情報が未設定のため、文書追加は利用できません。Webhook用認証情報を設定するか、管理者に確認してください。',
|
|
1798
|
-
github_document_ingress_configuration_missing: 'GitHubのリポジトリ・参照先・取り込みパス・コールバック先の設定が未設定または不正なため、文書追加は利用できません。リポジトリ、参照先、取り込みパス、コールバック先を確認してください。',
|
|
1799
|
-
};
|
|
1800
|
-
return { available: false, reason: reasons[candidate.reason] ?? `${provider}外部プロバイダー固有のWebhook接続を確認できないため、文書追加は利用できません。` };
|
|
1801
|
-
}
|
|
1802
|
-
return { available: true, reason: '' };
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
async function verifyContractReadback(id, mutationPayload, expectedStatus = null, expectedFields = null) {
|
|
1806
|
-
const applyReadback = (readback) => {
|
|
1807
|
-
state.contract.detail = readback;
|
|
1808
|
-
state.contract.selectedId = id;
|
|
1809
|
-
state.contract.status = 'ready';
|
|
1810
|
-
const index = state.contract.records.findIndex((record) => record.contractId === id);
|
|
1811
|
-
if (index === -1) state.contract.records = [...state.contract.records, readback];
|
|
1812
|
-
else state.contract.records = state.contract.records.map((record, recordIndex) => recordIndex === index ? readback : record);
|
|
1813
|
-
return readback;
|
|
1814
|
-
};
|
|
1815
|
-
const inline = normalizeContract(mutationPayload);
|
|
1816
|
-
const expectedVersion = inline.version;
|
|
1817
|
-
if (!Number.isInteger(expectedVersion)) return null;
|
|
1818
|
-
const expectation = { id, version: expectedVersion, fields: expectedFields, status: expectedStatus };
|
|
1819
|
-
if (contractMatchesExpected(inline, expectation) && isReadbackVerified(mutationPayload)) return applyReadback(inline);
|
|
1820
|
-
try {
|
|
1821
|
-
const payload = await callApi(path('contractDetail', { contractId: id }));
|
|
1822
|
-
const readback = normalizeContract(payload);
|
|
1823
|
-
if (contractMatchesExpected(readback, expectation)) return applyReadback(readback);
|
|
1824
|
-
} catch {
|
|
1825
|
-
// A successful write without a readback is intentionally not a verified state.
|
|
1826
|
-
}
|
|
1827
|
-
return null;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
async function saveContractDraft(draft, { create = false } = {}) {
|
|
1831
|
-
const id = contractId();
|
|
1832
|
-
const body = objectValue(draft);
|
|
1833
|
-
state.contract.formDraft = body;
|
|
1834
|
-
state.contract.status = 'saving'; state.contract.error = null; render();
|
|
1835
|
-
try {
|
|
1836
|
-
const target = create || !id ? path('contractList') : path('contractDetail', { contractId: id });
|
|
1837
|
-
const method = create || !id ? 'POST' : 'PATCH';
|
|
1838
|
-
const mutationBody = id && !create && Number.isInteger(currentContract()?.version) ? { ...body, expected_version: currentContract().version } : body;
|
|
1839
|
-
const fingerprint = `${method}:${target}:${JSON.stringify(mutationBody)}`;
|
|
1840
|
-
if (state.contract.pendingSave?.fingerprint !== fingerprint) {
|
|
1841
|
-
state.contract.pendingSave = { fingerprint, key: idempotencyKey(create || !id ? 'mana-contract-create' : 'mana-contract-save') };
|
|
1842
|
-
}
|
|
1843
|
-
const payload = await callMutation(target, {
|
|
1844
|
-
method,
|
|
1845
|
-
body: mutationBody,
|
|
1846
|
-
key: state.contract.pendingSave.key,
|
|
1847
|
-
});
|
|
1848
|
-
const saved = normalizeContract(payload);
|
|
1849
|
-
const savedId = saved.contractId || id;
|
|
1850
|
-
state.contract.formDraft = null;
|
|
1851
|
-
if (savedId) {
|
|
1852
|
-
const readback = await verifyContractReadback(savedId, payload, null, body);
|
|
1853
|
-
if (readback) {
|
|
1854
|
-
state.contract.detail = readback; state.contract.selectedId = savedId;
|
|
1855
|
-
state.contract.records = state.contract.records.some((item) => item.contractId === savedId)
|
|
1856
|
-
? state.contract.records.map((item) => item.contractId === savedId ? readback : item)
|
|
1857
|
-
: [readback, ...state.contract.records];
|
|
1858
|
-
state.contract.status = 'ready';
|
|
1859
|
-
state.contract.pendingSave = null;
|
|
1860
|
-
if (state.safeTest.result) await refreshSafeTestReadback();
|
|
1861
|
-
} else {
|
|
1862
|
-
state.contract.status = 'saved_unverified';
|
|
1863
|
-
}
|
|
1864
|
-
} else state.contract.status = 'saved_unverified';
|
|
1865
|
-
} catch (error) {
|
|
1866
|
-
setError(state.contract, error);
|
|
1867
|
-
}
|
|
1868
|
-
render(); notifyChange(); return state.contract;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
async function cloneContract(id = contractId()) {
|
|
1872
|
-
if (!id) return null;
|
|
1873
|
-
state.contract.status = 'saving'; render();
|
|
1874
|
-
try {
|
|
1875
|
-
const payload = await callMutation(path('contractClone', { contractId: id }), { body: {}, key: idempotencyKey('mana-contract-clone') });
|
|
1876
|
-
const cloned = normalizeContract(payload);
|
|
1877
|
-
const readback = cloned.contractId ? await verifyContractReadback(cloned.contractId, payload, 'draft') : null;
|
|
1878
|
-
if (readback) {
|
|
1879
|
-
state.contract.records = [readback, ...state.contract.records.filter((item) => item.contractId !== readback.contractId)];
|
|
1880
|
-
state.contract.selectedId = readback.contractId; state.contract.detail = readback; state.contract.status = 'ready';
|
|
1881
|
-
} else state.contract.status = 'saved_unverified';
|
|
1882
|
-
} catch (error) { setError(state.contract, error); }
|
|
1883
|
-
render(); notifyChange(); return state.contract;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
async function retireContract(id = contractId()) {
|
|
1887
|
-
if (!id || !confirmAction('この委任契約を廃止します。履歴は保持されます。続けますか?')) return null;
|
|
1888
|
-
state.contract.status = 'saving'; render();
|
|
1889
|
-
try {
|
|
1890
|
-
const current = currentContract();
|
|
1891
|
-
const payload = await callMutation(path('contractRetire', { contractId: id }), {
|
|
1892
|
-
body: Number.isInteger(current?.version) ? { expected_version: current.version } : {}, key: idempotencyKey('mana-contract-retire'),
|
|
1893
|
-
});
|
|
1894
|
-
const readback = await verifyContractReadback(id, payload, 'retired');
|
|
1895
|
-
if (readback) {
|
|
1896
|
-
state.contract.detail = readback; state.contract.records = state.contract.records.map((item) => item.contractId === id ? readback : item); state.contract.status = 'ready';
|
|
1897
|
-
} else state.contract.status = 'saved_unverified';
|
|
1898
|
-
} catch (error) { setError(state.contract, error); }
|
|
1899
|
-
render(); notifyChange(); return state.contract;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
async function loadAuthority(id = contractId()) {
|
|
1903
|
-
setActiveStage('authority');
|
|
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;
|
|
1912
|
-
state.authority.status = 'loading'; state.authority.error = null; render();
|
|
1913
|
-
try {
|
|
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;
|
|
1916
|
-
state.authority = { ...state.authority, ...normalizeAuthority(payload), error: null };
|
|
1917
|
-
} catch (error) {
|
|
1918
|
-
if (state.contract.selectedId === requestedId && authorityRequestGeneration === requestGeneration && currentContract()?.version === requestedVersion) setError(state.authority, error);
|
|
1919
|
-
}
|
|
1920
|
-
render(); notifyChange(); return state.authority;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
async function saveAuthority(matrix, id = contractId()) {
|
|
1924
|
-
if (!id) return null;
|
|
1925
|
-
state.authority.draft = matrix; state.authority.status = 'saving'; state.authority.error = null; render();
|
|
1926
|
-
try {
|
|
1927
|
-
const current = currentContract();
|
|
1928
|
-
const entries = Array.isArray(matrix) ? matrix : [];
|
|
1929
|
-
for (const entry of entries) {
|
|
1930
|
-
await callMutation(path('authority', { contractId: id }), {
|
|
1931
|
-
method: 'PUT', body: { contract_version: current?.version, operation: entry.operation,
|
|
1932
|
-
resource: entry.resource ?? entry.target, effect: entry.effect ?? entry.policy },
|
|
1933
|
-
key: idempotencyKey(`mana-authority-save-${entry.operation ?? 'rule'}`),
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
const payload = await callApi(path('authority', { contractId: id, contractVersion: current?.version }));
|
|
1937
|
-
const readback = normalizeAuthority(payload);
|
|
1938
|
-
const signature = (entry) => JSON.stringify({
|
|
1939
|
-
operation: entry.operation,
|
|
1940
|
-
resource: entry.resource ?? entry.target,
|
|
1941
|
-
effect: entry.effect ?? entry.policy,
|
|
1942
|
-
actor: entry.actorId ?? entry.actor ?? null,
|
|
1943
|
-
});
|
|
1944
|
-
const expected = entries.map(signature).sort();
|
|
1945
|
-
const actual = readback.matrix.map(signature).sort();
|
|
1946
|
-
state.authority = { ...state.authority, ...readback, draft: null,
|
|
1947
|
-
status: expected.length === actual.length && expected.every((value, index) => value === actual[index]) ? 'ready' : 'saved_unverified', error: null };
|
|
1948
|
-
} catch (error) { setError(state.authority, error); }
|
|
1949
|
-
render(); notifyChange(); return state.authority;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
async function saveTriggers(trigger, id = contractId()) {
|
|
1953
|
-
if (!id) return null;
|
|
1954
|
-
state.triggers.draft = normalizeTrigger(trigger); state.triggers.status = 'saving'; state.triggers.error = null; render();
|
|
1955
|
-
try {
|
|
1956
|
-
const current = currentContract();
|
|
1957
|
-
const normalized = normalizeTrigger(trigger);
|
|
1958
|
-
const cleanTrigger = normalized.type === 'schedule' ? { type: 'schedule', scheduleId: normalized.scheduleId }
|
|
1959
|
-
: normalized.type === 'document' ? { type: 'document', subscriptionId: normalized.subscriptionId, provider: normalized.provider }
|
|
1960
|
-
: { type: 'manual' };
|
|
1961
|
-
const payload = await callMutation(path('triggers', { contractId: id }), {
|
|
1962
|
-
method: 'PUT', body: { contract: contractInput(current, { trigger: cleanTrigger }), ...(Number.isInteger(current?.version) ? { expected_version: current.version } : {}) }, key: idempotencyKey('mana-trigger-save'),
|
|
1963
|
-
});
|
|
1964
|
-
if (isReadbackVerified(payload)) {
|
|
1965
|
-
const savedContract = normalizeContract(payload);
|
|
1966
|
-
const saved = savedContract.trigger;
|
|
1967
|
-
state.contract.detail = savedContract;
|
|
1968
|
-
state.triggers = { ...state.triggers, status: 'ready', trigger: saved, error: null, draft: null };
|
|
1969
|
-
await loadTriggerCatalog(saved.type, { renderAfter: false });
|
|
1970
|
-
} else state.triggers.status = 'saved_unverified';
|
|
1971
|
-
} catch (error) { setError(state.triggers, error); }
|
|
1972
|
-
render(); notifyChange(); return state.triggers;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
async function runSafeTest(input = {}, id = contractId()) {
|
|
1976
|
-
setActiveStage('safe_test');
|
|
1977
|
-
if (!id) return null;
|
|
1978
|
-
const body = {
|
|
1979
|
-
contract_version: input.contractVersion ?? input.contract_version ?? currentContract()?.version,
|
|
1980
|
-
sample_input: input.sampleInput ?? input.sample_input ?? '',
|
|
1981
|
-
mode: 'isolated',
|
|
1982
|
-
external_send: false,
|
|
1983
|
-
production_writes: false,
|
|
1984
|
-
};
|
|
1985
|
-
stopSafeTestReadback();
|
|
1986
|
-
safeTestReadCount = 0;
|
|
1987
|
-
state.safeTest.runId = null;
|
|
1988
|
-
state.safeTest.contractId = id;
|
|
1989
|
-
state.safeTest.formDraft = body; state.safeTest.status = 'loading'; state.safeTest.error = null; render();
|
|
1990
|
-
try {
|
|
1991
|
-
const payload = await callMutation(path('tests', { contractId: id }), { body, key: idempotencyKey('mana-safe-test') });
|
|
1992
|
-
const accepted = normalizeRun(payload);
|
|
1993
|
-
const acceptedId = accepted.id;
|
|
1994
|
-
state.safeTest.runId = acceptedId || null;
|
|
1995
|
-
state.safeTest.result = payload;
|
|
1996
|
-
rememberSafeTestRun(id, acceptedId);
|
|
1997
|
-
state.safeTest.formDraft = null;
|
|
1998
|
-
state.safeTest.status = safeTestStatus(payload, currentContract());
|
|
1999
|
-
if (acceptedId) await refreshSafeTestReadback();
|
|
2000
|
-
} catch (error) { setError(state.safeTest, error); }
|
|
2001
|
-
render(); notifyChange(); return state.safeTest;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
async function refreshSafeTestReadback() {
|
|
2005
|
-
const runId = state.safeTest.runId ?? normalizeRun(state.safeTest.result).id;
|
|
2006
|
-
if (!runId) return state.safeTest;
|
|
2007
|
-
stopSafeTestReadback();
|
|
2008
|
-
try {
|
|
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');
|
|
2012
|
-
state.safeTest.result = result;
|
|
2013
|
-
state.safeTest.status = safeTestStatus(result, currentContract());
|
|
2014
|
-
state.safeTest.error = null;
|
|
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);
|
|
2019
|
-
state.safeTest.status = 'unknown';
|
|
2020
|
-
}
|
|
2021
|
-
render(); notifyChange();
|
|
2022
|
-
return state.safeTest;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
async function activate(id = contractId()) {
|
|
2026
|
-
setActiveStage('triggers');
|
|
2027
|
-
if (!id) return null;
|
|
2028
|
-
const contract = currentContract();
|
|
2029
|
-
const availability = triggerAvailability(triggerFromContract(currentContract(), state.triggers.draft));
|
|
2030
|
-
const testStatus = state.safeTest.result ? safeTestStatus(state.safeTest.result, contract) : state.safeTest.status;
|
|
2031
|
-
const authorityVerified = state.authority.status === 'ready' && state.authority.matrix.length > 0;
|
|
2032
|
-
const blockedReason = !availability.available ? availability.reason
|
|
2033
|
-
: !authorityVerified ? '権限マトリクスを確認できていないため、委任を有効化できません。'
|
|
2034
|
-
: testStatus !== 'verified' ? '現在の設定で完了条件まで検証した隔離試験が必要です。' : '';
|
|
2035
|
-
if (blockedReason) {
|
|
2036
|
-
state.activation = { status: 'unknown', error: { kind: 'unknown', message: blockedReason }, message: '' };
|
|
2037
|
-
render(); notifyChange(); return null;
|
|
2038
|
-
}
|
|
2039
|
-
state.activation.status = 'loading'; state.activation.error = null; state.activation.message = ''; render();
|
|
2040
|
-
try {
|
|
2041
|
-
const current = currentContract();
|
|
2042
|
-
const payload = await callMutation(path('activation', { contractId: id }), {
|
|
2043
|
-
body: { action: 'activate', ...(Number.isInteger(current?.version) ? { expected_version: current.version } : {}) }, key: idempotencyKey('mana-activation'),
|
|
2044
|
-
});
|
|
2045
|
-
const readback = await verifyContractReadback(id, payload, 'active');
|
|
2046
|
-
if (readback) { state.activation.status = 'verified'; state.activation.message = '有効化済みの契約を再取得して確認しました。'; }
|
|
2047
|
-
else state.activation.status = 'saved_unverified';
|
|
2048
|
-
} catch (error) { const normalized = normalizeError(error); state.activation.error = normalized; state.activation.status = normalized.kind; }
|
|
2049
|
-
render(); notifyChange(); return state.activation;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
async function stopActivation(id = contractId()) {
|
|
2053
|
-
setActiveStage('triggers');
|
|
2054
|
-
if (!id || !confirmAction('この委任を停止します。進行中の実行は実行詳細で確認してください。続けますか?')) return null;
|
|
2055
|
-
state.activation.status = 'loading'; state.activation.error = null; render();
|
|
2056
|
-
try {
|
|
2057
|
-
const current = currentContract();
|
|
2058
|
-
const payload = await callMutation(path('activation', { contractId: id }), {
|
|
2059
|
-
body: { action: 'stop', ...(Number.isInteger(current?.version) ? { expected_version: current.version } : {}) }, key: idempotencyKey('mana-activation-stop'),
|
|
2060
|
-
});
|
|
2061
|
-
const readback = await verifyContractReadback(id, payload, 'stopped');
|
|
2062
|
-
state.activation.status = readback ? 'verified' : 'saved_unverified';
|
|
2063
|
-
if (readback) state.activation.message = '停止状態を再取得して確認しました。';
|
|
2064
|
-
} catch (error) { const normalized = normalizeError(error); state.activation.error = normalized; state.activation.status = normalized.kind; }
|
|
2065
|
-
render(); notifyChange(); return state.activation;
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
async function loadRuns(id = contractId()) {
|
|
2069
|
-
setActiveStage('runs');
|
|
2070
|
-
if (!id) { state.runs.status = 'unknown'; render(); return state.runs; }
|
|
2071
|
-
state.runs.status = 'loading'; state.runs.error = null; render();
|
|
2072
|
-
try {
|
|
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());
|
|
2077
|
-
state.runs = { ...state.runs, ...normalized, status: normalized.state, error: null };
|
|
2078
|
-
const latestSafeTest = normalized.records.find((run) => run.mode === 'safe_test' && run.contractId === id);
|
|
2079
|
-
if (latestSafeTest) {
|
|
2080
|
-
stopSafeTestReadback();
|
|
2081
|
-
state.safeTest.runId = latestSafeTest.id;
|
|
2082
|
-
state.safeTest.contractId = id;
|
|
2083
|
-
rememberSafeTestRun(id, latestSafeTest.id);
|
|
2084
|
-
state.safeTest.result = latestSafeTest;
|
|
2085
|
-
state.safeTest.status = safeTestStatus(latestSafeTest, currentContract());
|
|
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 });
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
catch (error) { setError(state.runs, error); }
|
|
2096
|
-
render(); notifyChange(); return state.runs;
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
async function loadRunDetail(id = state.runs.selectedId, { renderAfter = true } = {}) {
|
|
2100
|
-
setActiveStage('runs');
|
|
2101
|
-
if (!id) { state.runs.status = 'unknown'; if (renderAfter) render(); return null; }
|
|
2102
|
-
state.runs.status = 'loading'; state.runs.error = null; state.runs.selectedId = id; if (renderAfter) render();
|
|
2103
|
-
try {
|
|
2104
|
-
const payload = await callApi(path('runDetail', { runId: id }));
|
|
2105
|
-
state.runs.detail = normalizeRun(payload); state.runs.status = 'ready';
|
|
2106
|
-
} catch (error) { setError(state.runs, error); }
|
|
2107
|
-
if (renderAfter) { render(); notifyChange(); }
|
|
2108
|
-
return state.runs.detail;
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
async function runAction(action, input = {}, id = state.runs.selectedId) {
|
|
2112
|
-
setActiveStage('runs');
|
|
2113
|
-
if (!id) return null;
|
|
2114
|
-
const detail = state.runs.detail ?? state.runs.records.find((item) => item.id === id);
|
|
2115
|
-
const current = objectValue(detail);
|
|
2116
|
-
if (action === 'retry' && current.status === 'stopped') {
|
|
2117
|
-
state.runAction = { status: 'unknown', error: { kind: 'unknown', message: STOPPED_RUN_RESTART_UNAVAILABLE }, draft: null };
|
|
2118
|
-
render(); notifyChange(); return state.runAction;
|
|
2119
|
-
}
|
|
2120
|
-
if (action === 'approve' && !normalizeApprovalTarget(current.pendingApproval, current.contractVersion).complete) {
|
|
2121
|
-
state.runAction = { status: 'unknown', error: { kind: 'unknown', message: '承認対象の差分を取得できないため承認できません。' }, draft: null };
|
|
2122
|
-
render(); notifyChange(); return state.runAction;
|
|
2123
|
-
}
|
|
2124
|
-
const body = { ...objectValue(input) };
|
|
2125
|
-
if (Number.isInteger(current.version)) body.expected_version = current.version;
|
|
2126
|
-
state.runAction = { status: 'loading', error: null, draft: body }; render();
|
|
2127
|
-
try {
|
|
2128
|
-
let targetName = 'runStop';
|
|
2129
|
-
if (action === 'approve' || action === 'reject') {
|
|
2130
|
-
targetName = 'decisions'; body.decision = action;
|
|
2131
|
-
body.approvalId = current.pendingApproval?.approvalId ?? current.pendingApproval?.approval_id ?? '';
|
|
2132
|
-
}
|
|
2133
|
-
else if (action === 'retry') targetName = 'resume';
|
|
2134
|
-
const payload = await callMutation(path(targetName, { runId: id }), { body, key: idempotencyKey(`mana-run-${action}`) });
|
|
2135
|
-
state.runAction.draft = null;
|
|
2136
|
-
const readback = await loadRunDetail(id, { renderAfter: false });
|
|
2137
|
-
state.runAction.status = readback ? 'verified' : isReadbackVerified(payload) ? 'verified' : 'saved_unverified';
|
|
2138
|
-
} catch (error) { state.runAction.status = normalizeError(error).kind; state.runAction.error = normalizeError(error); }
|
|
2139
|
-
render(); notifyChange(); return state.runAction;
|
|
2140
|
-
}
|
|
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
|
-
|
|
2416
|
-
function renderContractSection() {
|
|
2417
|
-
const section = createElement(document, 'section', { className: 'outcome-mana-section outcome-mana-contract', attrs: { 'aria-labelledby': 'outcome-mana-contract-heading' } });
|
|
2418
|
-
section.append(sectionHeading(document, '成果契約', state.contract.status, '期待する成果、対象範囲、根拠、保存先、完了条件を版付きで管理します。保存と有効化は別操作です。', 'outcome-mana-contract-heading'));
|
|
2419
|
-
const content = createElement(document, 'div', { className: 'outcome-mana-section-content' });
|
|
2420
|
-
if (['loading', 'empty', 'permission_denied', 'error_retryable', 'conflict', 'unknown'].includes(state.contract.status)) {
|
|
2421
|
-
const message = state.contract.status === 'empty'
|
|
2422
|
-
? stateMessage(document, EMPTY, { emptyTitle: '委任契約はありません', emptyText: '新しい下書きを作成すると、成果と完了条件を入力できます。' })
|
|
2423
|
-
: stateMessage(document, state.contract.status, { message: state.contract.error?.message, retry: loadContracts });
|
|
2424
|
-
if (message) content.append(message);
|
|
2425
|
-
if (state.contract.status === 'empty' && canManage(currentSession())) content.append(button(document, '新しい下書きを作成', () => { state.contract.detail = normalizeContract({ status: 'draft', project: currentProject().code }); state.contract.selectedId = null; state.contract.formDraft = {}; state.contract.status = 'ready'; render(); }));
|
|
2426
|
-
section.append(content); return section;
|
|
2427
|
-
}
|
|
2428
|
-
const list = createElement(document, 'div', { className: 'outcome-mana-contract-list', attrs: { role: 'list', 'aria-label': '委任契約一覧' } });
|
|
2429
|
-
state.contract.records.forEach((record) => {
|
|
2430
|
-
const item = createElement(document, 'div', { className: `outcome-mana-contract-list-item${record.contractId === state.contract.selectedId ? ' is-selected' : ''}`, attrs: { role: 'listitem' } });
|
|
2431
|
-
const select = button(document, `${present(record.outcome, '成果未確認')} / ${record.version === null ? '版未確認' : `v${record.version}`}`, () => { state.contract.selectedId = record.contractId; loadContractDetail(record.contractId); }, { className: 'outcome-mana-list-select', attrs: { 'aria-current': record.contractId === state.contract.selectedId ? 'true' : 'false' } });
|
|
2432
|
-
item.append(select, statusBadge(document, record.status)); list.append(item);
|
|
2433
|
-
});
|
|
2434
|
-
content.append(list);
|
|
2435
|
-
const contract = currentContract() ?? normalizeContract({ status: 'draft', project: currentProject().code });
|
|
2436
|
-
const values = contractFormValues(contract, state.contract.formDraft);
|
|
2437
|
-
const form = createElement(document, 'form', { className: 'outcome-mana-form', attrs: { 'aria-label': '成果契約の編集' } });
|
|
2438
|
-
const fields = {};
|
|
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);
|
|
2442
|
-
addField('outcome', '期待する成果', 'textarea', { attrs: { rows: '4', required: 'required', maxlength: '2000' }, helper: '手順ではなく、受け取りたい状態を具体的に書きます。' });
|
|
2443
|
-
addField('scope', '対象範囲', 'textarea', { attrs: { rows: '3', required: 'required', maxlength: '2000' }, helper: '対象プロジェクトや資料の範囲。サーバーでも検証します。' });
|
|
2444
|
-
if (!pickers) addField('inputRefs', '入力資料(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', required: 'required', maxlength: '4000' } });
|
|
2445
|
-
addField('knowledgeRefs', '参照する知識(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', maxlength: '4000' } });
|
|
2446
|
-
addField('judgmentRefs', '参照する判断(ID@版、1行1件)', 'textarea', { attrs: { rows: '3', maxlength: '4000' } });
|
|
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
|
-
}
|
|
2452
|
-
addField('environment', '保存環境', 'select', { options: [{ value: 'isolated', label: '隔離保存先' }, { value: 'production', label: '本番保存先' }], helper: '隔離試験は本番保存先へ書き込みません。' });
|
|
2453
|
-
addField('completionCriteria', '完了条件(id・説明・kind・expected、タブ区切り)', 'textarea', { attrs: { rows: '5', required: 'required', maxlength: '8000' }, helper: '例: criteria-1[Tab]本文を含む[Tab]content_includes[Tab]報告' });
|
|
2454
|
-
addField('ownerActorId', '責任者の主体ID', 'input', { attrs: { required: 'required', maxlength: '128' }, helper: '主体はサーバーで現在の組織権限と照合します。' });
|
|
2455
|
-
const limits = createElement(document, 'div', { className: 'outcome-mana-inline-fields' });
|
|
2456
|
-
for (const [name, label, helper] of [
|
|
2457
|
-
['maxAttempts', '最大試行回数'],
|
|
2458
|
-
['timeoutMs', 'タイムアウト(ms)'],
|
|
2459
|
-
['budgetUnits', '外部処理の実行上限', 'AI生成・保存・外部送信を新しく開始するたびに1回として数えます。1回の処理に含まれる保存確認などは追加で数えません。読み取り・確認、完了済み処理の再開は数えません。'],
|
|
2460
|
-
]) {
|
|
2461
|
-
const result = field(document, label, 'input', name, values[name], { attrs: { type: 'number', min: '1', required: 'required' }, helper }); fields[name] = result.input; limits.append(result.label);
|
|
2462
|
-
}
|
|
2463
|
-
form.append(limits);
|
|
2464
|
-
const formError = state.contract.error ? createElement(document, 'p', { className: 'outcome-mana-inline-error', text: state.contract.error.message, attrs: { role: 'alert' } }) : null;
|
|
2465
|
-
if (formError) form.append(formError);
|
|
2466
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
2467
|
-
const manage = canManage(currentSession());
|
|
2468
|
-
const isExisting = Boolean(contract.contractId);
|
|
2469
|
-
const save = button(document, isExisting ? '下書きを保存' : '下書きを作成', async () => {
|
|
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
|
-
}
|
|
2478
|
-
await saveContractDraft(contractDraftFromForm(fields), { create: !isExisting });
|
|
2479
|
-
}, { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !manage || state.contract.status === 'saving', attrs: { 'aria-label': isExisting ? '成果契約の下書きを保存' : '成果契約の下書きを作成' } });
|
|
2480
|
-
actions.append(save);
|
|
2481
|
-
if (isExisting) {
|
|
2482
|
-
actions.append(button(document, '複製', () => cloneContract(contract.contractId), { disabled: !manage || state.contract.status === 'saving' }));
|
|
2483
|
-
if (contract.status !== 'retired') actions.append(button(document, '廃止', () => retireContract(contract.contractId), { className: 'outcome-mana-button outcome-mana-button-danger', disabled: !manage || state.contract.status === 'saving' }));
|
|
2484
|
-
}
|
|
2485
|
-
form.append(actions);
|
|
2486
|
-
listen(form, 'submit', (event) => { event.preventDefault?.(); save.click?.(); });
|
|
2487
|
-
content.append(form); section.append(content); return section;
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
function renderAuthoritySection() {
|
|
2491
|
-
const section = createElement(document, 'section', { className: 'outcome-mana-section', attrs: { 'aria-labelledby': 'outcome-mana-authority-heading' } });
|
|
2492
|
-
section.append(sectionHeading(document, '権限マトリクス', state.authority.status, '操作・対象資源・プロジェクト・主体ごとに、自動許可・承認・禁止を設定します。選択だけで権限は拡大しません。', 'outcome-mana-authority-heading'));
|
|
2493
|
-
const content = createElement(document, 'div', { className: 'outcome-mana-section-content' });
|
|
2494
|
-
if (state.authority.status === 'idle') content.append(stateMessage(document, 'unknown', { message: '契約を選択すると、実際の権限マトリクスを確認できます.', retry: contractId() ? () => loadAuthority() : null }));
|
|
2495
|
-
else if (['loading', 'empty', 'permission_denied', 'error_retryable', 'conflict', 'unknown'].includes(state.authority.status)) {
|
|
2496
|
-
const message = stateMessage(document, state.authority.status, { emptyTitle: '権限設定は未登録です', emptyText: '上流の権限設定を確認してから追加します。', message: state.authority.error?.message, retry: () => loadAuthority() });
|
|
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
|
-
}
|
|
2511
|
-
} else {
|
|
2512
|
-
const matrix = state.authority.draft ?? state.authority.matrix;
|
|
2513
|
-
const table = createElement(document, 'div', { className: 'outcome-mana-authority-table', attrs: { role: 'table', 'aria-label': '操作権限マトリクス' } });
|
|
2514
|
-
const header = createElement(document, 'div', { className: 'outcome-mana-authority-row outcome-mana-authority-header', attrs: { role: 'row' } });
|
|
2515
|
-
['操作', '対象資源', 'プロジェクト', '主体', '設定'].forEach((label) => header.append(createElement(document, 'span', { text: label, attrs: { role: 'columnheader' } }))); table.append(header);
|
|
2516
|
-
const controls = [];
|
|
2517
|
-
matrix.forEach((entry, index) => {
|
|
2518
|
-
const row = createElement(document, 'div', { className: 'outcome-mana-authority-row', attrs: { role: 'row' } });
|
|
2519
|
-
row.append(createElement(document, 'span', { text: entry.operation }), createElement(document, 'span', { text: entry.target }), createElement(document, 'span', { text: entry.project }), createElement(document, 'span', { text: entry.actor }));
|
|
2520
|
-
const select = createElement(document, 'select', { className: 'outcome-mana-input', attrs: { 'aria-label': `${entry.operation}の権限設定` } });
|
|
2521
|
-
Object.entries(AUTHORITY_LABELS).forEach(([value, label]) => select.append(createElement(document, 'option', { text: label, attrs: { value } })));
|
|
2522
|
-
select.value = entry.policy ?? '';
|
|
2523
|
-
controls.push({ select, index }); row.append(select); table.append(row);
|
|
2524
|
-
});
|
|
2525
|
-
content.append(table);
|
|
2526
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
2527
|
-
actions.append(button(document, '権限設定を保存', () => {
|
|
2528
|
-
const next = matrix.map((entry, index) => ({ ...entry, policy: controls[index]?.select.value || null }));
|
|
2529
|
-
saveAuthority(next);
|
|
2530
|
-
}, { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) || state.authority.status === 'saving' }));
|
|
2531
|
-
content.append(actions);
|
|
2532
|
-
}
|
|
2533
|
-
section.append(content); return section;
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
function renderTriggersSection() {
|
|
2537
|
-
const section = createElement(document, 'section', { className: 'outcome-mana-section', attrs: { 'aria-labelledby': 'outcome-mana-triggers-heading' } });
|
|
2538
|
-
const contract = currentContract();
|
|
2539
|
-
section.append(sectionHeading(document, '起動条件と有効化', contract?.status ?? state.triggers.status, '手動・定期・文書追加を設定し、接続・権限・入力・保存先・完了条件を確認してから明示的に有効化します。', 'outcome-mana-triggers-heading'));
|
|
2540
|
-
const content = createElement(document, 'div', { className: 'outcome-mana-section-content' });
|
|
2541
|
-
if (!contract) { content.append(stateMessage(document, 'unknown', { message: '契約を選択すると起動条件を設定できます。' })); section.append(content); return section; }
|
|
2542
|
-
const trigger = triggerFromContract(contract, state.triggers.draft);
|
|
2543
|
-
const availability = triggerAvailability(trigger);
|
|
2544
|
-
if (state.triggers.status === 'permission_denied' || state.triggers.status === 'error_retryable' || state.triggers.status === 'conflict') {
|
|
2545
|
-
const message = stateMessage(document, state.triggers.status, { message: state.triggers.error?.message, retry: () => loadContractDetail(contract.contractId) }); if (message) content.append(message);
|
|
2546
|
-
}
|
|
2547
|
-
const form = createElement(document, 'form', { className: 'outcome-mana-trigger-form', attrs: { 'aria-label': '起動条件の編集' } });
|
|
2548
|
-
const radioGroup = createElement(document, 'fieldset', { className: 'outcome-mana-trigger-options' });
|
|
2549
|
-
radioGroup.append(createElement(document, 'legend', { text: '起動方式' }));
|
|
2550
|
-
const triggerRadios = [];
|
|
2551
|
-
for (const [value, label] of Object.entries(TRIGGER_LABELS)) {
|
|
2552
|
-
const item = createElement(document, 'label', { className: 'outcome-mana-choice' });
|
|
2553
|
-
const radio = createElement(document, 'input', { attrs: { type: 'radio', name: 'outcome-mana-trigger-type', value } }); radio.checked = trigger.type === value; item.append(radio, createElement(document, 'span', { text: label })); radioGroup.append(item);
|
|
2554
|
-
triggerRadios.push(radio);
|
|
2555
|
-
}
|
|
2556
|
-
form.append(radioGroup);
|
|
2557
|
-
const controls = {};
|
|
2558
|
-
const schedule = field(document, 'スケジュールID', 'input', 'scheduleId', trigger.scheduleId, { attrs: { maxlength: '128' }, helper: '定期実行の接続が未確認なら有効化できません。' });
|
|
2559
|
-
const subscription = field(document, '文書購読ID', 'input', 'subscriptionId', trigger.subscriptionId, { attrs: { maxlength: '256' } });
|
|
2560
|
-
const provider = field(document, '文書プロバイダー', 'input', 'provider', trigger.provider, { attrs: { maxlength: '128' } });
|
|
2561
|
-
controls.scheduleId = schedule.input; controls.subscriptionId = subscription.input; controls.provider = provider.input;
|
|
2562
|
-
form.append(schedule.label, subscription.label, provider.label);
|
|
2563
|
-
const triggerStatus = availability.available
|
|
2564
|
-
? statusBadge(document, 'verified', '利用準備確認済み')
|
|
2565
|
-
: createElement(document, 'p', { className: 'outcome-mana-inline-error', text: availability.reason, attrs: { role: 'status' } });
|
|
2566
|
-
form.append(createElement(document, 'div', { className: 'outcome-mana-trigger-status' }), triggerStatus);
|
|
2567
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
2568
|
-
actions.append(button(document, '起動条件を保存', () => {
|
|
2569
|
-
const selected = triggerRadios.find((item) => item.checked)?.value ?? trigger.type;
|
|
2570
|
-
saveTriggers({ type: selected, scheduleId: controls.scheduleId.value, subscriptionId: controls.subscriptionId.value, provider: controls.provider.value });
|
|
2571
|
-
}, { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) }));
|
|
2572
|
-
const isActive = contract.status === 'active';
|
|
2573
|
-
const effectiveTestStatus = state.safeTest.status === 'loading' ? 'loading' : state.safeTest.result
|
|
2574
|
-
? safeTestStatus(state.safeTest.result, contract) : state.safeTest.status;
|
|
2575
|
-
const testVerified = effectiveTestStatus === 'verified';
|
|
2576
|
-
const authorityVerified = state.authority.status === 'ready' && state.authority.matrix.length > 0;
|
|
2577
|
-
actions.append(button(document, isActive ? '委任を停止' : '委任を有効化', () => isActive ? stopActivation(contract.contractId) : activate(contract.contractId), { className: isActive ? 'outcome-mana-button outcome-mana-button-danger' : 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) || state.activation.status === 'loading' || (!isActive && (!testVerified || !availability.available || !authorityVerified)) }));
|
|
2578
|
-
if (!isActive && !testVerified) actions.append(createElement(document, 'small', { className: 'outcome-mana-helper', text: '有効化前に隔離試験を実行し、完了条件の検証済み結果を確認してください。' }));
|
|
2579
|
-
if (!isActive && !authorityVerified) actions.append(createElement(document, 'small', { className: 'outcome-mana-helper', text: '有効化前に権限マトリクスのreadbackを確認してください。' }));
|
|
2580
|
-
if (state.activation.error) actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: state.activation.error.message, attrs: { role: 'alert' } }));
|
|
2581
|
-
if (state.activation.message) actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-success', text: state.activation.message, attrs: { role: 'status' } }));
|
|
2582
|
-
form.append(actions); listen(form, 'submit', (event) => event.preventDefault?.()); content.append(form); section.append(content); return section;
|
|
2583
|
-
}
|
|
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
|
-
|
|
2604
|
-
function renderSafeTestSection() {
|
|
2605
|
-
const section = createElement(document, 'section', { className: 'outcome-mana-section', attrs: { 'aria-labelledby': 'outcome-mana-safe-test-heading' } });
|
|
2606
|
-
const contract = currentContract();
|
|
2607
|
-
const inFlight = Boolean(state.safeTest.runId && state.safeTest.status !== 'unknown'
|
|
2608
|
-
&& ['queued', 'running'].includes(normalizeRun(state.safeTest.result).status));
|
|
2609
|
-
const effectiveTestStatus = state.safeTest.status === 'loading' ? 'loading' : state.safeTest.result
|
|
2610
|
-
? safeTestStatus(state.safeTest.result, contract) : state.safeTest.status;
|
|
2611
|
-
section.append(sectionHeading(document, '隔離試験', effectiveTestStatus, '契約版とサンプル入力で試験します。外部送信と本番書き込みはリクエストでも遮断し、未実行の操作を成功とは表示しません。', 'outcome-mana-safe-test-heading'));
|
|
2612
|
-
const content = createElement(document, 'div', { className: 'outcome-mana-section-content' });
|
|
2613
|
-
if (!contract) { content.append(stateMessage(document, 'unknown', { message: '契約を選択すると隔離試験を実行できます。' })); section.append(content); return section; }
|
|
2614
|
-
const form = createElement(document, 'form', { className: 'outcome-mana-safe-test-form', attrs: { 'aria-label': '隔離試験の実行' } });
|
|
2615
|
-
const draft = state.safeTest.formDraft ?? {};
|
|
2616
|
-
const version = field(document, '試験する契約版', 'input', 'contractVersion', draft.contract_version ?? contract.version ?? '', { attrs: { type: 'number', min: '1', required: 'required' }, helper: '実行は選択した版へ固定されます。' });
|
|
2617
|
-
const sample = field(document, 'サンプル入力', 'textarea', 'sampleInput', draft.sample_input ?? '', { attrs: { rows: '6', required: 'required', maxlength: '12000' }, helper: '送信先・本番データではなく、試験用の入力を指定します。' });
|
|
2618
|
-
form.append(version.label, sample.label);
|
|
2619
|
-
const safety = createElement(document, 'p', { className: 'outcome-mana-safe-note', text: '隔離モード: 外部送信なし / 本番書き込みなし / 成果物は隔離保存先のみ' }); form.append(safety);
|
|
2620
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
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 }));
|
|
2622
|
-
form.append(actions); listen(form, 'submit', (event) => event.preventDefault?.()); content.append(form);
|
|
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: '結果を再取得' }));
|
|
2629
|
-
if (effectiveTestStatus === 'verified') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-success', text: 'この設定で試験済み', attrs: { role: 'status' } }));
|
|
2630
|
-
if (effectiveTestStatus === 'stale') 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' } }));
|
|
2632
|
-
if (effectiveTestStatus === 'configuration_snapshot_missing') content.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '試験時の設定記録が不足しているため、現在の設定と一致するか確認できません。再試験が必要です。', attrs: { role: 'status' } }));
|
|
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
|
-
}
|
|
2644
|
-
if (state.safeTest.result) {
|
|
2645
|
-
const result = objectValue(state.safeTest.result);
|
|
2646
|
-
const execution = objectValue(result.execution ?? result.testRun ?? result.run);
|
|
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);
|
|
2660
|
-
content.append(evidenceList(document, '予定した操作', result.operations ?? execution.operations));
|
|
2661
|
-
content.append(evidenceList(document, '必要な承認', result.approvals ?? execution.approvals));
|
|
2662
|
-
content.append(evidenceList(document, '参照した根拠', result.evidence ?? execution.evidence));
|
|
2663
|
-
content.append(evidenceList(document, '隔離成果物', run.artifacts));
|
|
2664
|
-
content.append(evidenceList(document, '完了条件の評価', criterionSummaries(run.completion.criteria)));
|
|
2665
|
-
}
|
|
2666
|
-
section.append(content); return section;
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
function renderRunDetail() {
|
|
2670
|
-
const detail = state.runs.detail;
|
|
2671
|
-
const section = createElement(document, 'div', { className: 'outcome-mana-run-detail' });
|
|
2672
|
-
if (!detail) { section.append(stateMessage(document, 'unknown', { message: '実行を選択すると、起動理由・契約版・段階・成果物・完了条件・再開地点を確認できます。' })); return section; }
|
|
2673
|
-
section.append(createElement(document, 'h3', { text: '実行詳細' }), statusBadge(document, detail.status));
|
|
2674
|
-
section.append(definitionList(document, [
|
|
2675
|
-
['実行ID', detail.id],
|
|
2676
|
-
['起動理由', triggerLabel(detail.trigger ?? detail.reason)],
|
|
2677
|
-
['契約版', detail.contractVersion === null ? UNKNOWN : `v${detail.contractVersion}`],
|
|
2678
|
-
['設定ハッシュ', detail.configHash],
|
|
2679
|
-
['現在の処理段階', detail.stage],
|
|
2680
|
-
['再開地点', detail.resumePoint],
|
|
2681
|
-
['成果物readback', approvalDisplayValue(detail.artifactReadback)],
|
|
2682
|
-
['判断結果', approvalDisplayValue(detail.judgment)],
|
|
2683
|
-
['判断理由', detail.judgmentReason],
|
|
2684
|
-
['開始', dateLabel(detail.startedAt)],
|
|
2685
|
-
['更新', dateLabel(detail.updatedAt)],
|
|
2686
|
-
]));
|
|
2687
|
-
section.append(stageList(document, detail.stages));
|
|
2688
|
-
const preflight = renderPreflightChecks(detail.preflight);
|
|
2689
|
-
if (preflight) section.append(preflight);
|
|
2690
|
-
section.append(evidenceList(document, '参照した根拠', detail.evidence), evidenceList(document, '成果物', detail.artifacts), evidenceList(document, '完了条件', detail.completion.criteria));
|
|
2691
|
-
if (detail.completion.reason) section.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: detail.completion.reason, attrs: { role: 'alert' } }));
|
|
2692
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
2693
|
-
const manageable = canManage(currentSession());
|
|
2694
|
-
if (detail.status === 'waiting_approval') {
|
|
2695
|
-
const approval = normalizeApprovalTarget(detail.pendingApproval, detail.contractVersion);
|
|
2696
|
-
section.append(createElement(document, 'h4', { text: '承認対象の差分と外部影響' }));
|
|
2697
|
-
section.append(definitionList(document, [
|
|
2698
|
-
['差分', approvalDisplayValue(approval.diff)],
|
|
2699
|
-
['操作', approvalDisplayValue(approval.operation)],
|
|
2700
|
-
['対象リソース', approvalDisplayValue(approval.resource)],
|
|
2701
|
-
['payload hash', approvalDisplayValue(approval.payloadHash)],
|
|
2702
|
-
['外部送信先', approvalDisplayValue(approval.destination)],
|
|
2703
|
-
['契約版', approval.contractVersion === null ? UNKNOWN : `v${approval.contractVersion}`],
|
|
2704
|
-
['承認期限', dateLabel(approval.expiresAt)],
|
|
2705
|
-
]));
|
|
2706
|
-
if (!approval.complete) section.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '承認対象の差分を取得できないため承認できません。未確認項目を補ってから再読込してください。', attrs: { role: 'alert' } }));
|
|
2707
|
-
const reason = field(document, '却下理由(必須)', 'textarea', 'rejectReason', state.runAction.draft?.reason ?? '', { attrs: { rows: '3', maxlength: '2000' } });
|
|
2708
|
-
section.append(reason.label);
|
|
2709
|
-
actions.append(button(document, '承認する', () => runAction('approve'), { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !manageable || !approval.complete }), button(document, '却下する', () => runAction('reject', { reason: reason.input.value }), { className: 'outcome-mana-button outcome-mana-button-danger', disabled: !manageable }));
|
|
2710
|
-
}
|
|
2711
|
-
if (detail.status === 'failed') actions.append(button(document, '失敗地点から再開', () => runAction('retry'), { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !manageable }));
|
|
2712
|
-
if (detail.status === 'stopped') section.append(createElement(document, 'p', { className: 'outcome-mana-muted', text: STOPPED_RUN_RESTART_UNAVAILABLE }));
|
|
2713
|
-
if (['queued', 'running', 'waiting_approval'].includes(detail.status)) actions.append(button(document, 'この実行を停止', () => { if (confirmAction('この実行を停止します。続けますか?')) runAction('stop'); }, { className: 'outcome-mana-button outcome-mana-button-danger', disabled: !manageable }));
|
|
2714
|
-
if (state.runAction.error) actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: state.runAction.error.message, attrs: { role: 'alert' } }));
|
|
2715
|
-
if (state.runAction.status === 'saved_unverified') actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '操作は保存されましたが、最新の実行状態を確認できていません。成功とは扱いません。', attrs: { role: 'alert' } }));
|
|
2716
|
-
section.append(actions); return section;
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
function renderRunsSection() {
|
|
2720
|
-
const section = createElement(document, 'section', { className: 'outcome-mana-section outcome-mana-runs', attrs: { 'aria-labelledby': 'outcome-mana-runs-heading' } });
|
|
2721
|
-
section.append(sectionHeading(document, '実行履歴', state.runs.status, '起動理由、契約版、処理段階、承認、成果物、完了検証を実行単位で確認します。', 'outcome-mana-runs-heading'));
|
|
2722
|
-
const content = createElement(document, 'div', { className: 'outcome-mana-section-content' });
|
|
2723
|
-
const toolbar = createElement(document, 'div', { className: 'outcome-mana-toolbar' }); toolbar.append(button(document, '実行履歴を読み込む', () => loadRuns(), { className: 'outcome-mana-button outcome-mana-button-secondary', disabled: state.runs.status === 'loading' })); content.append(toolbar);
|
|
2724
|
-
if (['loading', 'empty', 'permission_denied', 'error_retryable', 'conflict', 'unknown'].includes(state.runs.status)) {
|
|
2725
|
-
const message = stateMessage(document, state.runs.status, { emptyTitle: '実行履歴はありません', emptyText: '有効化後に起動した実行がここへ表示されます。', message: state.runs.error?.message, retry: () => loadRuns() }); if (message) content.append(message);
|
|
2726
|
-
} else {
|
|
2727
|
-
const list = createElement(document, 'div', { className: 'outcome-mana-run-list outcome-mana-run-table', attrs: { role: 'list', 'aria-label': 'Mana実行履歴' } });
|
|
2728
|
-
const tableHeader = createElement(document, 'div', { className: 'outcome-mana-run-table-header', attrs: { role: 'row' } });
|
|
2729
|
-
['起動理由', '契約版', '状態', '更新'].forEach((label) => tableHeader.append(createElement(document, 'span', { text: label, attrs: { role: 'columnheader' } })));
|
|
2730
|
-
list.append(tableHeader);
|
|
2731
|
-
state.runs.records.forEach((run) => {
|
|
2732
|
-
const item = createElement(document, 'div', { className: `outcome-mana-run-item${run.id === state.runs.selectedId ? ' is-selected' : ''}`, attrs: { role: 'listitem' } });
|
|
2733
|
-
item.append(
|
|
2734
|
-
button(document, runReasonLabel(run.reason, run.trigger), () => loadRunDetail(run.id), { className: 'outcome-mana-list-select', attrs: { 'aria-current': run.id === state.runs.selectedId ? 'true' : 'false' } }),
|
|
2735
|
-
createElement(document, 'span', { className: 'outcome-mana-run-version', text: run.contractVersion === null ? '版未確認' : `v${run.contractVersion}` }),
|
|
2736
|
-
statusBadge(document, run.status),
|
|
2737
|
-
createElement(document, 'time', { className: 'outcome-mana-run-updated', text: dateLabel(run.updatedAt ?? run.startedAt) }),
|
|
2738
|
-
); list.append(item);
|
|
2739
|
-
});
|
|
2740
|
-
content.append(list, renderRunDetail());
|
|
2741
|
-
}
|
|
2742
|
-
section.append(content); return section;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
function renderWorkspaceNavigation() {
|
|
2746
|
-
const nav = createElement(document, 'nav', { className: 'outcome-mana-view-nav', attrs: { 'aria-label': 'Mana管理画面' } });
|
|
2747
|
-
const activeView = derivedActiveView() === 'delegation_settings' ? 'overview' : derivedActiveView();
|
|
2748
|
-
for (const view of MANA_VIEWS) {
|
|
2749
|
-
nav.append(button(document, view.label, () => {
|
|
2750
|
-
setActiveView(view.id);
|
|
2751
|
-
if (view.id === 'runs' && state.runs.status === 'idle' && contractId()) loadRuns();
|
|
2752
|
-
else if (view.id === 'connections' && state.connections.status === 'unknown') loadConnections();
|
|
2753
|
-
else if (view.id === 'settings' && state.manaSettings.status === 'unknown') loadManaSettings();
|
|
2754
|
-
else render();
|
|
2755
|
-
}, {
|
|
2756
|
-
className: `outcome-mana-view-tab${activeView === view.id ? ' is-active' : ''}`,
|
|
2757
|
-
attrs: { 'aria-current': activeView === view.id ? 'page' : 'false' },
|
|
2758
|
-
}));
|
|
2759
|
-
}
|
|
2760
|
-
return nav;
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
function renderOverviewView() {
|
|
2764
|
-
const view = createElement(document, 'section', { className: 'outcome-mana-overview', attrs: { 'aria-labelledby': 'outcome-mana-overview-heading' } });
|
|
2765
|
-
async function startNewDelegation() {
|
|
2766
|
-
state.contract.detail = null;
|
|
2767
|
-
state.contract.selectedId = null;
|
|
2768
|
-
state.contract.formDraft = {};
|
|
2769
|
-
setActiveStage('contract');
|
|
2770
|
-
render();
|
|
2771
|
-
await ensureConnectionsForDelegation();
|
|
2772
|
-
}
|
|
2773
|
-
const heading = createElement(document, 'div', { className: 'outcome-mana-view-heading' });
|
|
2774
|
-
heading.append(group(document, 'div', {}, [
|
|
2775
|
-
createElement(document, 'h2', { id: 'outcome-mana-overview-heading', text: '委任一覧' }),
|
|
2776
|
-
createElement(document, 'p', { text: 'Manaに任せた仕事の状態と、次に必要な操作を比較します。' }),
|
|
2777
|
-
]));
|
|
2778
|
-
if (state.contract.status !== 'empty') {
|
|
2779
|
-
heading.append(button(document, '新しい委任を作成', startNewDelegation, { className: 'outcome-mana-button outcome-mana-button-primary', disabled: !canManage(currentSession()) }));
|
|
2780
|
-
}
|
|
2781
|
-
view.append(heading);
|
|
2782
|
-
const hasContractSnapshot = ['ready', 'empty'].includes(state.contract.status);
|
|
2783
|
-
const records = state.contract.records;
|
|
2784
|
-
// The runs endpoint is scoped to a single delegation, so it cannot
|
|
2785
|
-
// support a workspace-wide running count without an aggregate API.
|
|
2786
|
-
const runningCount = UNKNOWN;
|
|
2787
|
-
const kpis = [
|
|
2788
|
-
['登録委任', hasContractSnapshot ? records.length : UNKNOWN, 'contract-count'],
|
|
2789
|
-
['有効', hasContractSnapshot ? records.filter((item) => item.status === 'active').length : UNKNOWN, 'active-count'],
|
|
2790
|
-
['確認待ち', hasContractSnapshot ? records.filter((item) => ['draft', 'waiting_approval', 'saved_unverified'].includes(item.status)).length : UNKNOWN, 'waiting-count'],
|
|
2791
|
-
['実行中', runningCount, 'running-count'],
|
|
2792
|
-
];
|
|
2793
|
-
const kpiGrid = createElement(document, 'div', { className: 'outcome-mana-kpi-grid', attrs: { 'aria-label': 'Mana委任の概要指標' } });
|
|
2794
|
-
kpis.forEach(([label, value, id]) => {
|
|
2795
|
-
const card = createElement(document, 'article', { className: 'outcome-mana-kpi-card', attrs: { 'data-kpi': id } });
|
|
2796
|
-
card.append(createElement(document, 'span', { className: 'outcome-mana-kpi-label', text: label }), createElement(document, 'strong', { className: 'outcome-mana-kpi-value', text: String(value) }));
|
|
2797
|
-
kpiGrid.append(card);
|
|
2798
|
-
});
|
|
2799
|
-
view.append(kpiGrid);
|
|
2800
|
-
if (state.contract.status === 'empty') {
|
|
2801
|
-
const empty = createElement(document, 'section', {
|
|
2802
|
-
className: 'outcome-mana-state outcome-mana-empty outcome-mana-empty-workspace',
|
|
2803
|
-
attrs: { role: 'region', 'aria-labelledby': 'outcome-mana-empty-workspace-heading' },
|
|
2804
|
-
});
|
|
2805
|
-
empty.append(
|
|
2806
|
-
createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '最初の委任' }),
|
|
2807
|
-
createElement(document, 'h3', { id: 'outcome-mana-empty-workspace-heading', text: 'Manaに仕事を委任しましょう' }),
|
|
2808
|
-
createElement(document, 'p', { text: '登録された委任はありません。成果と責任者を決め、使える資源と起動条件を確認してから安全試験へ進めます。' }),
|
|
2809
|
-
);
|
|
2810
|
-
const steps = createElement(document, 'ol', { className: 'outcome-mana-empty-steps' });
|
|
2811
|
-
[
|
|
2812
|
-
['成果と責任者', 'Manaに任せる結果と、その結果を確認する人を決めます。'],
|
|
2813
|
-
['資源と起動条件', '委任で使える資源と、実行を始める条件を設定します。'],
|
|
2814
|
-
['安全試験と有効化', '保存後に安全試験を行い、内容を確認してから有効化します。'],
|
|
2815
|
-
].forEach(([title, description], index) => {
|
|
2816
|
-
const item = createElement(document, 'li', { className: 'outcome-mana-empty-step' });
|
|
2817
|
-
item.append(createElement(document, 'span', { className: 'outcome-mana-empty-step-number', text: String(index + 1) }));
|
|
2818
|
-
item.append(group(document, 'span', { className: 'outcome-mana-empty-step-copy' }, [
|
|
2819
|
-
createElement(document, 'strong', { text: title }),
|
|
2820
|
-
createElement(document, 'small', { text: description }),
|
|
2821
|
-
]));
|
|
2822
|
-
steps.append(item);
|
|
2823
|
-
});
|
|
2824
|
-
empty.append(steps);
|
|
2825
|
-
empty.append(button(document, '成果契約を入力する', startNewDelegation, {
|
|
2826
|
-
className: 'outcome-mana-button outcome-mana-button-primary',
|
|
2827
|
-
disabled: !canManage(currentSession()),
|
|
2828
|
-
}));
|
|
2829
|
-
if (!canManage(currentSession())) {
|
|
2830
|
-
empty.append(createElement(document, 'small', { className: 'outcome-mana-helper', text: '委任を作成できる権限を持つ管理者に依頼してください。' }));
|
|
2831
|
-
}
|
|
2832
|
-
view.append(empty);
|
|
2833
|
-
return view;
|
|
2834
|
-
}
|
|
2835
|
-
if (['loading', 'permission_denied', 'error_retryable', 'unknown'].includes(state.contract.status)) {
|
|
2836
|
-
view.append(stateMessage(document, state.contract.status, { message: state.contract.error?.message, retry: loadContracts }));
|
|
2837
|
-
return view;
|
|
2838
|
-
}
|
|
2839
|
-
const layout = createElement(document, 'div', { className: 'outcome-mana-overview-layout' });
|
|
2840
|
-
const list = createElement(document, 'div', { className: 'outcome-mana-delegation-list', attrs: { role: 'list', 'aria-label': 'Mana委任一覧' } });
|
|
2841
|
-
for (const contract of state.contract.records) {
|
|
2842
|
-
const selected = contract.contractId === state.contract.selectedId;
|
|
2843
|
-
const item = button(document, '', async () => { await loadContractDetail(contract.contractId); setActiveView('overview'); render(); }, {
|
|
2844
|
-
className: `outcome-mana-delegation-card${selected ? ' is-selected' : ''}`,
|
|
2845
|
-
attrs: { role: 'listitem', 'aria-current': selected ? 'true' : 'false' },
|
|
2846
|
-
});
|
|
2847
|
-
const copy = createElement(document, 'span', { className: 'outcome-mana-delegation-copy' });
|
|
2848
|
-
const nextAction = contract.nextAction || (contract.status === 'active' ? '実行履歴を確認' : contract.status === 'draft' ? '委任設定を完了' : '状態を確認');
|
|
2849
|
-
copy.append(
|
|
2850
|
-
createElement(document, 'strong', { text: present(contract.outcome, '成果未登録') }),
|
|
2851
|
-
createElement(document, 'small', { text: `${triggerLabel(contract.trigger?.type)} · v${contract.version ?? UNKNOWN}` }),
|
|
2852
|
-
createElement(document, 'small', { text: `次の操作: ${nextAction}` }),
|
|
2853
|
-
);
|
|
2854
|
-
item.append(copy, statusBadge(document, contract.status));
|
|
2855
|
-
list.append(item);
|
|
2856
|
-
}
|
|
2857
|
-
const selected = currentContract();
|
|
2858
|
-
const inspector = createElement(document, 'aside', { className: 'outcome-mana-inspector', attrs: { 'aria-label': '選択した委任の概要' } });
|
|
2859
|
-
if (!selected) inspector.append(stateMessage(document, 'unknown', { message: '委任を選ぶと成果、責任者、起動条件、最終実行を確認できます。' }));
|
|
2860
|
-
else {
|
|
2861
|
-
inspector.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '選択中の委任' }), createElement(document, 'h3', { text: present(selected.outcome, '成果未登録') }));
|
|
2862
|
-
inspector.append(definitionList(document, [
|
|
2863
|
-
['責任者', present(selected.owner?.actorId)], ['対象範囲', present(selected.scope)], ['起動条件', triggerLabel(selected.trigger?.type)],
|
|
2864
|
-
['契約版', selected.version === null ? UNKNOWN : `v${selected.version}`], ['最終実行', state.runs.records[0] ? dateLabel(state.runs.records[0].updatedAt ?? state.runs.records[0].startedAt) : UNKNOWN],
|
|
2865
|
-
]));
|
|
2866
|
-
inspector.append(button(document, '委任設定を開く', async () => {
|
|
2867
|
-
setActiveView('delegation_settings');
|
|
2868
|
-
render();
|
|
2869
|
-
await ensureConnectionsForDelegation();
|
|
2870
|
-
}, { className: 'outcome-mana-button outcome-mana-button-primary' }));
|
|
2871
|
-
}
|
|
2872
|
-
layout.append(list, inspector); view.append(layout); return view;
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
function projectConnectors() {
|
|
2876
|
-
const project = currentProject();
|
|
2877
|
-
const source = project.connections ?? project.connectors ?? project.foundationConnections ?? project.foundation_connections;
|
|
2878
|
-
const values = Array.isArray(source) ? source : Object.entries(objectValue(source)).map(([id, value]) => ({ id, ...objectValue(value) }));
|
|
2879
|
-
const byId = new Map(values.map((item) => [normalizeConnector(item).id.toLowerCase(), normalizeConnector(item)]));
|
|
2880
|
-
const registeredById = new Map(connectorRecordsFromProject().map((item) => [item.id.toLowerCase(), item]));
|
|
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) => ({
|
|
2885
|
-
...definition,
|
|
2886
|
-
...(registeredById.get(definition.id) ?? {}),
|
|
2887
|
-
...(byId.get(definition.id) ?? {}),
|
|
2888
|
-
...(loadedById.get(definition.id) ?? {}),
|
|
2889
|
-
label: definition.label,
|
|
2890
|
-
description: definition.description,
|
|
2891
|
-
}));
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
function renderConnectionsView() {
|
|
2895
|
-
const view = createElement(document, 'section', { className: 'outcome-mana-connections', attrs: { 'aria-labelledby': 'outcome-mana-connections-heading' } });
|
|
2896
|
-
const headingCopy = group(document, 'div', {}, [createElement(document, 'h2', { id: 'outcome-mana-connections-heading', text: '接続状態' }), createElement(document, 'p', { text: 'Mana runtimeの組織接続と、選択中の委任が利用できる資源を分けて表示します。' })]);
|
|
2897
|
-
const headingActions = createElement(document, 'div', { className: 'outcome-mana-toolbar' });
|
|
2898
|
-
headingActions.append(button(document, state.connections.status === 'loading' ? '確認中…' : '接続を再確認', () => loadConnections(), { className: 'outcome-mana-button outcome-mana-button-secondary', disabled: state.connections.status === 'loading' }));
|
|
2899
|
-
view.append(group(document, 'div', { className: 'outcome-mana-view-heading' }, [headingCopy, headingActions]));
|
|
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();
|
|
2906
|
-
if (!state.connections.selectedId && connectors.length) state.connections.selectedId = connectors[0].id;
|
|
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
|
-
}
|
|
2912
|
-
const table = createElement(document, 'div', { className: 'outcome-mana-connection-table', attrs: { role: 'table', 'aria-label': 'Mana組織接続一覧' } });
|
|
2913
|
-
const header = createElement(document, 'div', { className: 'outcome-mana-connection-row outcome-mana-connection-header', attrs: { role: 'row' } });
|
|
2914
|
-
['接続', '状態', 'アカウント', catalogMode ? 'このプロジェクトで利用' : '登録資源', '最終確認'].forEach((label) => header.append(createElement(document, 'span', { text: label, attrs: { role: 'columnheader' } })));
|
|
2915
|
-
table.append(header);
|
|
2916
|
-
for (const connector of connectors) {
|
|
2917
|
-
const selected = connector.id === state.connections.selectedId;
|
|
2918
|
-
const row = button(document, '', () => {
|
|
2919
|
-
state.connections.selectedId = connector.id;
|
|
2920
|
-
state.connections.detail = connector;
|
|
2921
|
-
render();
|
|
2922
|
-
}, { className: `outcome-mana-connection-row${selected ? ' is-selected' : ''}`, attrs: { role: 'row', 'aria-current': selected ? 'true' : 'false' } });
|
|
2923
|
-
row.append(
|
|
2924
|
-
connectorIdentity(document, connector, { attrs: { role: 'cell' } }),
|
|
2925
|
-
group(document, 'span', { attrs: { role: 'cell' } }, [statusBadge(document, connector.status ?? 'unknown', connectionStatusLabel(connector.status))]),
|
|
2926
|
-
createElement(document, 'span', { text: present(connector.account), 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
|
-
}),
|
|
2933
|
-
createElement(document, 'span', { text: dateLabel(connector.checkedAt), attrs: { role: 'cell' } }),
|
|
2934
|
-
);
|
|
2935
|
-
table.append(row);
|
|
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
|
-
}
|
|
2956
|
-
const contract = currentContract();
|
|
2957
|
-
const inspector = createElement(document, 'aside', { className: 'outcome-mana-connection-inspector', attrs: { 'aria-label': '選択した接続の詳細' } });
|
|
2958
|
-
inspector.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '選択中の接続' }));
|
|
2959
|
-
if (selectedConnector) {
|
|
2960
|
-
inspector.append(connectorIdentity(document, selectedConnector, { large: true }));
|
|
2961
|
-
inspector.append(definitionList(document, [['接続状態', connectionStatusLabel(selectedConnector.status)], ['接続先', selectedConnector.account], ['確認日時', dateLabel(selectedConnector.checkedAt)], ['確認理由', connectionReasonLabel(selectedConnector.reason)]]));
|
|
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));
|
|
2968
|
-
} else inspector.append(stateMessage(document, 'unknown', { message: '接続を選ぶと登録資源と確認状態を表示します。' }));
|
|
2969
|
-
const allowed = createElement(document, 'aside', { className: 'outcome-mana-resource-panel' });
|
|
2970
|
-
allowed.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: catalogMode ? '3. 委任単位の許可範囲' : '委任単位の許可範囲' }), createElement(document, 'h3', { text: 'この委任が利用できる資源' }));
|
|
2971
|
-
if (!contract) allowed.append(stateMessage(document, 'unknown', { message: '委任を選ぶと、契約版に固定された参照先と保存先を確認できます。' }));
|
|
2972
|
-
else {
|
|
2973
|
-
const permitted = permittedResources(contract);
|
|
2974
|
-
allowed.append(evidenceList(document, '許可リソース', permitted));
|
|
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)]]));
|
|
2976
|
-
}
|
|
2977
|
-
if (!catalogMode) view.append(table);
|
|
2978
|
-
view.append(group(document, 'div', { className: 'outcome-mana-connection-detail-layout' }, [inspector, allowed])); return view;
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
function renderDelegationSettings() {
|
|
2982
|
-
const view = createElement(document, 'section', { className: 'outcome-mana-settings-workspace', attrs: { 'aria-labelledby': 'outcome-mana-settings-workspace-heading' } });
|
|
2983
|
-
const top = createElement(document, 'div', { className: 'outcome-mana-view-heading' });
|
|
2984
|
-
top.append(group(document, 'div', {}, [createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '委任設定' }), createElement(document, 'h2', { id: 'outcome-mana-settings-workspace-heading', text: present(currentContract()?.outcome, '新しい委任') }), createElement(document, 'p', { text: '保存、安全試験、有効化を別々に確認しながら進めます。' })]), button(document, '一覧へ戻る', () => { setActiveView('overview'); render(); }, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
|
|
2985
|
-
const tabs = createElement(document, 'div', { className: 'outcome-mana-detail-tabs', attrs: { role: 'tablist', 'aria-label': '委任詳細' } });
|
|
2986
|
-
[['summary', '概要'], ['settings', '設定'], ['runs', '実行履歴']].forEach(([id, label]) => {
|
|
2987
|
-
const active = state.delegationTab === id;
|
|
2988
|
-
tabs.append(button(document, label, () => {
|
|
2989
|
-
if (id === 'summary') { setActiveView('overview'); return; }
|
|
2990
|
-
state.delegationTab = id;
|
|
2991
|
-
setActiveView('delegation_settings');
|
|
2992
|
-
render();
|
|
2993
|
-
}, { className: `outcome-mana-detail-tab${active ? ' is-active' : ''}`, attrs: { role: 'tab', 'aria-selected': active ? 'true' : 'false' } }));
|
|
2994
|
-
});
|
|
2995
|
-
view.append(top, tabs);
|
|
2996
|
-
if (state.delegationTab === 'runs') {
|
|
2997
|
-
view.append(renderRunsSection());
|
|
2998
|
-
return view;
|
|
2999
|
-
}
|
|
3000
|
-
const workspace = createElement(document, 'div', { className: 'outcome-mana-delegation-layout' });
|
|
3001
|
-
workspace.append(renderStageNavigator(), renderStageWorkspace());
|
|
3002
|
-
view.append(workspace);
|
|
3003
|
-
const sticky = createElement(document, 'div', { className: 'outcome-mana-sticky-actions', attrs: { 'aria-label': '委任設定の次の操作' } });
|
|
3004
|
-
sticky.append(createElement(document, 'span', { className: 'outcome-mana-muted', text: '保存と安全試験は別の工程として記録します。' }));
|
|
3005
|
-
sticky.append(button(document, '安全試験を開く', () => { state.delegationTab = 'settings'; setActiveStage('safe_test'); render(); }, { className: 'outcome-mana-button outcome-mana-button-primary' }));
|
|
3006
|
-
sticky.append(button(document, '実行履歴を見る', () => { state.delegationTab = 'runs'; setActiveView('delegation_settings'); render(); }, { className: 'outcome-mana-button outcome-mana-button-secondary' }));
|
|
3007
|
-
view.append(sticky);
|
|
3008
|
-
return view;
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
function renderRunsView() {
|
|
3012
|
-
const view = createElement(document, 'section', { className: 'outcome-mana-runs-view', attrs: { 'aria-labelledby': 'outcome-mana-runs-view-heading' } });
|
|
3013
|
-
view.append(group(document, 'div', { className: 'outcome-mana-view-heading' }, [group(document, 'div', {}, [createElement(document, 'h2', { id: 'outcome-mana-runs-view-heading', text: '実行履歴' }), createElement(document, 'p', { text: '成果生成から保存、readback、完了判定までを実行単位で追跡します。' })])]));
|
|
3014
|
-
view.append(renderRunsSection()); return view;
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
function renderSettingsView() {
|
|
3018
|
-
const view = createElement(document, 'section', { className: 'outcome-mana-runtime-settings', attrs: { 'aria-labelledby': 'outcome-mana-runtime-settings-heading' } });
|
|
3019
|
-
view.append(group(document, 'div', { className: 'outcome-mana-view-heading' }, [group(document, 'div', {}, [createElement(document, 'h2', { id: 'outcome-mana-runtime-settings-heading', text: 'Mana設定' }), createElement(document, 'p', { text: 'runtime全体の既定値、安全上限、通知、監査を確認します。実効権限は各委任契約の版で決まります。' })])]));
|
|
3020
|
-
if (['loading', 'permission_denied', 'error_retryable', 'conflict'].includes(state.manaSettings.status)) {
|
|
3021
|
-
const message = stateMessage(document, state.manaSettings.status, {
|
|
3022
|
-
message: state.manaSettings.error?.message,
|
|
3023
|
-
retry: () => loadManaSettings(),
|
|
3024
|
-
loadingText: 'Mana設定を読み込んでいます…',
|
|
3025
|
-
});
|
|
3026
|
-
if (message) view.append(message);
|
|
3027
|
-
return view;
|
|
3028
|
-
}
|
|
3029
|
-
if (state.manaSettings.status !== 'ready') {
|
|
3030
|
-
view.append(stateMessage(document, 'unknown', { message: '現在のruntime設定を確認できていません。取得できるまで編集と保存はできません。' }));
|
|
3031
|
-
return view;
|
|
3032
|
-
}
|
|
3033
|
-
const data = objectValue(state.manaSettings.data);
|
|
3034
|
-
const defaults = objectValue(firstPresent(data.defaults, data.runtimeDefaults, data.runtime_defaults));
|
|
3035
|
-
const safety = objectValue(firstPresent(data.safetyLimits, data.safety_limits));
|
|
3036
|
-
const notifications = objectValue(data.notifications);
|
|
3037
|
-
const audit = objectValue(data.audit);
|
|
3038
|
-
const controls = {};
|
|
3039
|
-
const form = createElement(document, 'form', { className: 'outcome-mana-settings-form', attrs: { 'aria-label': 'Mana runtime設定の編集' } });
|
|
3040
|
-
const defaultFieldset = createElement(document, 'fieldset', { className: 'outcome-mana-settings-fieldset' });
|
|
3041
|
-
defaultFieldset.append(createElement(document, 'legend', { text: '既定値' }));
|
|
3042
|
-
const timeout = field(document, 'タイムアウト表示', 'input', 'timeout', settingValue(defaults, 'timeout', 'duration'), { helper: '未確認の場合は空欄のまま保持します。', attrs: { placeholder: UNKNOWN } });
|
|
3043
|
-
const timeoutMs = field(document, 'タイムアウト(ms)', 'input', 'timeoutMs', settingValue(defaults, 'timeoutMs', 'timeout_ms'), { attrs: { inputmode: 'numeric', placeholder: UNKNOWN } });
|
|
3044
|
-
const budgetUnits = field(document, '外部処理の上限', 'input', 'budgetUnits', settingValue(defaults, 'budgetUnits', 'budget_units'), { attrs: { inputmode: 'numeric', placeholder: UNKNOWN } });
|
|
3045
|
-
controls.timeout = timeout.input; controls.timeoutMs = timeoutMs.input; controls.budgetUnits = budgetUnits.input;
|
|
3046
|
-
defaultFieldset.append(timeout.label, timeoutMs.label, budgetUnits.label);
|
|
3047
|
-
const safetyFieldset = createElement(document, 'fieldset', { className: 'outcome-mana-settings-fieldset' });
|
|
3048
|
-
safetyFieldset.append(createElement(document, 'legend', { text: '安全上限' }));
|
|
3049
|
-
const attemptKeys = ['maxAttempts', 'max_attempts', 'attempts'];
|
|
3050
|
-
const knownSafetyAttemptKey = attemptKeys.find((key) => hasOwn(safety, key));
|
|
3051
|
-
const knownDefaultsAttemptKey = attemptKeys.find((key) => hasOwn(defaults, key));
|
|
3052
|
-
const rawMaxAttempts = knownSafetyAttemptKey ? safety[knownSafetyAttemptKey] : knownDefaultsAttemptKey ? defaults[knownDefaultsAttemptKey] : '';
|
|
3053
|
-
const maxAttemptsValue = scalarSettingValue(rawMaxAttempts);
|
|
3054
|
-
const maxAttempts = field(document, '最大試行回数', 'input', 'maxAttempts', maxAttemptsValue, { attrs: { inputmode: 'numeric', placeholder: UNKNOWN } });
|
|
3055
|
-
controls.maxAttempts = maxAttempts.input;
|
|
3056
|
-
safetyFieldset.append(maxAttempts.label);
|
|
3057
|
-
const toggleField = (labelText, name, value) => {
|
|
3058
|
-
const label = createElement(document, 'label', { className: 'outcome-mana-toggle-field' });
|
|
3059
|
-
const input = createElement(document, 'input', { attrs: { type: 'checkbox', name, id: `outcome-mana-${name}` } });
|
|
3060
|
-
const known = typeof value === 'boolean';
|
|
3061
|
-
input.checked = known && value;
|
|
3062
|
-
if (!known) input.disabled = true;
|
|
3063
|
-
label.append(input, createElement(document, 'span', { text: labelText }), createElement(document, 'small', { className: 'outcome-mana-helper', text: known ? (value ? '有効' : '無効') : UNKNOWN }));
|
|
3064
|
-
return { label, input };
|
|
3065
|
-
};
|
|
3066
|
-
const safeTestRequired = toggleField('安全試験を必須にする', 'safeTestRequired', firstPresent(safety.safeTestRequired, safety.safe_test_required));
|
|
3067
|
-
const externalEffectApproval = toggleField('外部作用に承認を要求する', 'externalEffectApproval', firstPresent(safety.externalEffectApproval, safety.external_effect_approval));
|
|
3068
|
-
controls.safeTestRequired = safeTestRequired.input; controls.externalEffectApproval = externalEffectApproval.input;
|
|
3069
|
-
safetyFieldset.append(safeTestRequired.label, externalEffectApproval.label);
|
|
3070
|
-
const notificationFieldset = createElement(document, 'fieldset', { className: 'outcome-mana-settings-fieldset' });
|
|
3071
|
-
notificationFieldset.append(createElement(document, 'legend', { text: '通知' }));
|
|
3072
|
-
const channel = field(document, '通知先', 'input', 'channel', settingValue(notifications, 'channel', 'destination'), { attrs: { placeholder: UNKNOWN } });
|
|
3073
|
-
const events = field(document, '対象イベント', 'input', 'events', settingValue(notifications, 'events', 'eventFilter', 'event_filter'), { attrs: { placeholder: UNKNOWN } });
|
|
3074
|
-
controls.channel = channel.input; controls.events = events.input;
|
|
3075
|
-
notificationFieldset.append(channel.label, events.label);
|
|
3076
|
-
const auditFieldset = createElement(document, 'fieldset', { className: 'outcome-mana-settings-fieldset' });
|
|
3077
|
-
auditFieldset.append(createElement(document, 'legend', { text: '監査' }));
|
|
3078
|
-
const retention = field(document, '保持期間', 'input', 'retention', settingValue(audit, 'retention', 'retentionPeriod', 'retention_period'), { attrs: { placeholder: UNKNOWN } });
|
|
3079
|
-
const auditDestination = field(document, '監査保存先', 'input', 'auditDestination', settingValue(audit, 'destination', 'location'), { attrs: { placeholder: UNKNOWN } });
|
|
3080
|
-
controls.retention = retention.input; controls.auditDestination = auditDestination.input;
|
|
3081
|
-
auditFieldset.append(retention.label, auditDestination.label);
|
|
3082
|
-
form.append(defaultFieldset, safetyFieldset, notificationFieldset, auditFieldset);
|
|
3083
|
-
const actions = createElement(document, 'div', { className: 'outcome-mana-actions' });
|
|
3084
|
-
const saveTarget = settingsSaveTarget();
|
|
3085
|
-
actions.append(button(document, 'Mana設定を保存', () => saveManaSettings(settingsDraftFromFields(data, controls)), { className: 'outcome-mana-button outcome-mana-button-primary', disabled: state.manaSettings.status !== 'ready' || state.manaSettings.saveStatus === 'saving' || !saveTarget || !canManage(currentSession()) }));
|
|
3086
|
-
if (!saveTarget) actions.append(createElement(document, 'small', { className: 'outcome-mana-helper', text: '設定保存経路は未確認です。読み取り専用のまま保持しています。' }));
|
|
3087
|
-
if (state.manaSettings.saveStatus === 'saved_unverified') actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: '保存応答は受け取りましたが、設定のreadbackを確認できていません。', attrs: { role: 'alert' } }));
|
|
3088
|
-
if (state.manaSettings.saveStatus === 'error_retryable') actions.append(createElement(document, 'p', { className: 'outcome-mana-inline-error', text: state.manaSettings.saveError?.message ?? '設定を保存できませんでした。', attrs: { role: 'alert' } }));
|
|
3089
|
-
form.append(actions);
|
|
3090
|
-
const rail = createElement(document, 'aside', { className: 'outcome-mana-settings-rail', attrs: { 'aria-label': 'Mana設定の適用範囲' } });
|
|
3091
|
-
rail.append(createElement(document, 'span', { className: 'outcome-mana-eyebrow', text: '適用範囲' }), createElement(document, 'h3', { text: 'runtime全体' }));
|
|
3092
|
-
rail.append(definitionList(document, [['取得状態', state.manaSettings.status === 'ready' ? '取得済み' : UNKNOWN], ['保存状態', state.manaSettings.saveStatus === 'ready' ? '保存済み・readback確認済み' : state.manaSettings.saveStatus === 'saved_unverified' ? '保存済み・確認待ち' : UNKNOWN], ['実効権限', '委任契約の版で確定'], ['外部作用', '委任単位の許可範囲に従う']]));
|
|
3093
|
-
rail.append(createElement(document, 'p', { className: 'outcome-mana-safe-note', text: 'runtimeの既定値は初期値です。実効権限、利用可能な資源、外部作用は委任契約の版が所有します。' }));
|
|
3094
|
-
const layout = createElement(document, 'div', { className: 'outcome-mana-settings-layout' });
|
|
3095
|
-
layout.append(form, rail);
|
|
3096
|
-
view.append(layout); return view;
|
|
3097
|
-
}
|
|
3098
|
-
|
|
3099
|
-
function renderStageNavigator() {
|
|
3100
|
-
const nav = createElement(document, 'nav', { className: 'outcome-mana-stage-nav', attrs: { 'aria-label': 'Manaの設定工程' } });
|
|
3101
|
-
const list = createElement(document, 'ol', { className: 'outcome-mana-stage-list' });
|
|
3102
|
-
const activeStage = derivedActiveStage();
|
|
3103
|
-
for (const [index, stage] of WORKFLOW_STAGES.entries()) {
|
|
3104
|
-
const item = createElement(document, 'li', { className: `outcome-mana-stage-item${activeStage === stage.id ? ' is-active' : ''}` });
|
|
3105
|
-
const status = stageStatus(stage.id);
|
|
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
|
-
}, {
|
|
3114
|
-
className: 'outcome-mana-stage-button',
|
|
3115
|
-
attrs: {
|
|
3116
|
-
'aria-current': activeStage === stage.id ? 'step' : 'false',
|
|
3117
|
-
'aria-controls': 'outcome-mana-stage-workspace',
|
|
3118
|
-
'aria-label': `${stage.label} / ${statusLabel(status)}`,
|
|
3119
|
-
},
|
|
3120
|
-
});
|
|
3121
|
-
const stageCopy = createElement(document, 'span', { className: 'outcome-mana-stage-copy' });
|
|
3122
|
-
stageCopy.append(
|
|
3123
|
-
createElement(document, 'strong', { text: stage.label }),
|
|
3124
|
-
createElement(document, 'small', { text: stage.description }),
|
|
3125
|
-
);
|
|
3126
|
-
stageButton.append(
|
|
3127
|
-
createElement(document, 'span', { className: 'outcome-mana-stage-number', text: String(index + 1).padStart(2, '0'), attrs: { 'aria-hidden': 'true' } }),
|
|
3128
|
-
stageCopy,
|
|
3129
|
-
statusBadge(document, status),
|
|
3130
|
-
);
|
|
3131
|
-
item.append(stageButton);
|
|
3132
|
-
list.append(item);
|
|
3133
|
-
}
|
|
3134
|
-
nav.append(list);
|
|
3135
|
-
return nav;
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
function renderStageWorkspace() {
|
|
3139
|
-
const activeStage = derivedActiveStage();
|
|
3140
|
-
const workspace = createElement(document, 'section', {
|
|
3141
|
-
className: 'outcome-mana-stage-workspace',
|
|
3142
|
-
attrs: { id: 'outcome-mana-stage-workspace', 'aria-label': `${WORKFLOW_STAGES.find((stage) => stage.id === activeStage)?.label ?? 'Mana'}の作業領域`, 'aria-live': 'polite' },
|
|
3143
|
-
});
|
|
3144
|
-
const renderers = {
|
|
3145
|
-
contract: renderContractSection,
|
|
3146
|
-
authority: renderAuthoritySection,
|
|
3147
|
-
triggers: renderTriggersSection,
|
|
3148
|
-
safe_test: renderSafeTestSection,
|
|
3149
|
-
runs: renderRunsSection,
|
|
3150
|
-
};
|
|
3151
|
-
const panel = renderers[activeStage]?.() ?? renderContractSection();
|
|
3152
|
-
panel.className = `${panel.className} outcome-mana-stage-panel`;
|
|
3153
|
-
panel.setAttribute?.('data-stage', activeStage);
|
|
3154
|
-
workspace.append(panel);
|
|
3155
|
-
return workspace;
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
|
-
function render(target = root) {
|
|
3159
|
-
const destination = target;
|
|
3160
|
-
const activeStage = derivedActiveStage();
|
|
3161
|
-
if (!destination) {
|
|
3162
|
-
const fragment = document.createDocumentFragment?.() ?? createElement(document, 'div');
|
|
3163
|
-
fragment.append(renderStageNavigator(), renderStageWorkspace());
|
|
3164
|
-
return fragment;
|
|
3165
|
-
}
|
|
3166
|
-
clearElement(destination);
|
|
3167
|
-
const activeView = derivedActiveView();
|
|
3168
|
-
const wrapper = createElement(document, 'div', { className: 'outcome-mana-root', attrs: { 'data-project-code': currentProject().code ?? '', 'data-active-stage': activeStage, 'data-active-view': activeView } });
|
|
3169
|
-
const header = createElement(document, 'header', { className: 'outcome-mana-header' });
|
|
3170
|
-
const headerCopy = createElement(document, 'div', { className: 'outcome-mana-header-copy' });
|
|
3171
|
-
headerCopy.append(createElement(document, 'div', { className: 'outcome-mana-eyebrow', text: 'BRAINBASE / MANA' }), createElement(document, 'h1', { text: 'Manaに任せる仕事' }), createElement(document, 'p', { text: `${present(currentProject().name, 'プロジェクト未確認')} / ${present(currentProject().code, 'プロジェクトコード未確認')} / セッション: ${present(currentSession().displayName ?? currentSession().actorId, '利用者未確認')}` }));
|
|
3172
|
-
header.append(headerCopy, statusBadge(document, currentContract()?.status ?? state.contract.status));
|
|
3173
|
-
const renderers = { overview: renderOverviewView, delegation_settings: renderDelegationSettings, connections: renderConnectionsView, runs: renderRunsView, settings: renderSettingsView };
|
|
3174
|
-
wrapper.append(header, renderWorkspaceNavigation(), renderers[activeView]?.() ?? renderOverviewView());
|
|
3175
|
-
destination.append(wrapper); return destination;
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
const controller = {
|
|
3179
|
-
state,
|
|
3180
|
-
render,
|
|
3181
|
-
setActiveView: (id) => { setActiveView(id); render(); return state.activeView; },
|
|
3182
|
-
setActiveStage: (id) => { setActiveStage(id); render(); return state.activeStage; },
|
|
3183
|
-
load: loadContracts,
|
|
3184
|
-
loadContracts,
|
|
3185
|
-
loadContractDetail,
|
|
3186
|
-
loadConnections,
|
|
3187
|
-
loadManaSettings,
|
|
3188
|
-
loadTriggerCatalog,
|
|
3189
|
-
saveContractDraft,
|
|
3190
|
-
cloneContract,
|
|
3191
|
-
retireContract,
|
|
3192
|
-
loadAuthority,
|
|
3193
|
-
saveAuthority,
|
|
3194
|
-
saveTriggers,
|
|
3195
|
-
runSafeTest,
|
|
3196
|
-
refreshSafeTestReadback,
|
|
3197
|
-
activate,
|
|
3198
|
-
stopActivation,
|
|
3199
|
-
loadRuns,
|
|
3200
|
-
loadRunDetail,
|
|
3201
|
-
saveManaSettings,
|
|
3202
|
-
approveRun: (id, input = {}) => runAction('approve', input, id),
|
|
3203
|
-
rejectRun: (id, input = {}) => runAction('reject', input, id),
|
|
3204
|
-
retryRun: (id, input = {}) => runAction('retry', input, id),
|
|
3205
|
-
stopRun: (id, input = {}) => runAction('stop', input, id),
|
|
3206
|
-
paths: currentPaths,
|
|
3207
|
-
};
|
|
3208
|
-
if (root && options.autoLoad !== false) {
|
|
3209
|
-
render();
|
|
3210
|
-
if (typeof api === 'function') loadContracts();
|
|
3211
|
-
} else if (root) render();
|
|
3212
|
-
return controller;
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
export function renderManaOutcome(target, options = {}) {
|
|
3216
|
-
return createManaOutcomeUI({ ...options, root: target, autoLoad: options.autoLoad ?? false }).render();
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
export const createOutcomeManaUI = createManaOutcomeUI;
|
|
3220
|
-
export const renderOutcomeMana = renderManaOutcome;
|
|
3221
|
-
export { authorityLabel, triggerLabel, statusLabel, normalizeError, isReadbackVerified };
|
|
3222
|
-
export default createManaOutcomeUI;
|