dsh-kingdom 0.8.0 → 2.0.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/ASSETS_NOTICE.md +22 -0
- package/LICENSE +661 -29
- package/NOTICE +9 -0
- package/README.md +93 -94
- package/THIRD_PARTY_NOTICES.md +92 -0
- package/cordis.patch.yml +6 -6
- package/docs/V2.0-RELEASE-NOTES.md +30 -0
- package/docs/V2.0-USER-GUIDE.md +74 -0
- package/lib/adapter/dsh-backend.js +327 -24
- package/lib/adapter/dsh-backend.js.map +1 -1
- package/lib/adapter/dsh-cost.js +68 -0
- package/lib/adapter/dsh-cost.js.map +1 -0
- package/lib/adapter/dsh-session-events.js +86 -0
- package/lib/adapter/dsh-session-events.js.map +1 -0
- package/lib/adapter/dsh-usage.js +159 -0
- package/lib/adapter/dsh-usage.js.map +1 -0
- package/lib/capability/admin.js +72 -0
- package/lib/capability/admin.js.map +1 -0
- package/lib/capability/dsh-enforcement.js +143 -34
- package/lib/capability/dsh-enforcement.js.map +1 -1
- package/lib/capability/resolver.js +21 -8
- package/lib/capability/resolver.js.map +1 -1
- package/lib/capability/service.js +169 -26
- package/lib/capability/service.js.map +1 -1
- package/lib/capability/tool-disclosure.js +144 -0
- package/lib/capability/tool-disclosure.js.map +1 -0
- package/lib/core/binding.js +52 -34
- package/lib/core/binding.js.map +1 -1
- package/lib/core/budget.js +304 -0
- package/lib/core/budget.js.map +1 -0
- package/lib/core/collaboration.js +422 -0
- package/lib/core/collaboration.js.map +1 -0
- package/lib/core/cost.js +212 -0
- package/lib/core/cost.js.map +1 -0
- package/lib/core/db.js +197 -4
- package/lib/core/db.js.map +1 -1
- package/lib/core/execution.js +2 -2
- package/lib/core/execution.js.map +1 -1
- package/lib/core/governed-claim.js +46 -0
- package/lib/core/governed-claim.js.map +1 -0
- package/lib/core/governed.js +891 -98
- package/lib/core/governed.js.map +1 -1
- package/lib/core/kingdom.js +78 -70
- package/lib/core/kingdom.js.map +1 -1
- package/lib/core/owner-binding-intent.js +732 -0
- package/lib/core/owner-binding-intent.js.map +1 -0
- package/lib/core/owner-control.js +86 -0
- package/lib/core/owner-control.js.map +1 -0
- package/lib/core/owner-window.js +822 -0
- package/lib/core/owner-window.js.map +1 -0
- package/lib/core/task-service.js +296 -133
- package/lib/core/task-service.js.map +1 -1
- package/lib/core/territory.js +44 -14
- package/lib/core/territory.js.map +1 -1
- package/lib/core/usage.js +39 -0
- package/lib/core/usage.js.map +1 -0
- package/lib/core/workspace-admission.js +225 -0
- package/lib/core/workspace-admission.js.map +1 -0
- package/lib/dispatch/evidence.js +5 -2
- package/lib/dispatch/evidence.js.map +1 -1
- package/lib/dispatch/reconcile.js +3 -9
- package/lib/dispatch/reconcile.js.map +1 -1
- package/lib/dispatch/service.js +464 -64
- package/lib/dispatch/service.js.map +1 -1
- package/lib/gui/assets/characters/chancellor-idle.svg +67 -0
- package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -0
- package/lib/gui/assets/characters/chancellor-thinking.svg +67 -0
- package/lib/gui/assets/characters/chancellor-working.svg +67 -0
- package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -0
- package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -0
- package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -0
- package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -0
- package/lib/gui/assets/characters/supervisor-idle.svg +67 -0
- package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -0
- package/lib/gui/assets/characters/supervisor-thinking.svg +67 -0
- package/lib/gui/assets/characters/supervisor-working.svg +67 -0
- package/lib/gui/console-app.js +2330 -0
- package/lib/gui/console-app.js.map +1 -0
- package/lib/gui/contract.js +5 -5
- package/lib/gui/contract.js.map +1 -1
- package/lib/gui/control-contract.js +232 -0
- package/lib/gui/control-contract.js.map +1 -0
- package/lib/gui/local-control.js +423 -0
- package/lib/gui/local-control.js.map +1 -0
- package/lib/gui/owner-control.js +310 -0
- package/lib/gui/owner-control.js.map +1 -0
- package/lib/gui/owner-ui.js +191 -0
- package/lib/gui/owner-ui.js.map +1 -0
- package/lib/gui/server.js +445 -35
- package/lib/gui/server.js.map +1 -1
- package/lib/gui/snapshot.js +1346 -67
- package/lib/gui/snapshot.js.map +1 -1
- package/lib/gui/visual-assets.js +56 -0
- package/lib/gui/visual-assets.js.map +1 -0
- package/lib/gui/visual-system.js +272 -0
- package/lib/gui/visual-system.js.map +1 -0
- package/lib/gui/workbench-ui.js +358 -0
- package/lib/gui/workbench-ui.js.map +1 -0
- package/lib/gui/workbench.js +163 -0
- package/lib/gui/workbench.js.map +1 -0
- package/lib/index.js +1384 -361
- package/lib/index.js.map +1 -1
- package/lib/runner-context-broker.js +1134 -0
- package/lib/runner-context-broker.js.map +1 -0
- package/lib/runtime-cost-observer.js +145 -0
- package/lib/runtime-cost-observer.js.map +1 -0
- package/lib/types/adapter/contract.d.ts +61 -1
- package/lib/types/adapter/dsh-backend.d.ts +32 -13
- package/lib/types/adapter/dsh-cost.d.ts +13 -0
- package/lib/types/adapter/dsh-session-events.d.ts +16 -0
- package/lib/types/adapter/dsh-usage.d.ts +36 -0
- package/lib/types/capability/admin.d.ts +13 -0
- package/lib/types/capability/dsh-enforcement.d.ts +11 -7
- package/lib/types/capability/service.d.ts +10 -3
- package/lib/types/capability/tool-disclosure.d.ts +36 -0
- package/lib/types/core/binding.d.ts +19 -10
- package/lib/types/core/budget.d.ts +81 -0
- package/lib/types/core/collaboration.d.ts +110 -0
- package/lib/types/core/cost.d.ts +99 -0
- package/lib/types/core/db.d.ts +44 -4
- package/lib/types/core/execution.d.ts +1 -1
- package/lib/types/core/governed-claim.d.ts +7 -0
- package/lib/types/core/governed.d.ts +182 -1
- package/lib/types/core/kingdom.d.ts +3 -0
- package/lib/types/core/owner-binding-intent.d.ts +176 -0
- package/lib/types/core/owner-control.d.ts +53 -0
- package/lib/types/core/owner-window.d.ts +226 -0
- package/lib/types/core/task-service.d.ts +60 -13
- package/lib/types/core/territory.d.ts +9 -2
- package/lib/types/core/usage.d.ts +26 -0
- package/lib/types/core/workspace-admission.d.ts +43 -0
- package/lib/types/dispatch/evidence.d.ts +2 -5
- package/lib/types/dispatch/service.d.ts +62 -4
- package/lib/types/gui/console-app.d.ts +137 -0
- package/lib/types/gui/contract.d.ts +396 -16
- package/lib/types/gui/control-contract.d.ts +111 -0
- package/lib/types/gui/local-control.d.ts +120 -0
- package/lib/types/gui/owner-control.d.ts +45 -0
- package/lib/types/gui/owner-ui.d.ts +2 -0
- package/lib/types/gui/server.d.ts +17 -3
- package/lib/types/gui/snapshot.d.ts +33 -11
- package/lib/types/gui/visual-assets.d.ts +27 -0
- package/lib/types/gui/visual-system.d.ts +2 -0
- package/lib/types/gui/workbench-ui.d.ts +5 -0
- package/lib/types/gui/workbench.d.ts +33 -0
- package/lib/types/index.d.ts +157 -6
- package/lib/types/runner-context-broker.d.ts +119 -0
- package/lib/types/runtime-cost-observer.d.ts +22 -0
- package/lib/types/worker/collaboration.d.ts +35 -0
- package/lib/types/worker/dsh-subagent.d.ts +4 -4
- package/lib/types/worker/executor-factory.d.ts +2 -1
- package/lib/types/worker/executor.d.ts +8 -3
- package/lib/types/worker/governed-executor.d.ts +8 -1
- package/lib/worker/collaboration.js +172 -0
- package/lib/worker/collaboration.js.map +1 -0
- package/lib/worker/dsh-subagent.js +4 -4
- package/lib/worker/executor-factory.js.map +1 -1
- package/lib/worker/executor.js +7 -2
- package/lib/worker/executor.js.map +1 -1
- package/lib/worker/governed-executor.js +295 -84
- package/lib/worker/governed-executor.js.map +1 -1
- package/package.json +23 -8
package/lib/gui/snapshot.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-kingdom — DB 行 → GUI 视图的投影(Phase 3)。
|
|
3
|
+
*
|
|
4
|
+
* 全部是**纯函数**:给定 (库状态, now) 就唯一确定输出。
|
|
5
|
+
* 因此 GUI 轮询即可拿到正确的表演状态,服务端不需要任何定时器或推送状态机。
|
|
6
|
+
*
|
|
7
|
+
* 再次强调边界:本文件只产出 `{ role, state, activity }` 这类语义,
|
|
8
|
+
* 绝不产出贴图、clip、场景文件名——那些是 GUI 的 visual-map 的事。
|
|
9
|
+
*/
|
|
10
|
+
import { readDispatchUsage } from '../core/usage.js';
|
|
11
|
+
import { readAdditionalRoleCost, readLatestPromptCosts } from '../core/cost.js';
|
|
12
|
+
import { readBudgetView } from '../core/budget.js';
|
|
13
|
+
import { listCollaborationPlans, readCollaborationReadiness, readPlanBudgetView } from '../core/collaboration.js';
|
|
14
|
+
import { readWorkspaceReservations } from '../core/workspace-admission.js';
|
|
15
|
+
import { buildPersonalWorkbench } from './workbench.js';
|
|
1
16
|
import { GUI_SCHEMA_VERSION, TRANSIENT_WINDOW_MS } from './contract.js';
|
|
2
|
-
import { asExecutionState, isLiveExecutionState } from '../core/execution.js';
|
|
17
|
+
import { asExecutionState, isLiveExecutionState, isTerminalExecutionState } from '../core/execution.js';
|
|
3
18
|
import { asTaskStatus } from '../core/task.js';
|
|
4
19
|
import { parseExecutionProfile } from '../core/binding.js';
|
|
5
20
|
// ── 行 → 视图 ───────────────────────────────────────────────────
|
|
@@ -17,6 +32,379 @@ function parseJson(text) {
|
|
|
17
32
|
function stringList(value) {
|
|
18
33
|
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
19
34
|
}
|
|
35
|
+
const REDACTED_JSON_VALUE = '[REDACTED]';
|
|
36
|
+
const TRUNCATED_JSON_DEPTH = '[TRUNCATED_DEPTH]';
|
|
37
|
+
const TRUNCATED_JSON_NODES = '[TRUNCATED_NODES]';
|
|
38
|
+
const TRUNCATED_JSON_ARRAY = '[TRUNCATED_ARRAY]';
|
|
39
|
+
const TRUNCATED_JSON_OBJECT_KEY = '__projectionTruncated__';
|
|
40
|
+
const MAX_PUBLIC_JSON_DEPTH = 6;
|
|
41
|
+
const MAX_PUBLIC_JSON_ENTRIES = 32;
|
|
42
|
+
const MAX_PUBLIC_JSON_NODES = 512;
|
|
43
|
+
const MAX_PUBLIC_JSON_STRING = 512;
|
|
44
|
+
const PUBLIC_JSON_TRUNCATION_SUFFIX = '…[TRUNCATED]';
|
|
45
|
+
const SENSITIVE_JSON_KEY_FRAGMENTS = [
|
|
46
|
+
'session',
|
|
47
|
+
'principal',
|
|
48
|
+
'private',
|
|
49
|
+
'config',
|
|
50
|
+
'token',
|
|
51
|
+
'cookie',
|
|
52
|
+
'credential',
|
|
53
|
+
'secret',
|
|
54
|
+
'password',
|
|
55
|
+
'authorization',
|
|
56
|
+
'apikey',
|
|
57
|
+
'accesskey',
|
|
58
|
+
'clientsecret',
|
|
59
|
+
'bearer',
|
|
60
|
+
'connectionstring',
|
|
61
|
+
'csrf',
|
|
62
|
+
'authority',
|
|
63
|
+
'ownercontrol',
|
|
64
|
+
];
|
|
65
|
+
function sensitivePublicJsonKey(key) {
|
|
66
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
67
|
+
return SENSITIVE_JSON_KEY_FRAGMENTS.some(fragment => normalized.includes(fragment));
|
|
68
|
+
}
|
|
69
|
+
function sanitizePublicJsonString(value, sensitivity = 'public-content') {
|
|
70
|
+
if (sensitivity === 'private-config')
|
|
71
|
+
return REDACTED_JSON_VALUE;
|
|
72
|
+
const redacted = value
|
|
73
|
+
.replace(/(?:authorization|bearer|session(?:[_ .-]?id)?|principal(?:[_ .-]?id)?|private(?:[_ .-]?config)?|token|cookie|credential|secret|password|api[_ .-]?key|access[_ .-]?key|client[_ .-]?secret|connection[_ .-]?string|csrf)\b(?:\s*["']?\s*(?:=|:)\s*["']?\s*|\s+)[^\s,;&}]+/giu, REDACTED_JSON_VALUE)
|
|
74
|
+
.replace(/[A-Za-z]:[\\/][^\s<>"']*/gu, '[redacted-path]')
|
|
75
|
+
.replace(/\\\\[^\s<>"']*/gu, '[redacted-path]')
|
|
76
|
+
.replace(/(^|[\s("'=])\/[^\s<>"']*/gu, '$1[redacted-path]')
|
|
77
|
+
.replace(/\bprivate[-_][^\s<>"']+/giu, REDACTED_JSON_VALUE);
|
|
78
|
+
if (redacted.length <= MAX_PUBLIC_JSON_STRING)
|
|
79
|
+
return redacted;
|
|
80
|
+
return `${redacted.slice(0, MAX_PUBLIC_JSON_STRING - PUBLIC_JSON_TRUNCATION_SUFFIX.length)}${PUBLIC_JSON_TRUNCATION_SUFFIX}`;
|
|
81
|
+
}
|
|
82
|
+
function sanitizePublicJsonValue(value, depth, budget) {
|
|
83
|
+
if (budget.remaining <= 0)
|
|
84
|
+
return TRUNCATED_JSON_NODES;
|
|
85
|
+
budget.remaining -= 1;
|
|
86
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
87
|
+
return value;
|
|
88
|
+
if (typeof value === 'string')
|
|
89
|
+
return sanitizePublicJsonString(value);
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
if (depth >= MAX_PUBLIC_JSON_DEPTH)
|
|
92
|
+
return TRUNCATED_JSON_DEPTH;
|
|
93
|
+
const result = value
|
|
94
|
+
.slice(0, MAX_PUBLIC_JSON_ENTRIES)
|
|
95
|
+
.map(item => sanitizePublicJsonValue(item, depth + 1, budget));
|
|
96
|
+
if (value.length > MAX_PUBLIC_JSON_ENTRIES)
|
|
97
|
+
result.push(TRUNCATED_JSON_ARRAY);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== 'object')
|
|
101
|
+
return null;
|
|
102
|
+
if (depth >= MAX_PUBLIC_JSON_DEPTH)
|
|
103
|
+
return TRUNCATED_JSON_DEPTH;
|
|
104
|
+
const entries = Object.entries(value);
|
|
105
|
+
const result = Object.create(null);
|
|
106
|
+
for (const [key, item] of entries.slice(0, MAX_PUBLIC_JSON_ENTRIES)) {
|
|
107
|
+
result[key] = sensitivePublicJsonKey(key)
|
|
108
|
+
? REDACTED_JSON_VALUE
|
|
109
|
+
: sanitizePublicJsonValue(item, depth + 1, budget);
|
|
110
|
+
}
|
|
111
|
+
if (entries.length > MAX_PUBLIC_JSON_ENTRIES) {
|
|
112
|
+
result[TRUNCATED_JSON_OBJECT_KEY] = entries.length - MAX_PUBLIC_JSON_ENTRIES;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/** Event payload 与 Binding session metadata 共用的递归、确定性、有界公共 JSON 转换。 */
|
|
117
|
+
function sanitizePublicJsonObject(value) {
|
|
118
|
+
const sanitized = sanitizePublicJsonValue(value, 0, { remaining: MAX_PUBLIC_JSON_NODES });
|
|
119
|
+
return typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)
|
|
120
|
+
? sanitized
|
|
121
|
+
: {};
|
|
122
|
+
}
|
|
123
|
+
function sanitizePublicStringList(value) {
|
|
124
|
+
const sanitized = sanitizePublicJsonValue(stringList(value), 0, { remaining: MAX_PUBLIC_JSON_NODES });
|
|
125
|
+
return Array.isArray(sanitized)
|
|
126
|
+
? sanitized.filter((item) => typeof item === 'string')
|
|
127
|
+
: [];
|
|
128
|
+
}
|
|
129
|
+
// ── v0.9 S1 只读 Projection ──────────────────────────────────────
|
|
130
|
+
const MAX_PROJECTION_SOURCE_REFS = 8;
|
|
131
|
+
const MAX_PROJECTION_TIMELINE_ITEMS = 200;
|
|
132
|
+
const MAX_PROJECTION_TEXT = 140;
|
|
133
|
+
const MAX_PROJECTION_ORGANIZATION_ITEMS = 64;
|
|
134
|
+
const MAX_PROJECTION_EXECUTION_SUMMARIES = 100;
|
|
135
|
+
const OWNER_ONLY_ACTIONS = [
|
|
136
|
+
'init',
|
|
137
|
+
'reset',
|
|
138
|
+
'ceiling',
|
|
139
|
+
'territory.create',
|
|
140
|
+
'territory.delete',
|
|
141
|
+
'territory.supervisor',
|
|
142
|
+
// v0.9 aliases remain additive for existing JSON consumers.
|
|
143
|
+
'binding.bind',
|
|
144
|
+
'binding.unbind',
|
|
145
|
+
'binding.session',
|
|
146
|
+
// Canonical direct Slash command names used by v1.0.
|
|
147
|
+
'role.bind',
|
|
148
|
+
'role.unbind',
|
|
149
|
+
'role.session',
|
|
150
|
+
'execution-profile',
|
|
151
|
+
];
|
|
152
|
+
const PUBLIC_ENTITY_REF_TYPES = new Set([
|
|
153
|
+
'kingdom',
|
|
154
|
+
'task',
|
|
155
|
+
'execution',
|
|
156
|
+
'territory',
|
|
157
|
+
'binding',
|
|
158
|
+
'affinity',
|
|
159
|
+
'lease',
|
|
160
|
+
'decision',
|
|
161
|
+
'dispatch',
|
|
162
|
+
]);
|
|
163
|
+
const PUBLIC_TABLE_SOURCE_ENTITY_TYPES = new Set([
|
|
164
|
+
'kingdoms',
|
|
165
|
+
'tasks',
|
|
166
|
+
'territories',
|
|
167
|
+
'role_bindings',
|
|
168
|
+
'task_assignments',
|
|
169
|
+
'worker_results',
|
|
170
|
+
'executions',
|
|
171
|
+
'worker_session_affinities',
|
|
172
|
+
'execution_leases',
|
|
173
|
+
'capability_decisions',
|
|
174
|
+
'dispatch_records',
|
|
175
|
+
]);
|
|
176
|
+
function publicReferenceId(value) {
|
|
177
|
+
if (!value)
|
|
178
|
+
return null;
|
|
179
|
+
const trimmed = value.trim();
|
|
180
|
+
return /^[A-Za-z0-9_.:@-]{1,96}$/u.test(trimmed) ? trimmed : null;
|
|
181
|
+
}
|
|
182
|
+
function boundedText(value) {
|
|
183
|
+
if (!value)
|
|
184
|
+
return '';
|
|
185
|
+
return sanitizePublicJsonString(value)
|
|
186
|
+
.replace(/[\r\n\t]+/gu, ' ')
|
|
187
|
+
.trim()
|
|
188
|
+
.slice(0, MAX_PROJECTION_TEXT);
|
|
189
|
+
}
|
|
190
|
+
function publicEntityRef(type, id) {
|
|
191
|
+
if (!PUBLIC_ENTITY_REF_TYPES.has(type))
|
|
192
|
+
return null;
|
|
193
|
+
const safeId = publicReferenceId(id);
|
|
194
|
+
return safeId ? { type, id: safeId } : null;
|
|
195
|
+
}
|
|
196
|
+
function tableSource(entityType, entityId) {
|
|
197
|
+
return { sourceType: 'table-row', entityType, entityId: publicReferenceId(entityId) };
|
|
198
|
+
}
|
|
199
|
+
function eventSource(seq) {
|
|
200
|
+
return { sourceType: 'event', entityType: 'events', entityId: null, eventSeq: seq };
|
|
201
|
+
}
|
|
202
|
+
function ruleSource(ruleCode) {
|
|
203
|
+
return { sourceType: 'derived-rule', entityType: 'projection-rule', entityId: null, ruleCode };
|
|
204
|
+
}
|
|
205
|
+
function publicSourceType(value) {
|
|
206
|
+
return value === 'table-row'
|
|
207
|
+
|| value === 'event'
|
|
208
|
+
|| value === 'runtime-evidence'
|
|
209
|
+
|| value === 'derived-rule'
|
|
210
|
+
? value
|
|
211
|
+
: 'derived-rule';
|
|
212
|
+
}
|
|
213
|
+
function publicSourceEntityType(sourceType, value) {
|
|
214
|
+
const safeValue = boundedText(value);
|
|
215
|
+
if (sourceType === 'table-row' && PUBLIC_TABLE_SOURCE_ENTITY_TYPES.has(safeValue))
|
|
216
|
+
return safeValue;
|
|
217
|
+
if (sourceType === 'event' && safeValue === 'events')
|
|
218
|
+
return safeValue;
|
|
219
|
+
if (sourceType === 'runtime-evidence' && safeValue === 'runtime-observation')
|
|
220
|
+
return safeValue;
|
|
221
|
+
if (sourceType === 'derived-rule' && safeValue === 'projection-rule')
|
|
222
|
+
return safeValue;
|
|
223
|
+
return 'redacted-source';
|
|
224
|
+
}
|
|
225
|
+
function publicRuleCode(value) {
|
|
226
|
+
if (!value)
|
|
227
|
+
return undefined;
|
|
228
|
+
const trimmed = value.trim();
|
|
229
|
+
return /^[A-Z][A-Z0-9_]{0,95}$/u.test(trimmed) ? trimmed : 'REDACTED_RULE';
|
|
230
|
+
}
|
|
231
|
+
/** 对外暴露前统一限制数量与标识格式;不接受 session/raw payload/path 作为引用。 */
|
|
232
|
+
export function boundedSourceRefs(refs) {
|
|
233
|
+
const result = [];
|
|
234
|
+
const seen = new Set();
|
|
235
|
+
for (const ref of refs) {
|
|
236
|
+
const sourceType = publicSourceType(ref.sourceType);
|
|
237
|
+
const entityType = publicSourceEntityType(sourceType, ref.entityType);
|
|
238
|
+
const ruleCode = sourceType === 'derived-rule' ? publicRuleCode(ref.ruleCode) : undefined;
|
|
239
|
+
const normalized = {
|
|
240
|
+
sourceType,
|
|
241
|
+
entityType,
|
|
242
|
+
entityId: sourceType === 'table-row' && entityType !== 'redacted-source'
|
|
243
|
+
? publicReferenceId(ref.entityId)
|
|
244
|
+
: null,
|
|
245
|
+
...(sourceType === 'event' && Number.isInteger(ref.eventSeq) && ref.eventSeq >= 0
|
|
246
|
+
? { eventSeq: ref.eventSeq }
|
|
247
|
+
: {}),
|
|
248
|
+
...(ruleCode ? { ruleCode } : {}),
|
|
249
|
+
};
|
|
250
|
+
const key = JSON.stringify(normalized);
|
|
251
|
+
if (seen.has(key))
|
|
252
|
+
continue;
|
|
253
|
+
seen.add(key);
|
|
254
|
+
result.push(normalized);
|
|
255
|
+
if (result.length >= MAX_PROJECTION_SOURCE_REFS)
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
function safeDate(value) {
|
|
261
|
+
if (!value)
|
|
262
|
+
return null;
|
|
263
|
+
const timestamp = Date.parse(value);
|
|
264
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
265
|
+
}
|
|
266
|
+
function projectionEnvelope(input) {
|
|
267
|
+
return {
|
|
268
|
+
revision: input.revision,
|
|
269
|
+
refreshedAt: input.refreshedAt,
|
|
270
|
+
entityRef: input.entityRef,
|
|
271
|
+
authoritativeState: input.authoritativeState,
|
|
272
|
+
sourceRefs: boundedSourceRefs(input.sourceRefs),
|
|
273
|
+
allowedActions: input.allowedActions ?? null,
|
|
274
|
+
attentionReason: input.attentionReason ?? null,
|
|
275
|
+
data: input.data,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function attentionReason(code, refs) {
|
|
279
|
+
return { code, sourceRefs: boundedSourceRefs(refs) };
|
|
280
|
+
}
|
|
281
|
+
function executionTerminality(state) {
|
|
282
|
+
if (state === 'COMPLETED' || state === 'FAILED' || state === 'ABORTED')
|
|
283
|
+
return 'TERMINAL';
|
|
284
|
+
if (state === 'STARTING' || state === 'RUNNING' || state === 'PAUSED')
|
|
285
|
+
return 'NON_TERMINAL';
|
|
286
|
+
return 'UNKNOWN';
|
|
287
|
+
}
|
|
288
|
+
/** 未终结的 governed attempt(含 RECOVERING)在完成对账前绝不能产生新 start。 */
|
|
289
|
+
function isGovernedStartBlocked(execution) {
|
|
290
|
+
return execution?.execution_contract === 'GOVERNED_PERSISTENT'
|
|
291
|
+
&& !isTerminalExecutionState(asExecutionState(execution.state));
|
|
292
|
+
}
|
|
293
|
+
function stateValue(value, sourceKind, refs) {
|
|
294
|
+
return { sourceKind, value: boundedText(value) || 'UNKNOWN', sourceRefs: boundedSourceRefs(refs) };
|
|
295
|
+
}
|
|
296
|
+
function timelineId(prefix, id) {
|
|
297
|
+
return `${prefix}:${publicReferenceId(id) ?? 'unknown'}`;
|
|
298
|
+
}
|
|
299
|
+
function actionBinding(store, task) {
|
|
300
|
+
const territory = store.getTerritoryById(task.territory_id);
|
|
301
|
+
return territory?.supervisor_binding_id ? store.getBindingById(territory.supervisor_binding_id) : null;
|
|
302
|
+
}
|
|
303
|
+
function commandForAction(action) {
|
|
304
|
+
if (action.startsWith('review:'))
|
|
305
|
+
return 'review';
|
|
306
|
+
if (action.startsWith('execution:'))
|
|
307
|
+
return action.replace(':', '.');
|
|
308
|
+
return action;
|
|
309
|
+
}
|
|
310
|
+
function firstMissingActionContext(store, task, action, security) {
|
|
311
|
+
const refs = [tableSource('tasks', task.task_id)];
|
|
312
|
+
const principalSessionId = security.principalSessionId;
|
|
313
|
+
if (security.sessionVerified !== true || typeof principalSessionId !== 'string' || principalSessionId.length === 0) {
|
|
314
|
+
return { reason: 'SESSION_AUTH_REQUIRED', refs: [...refs, ruleSource('ACTION_SESSION_VERIFIED')] };
|
|
315
|
+
}
|
|
316
|
+
const binding = actionBinding(store, task);
|
|
317
|
+
if (!binding || binding.role_type !== 'SUPERVISOR' || binding.status !== 'ACTIVE') {
|
|
318
|
+
return {
|
|
319
|
+
reason: 'TERRITORY_SUPERVISOR_MISSING',
|
|
320
|
+
refs: [...refs, tableSource('territories', task.territory_id), tableSource('role_bindings', binding?.binding_id), ruleSource('ACTION_TERRITORY_SUPERVISOR')],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (binding.session_id === null || binding.session_id !== principalSessionId) {
|
|
324
|
+
return {
|
|
325
|
+
reason: 'SESSION_AUTH_REQUIRED',
|
|
326
|
+
refs: [...refs, tableSource('role_bindings', binding.binding_id), ruleSource('ACTION_SUPERVISOR_SESSION_MATCH')],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const scope = security.scope ?? [];
|
|
330
|
+
if (!scope.includes(task.territory_id) && !scope.includes(`territory:${task.territory_id}`)) {
|
|
331
|
+
return { reason: 'ROLE_SCOPE_REQUIRED', refs: [...refs, tableSource('territories', task.territory_id), ruleSource('ACTION_TERRITORY_SCOPE')] };
|
|
332
|
+
}
|
|
333
|
+
if (security.hostContext !== true) {
|
|
334
|
+
return { reason: 'HOST_CONTEXT_REQUIRED', refs: [...refs, ruleSource('ACTION_HOST_CONTEXT')] };
|
|
335
|
+
}
|
|
336
|
+
if (!(security.commandCoverage ?? []).includes(commandForAction(action))) {
|
|
337
|
+
return { reason: 'COMMAND_UNAVAILABLE', refs: [...refs, ruleSource('ACTION_COMMAND_COVERAGE')] };
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
/** 只返回生命周期候选的可执行性,不执行 action,也不改变任何状态。 */
|
|
342
|
+
export function buildActionAvailability(store, task, execution, security = {}) {
|
|
343
|
+
const startBlocked = isGovernedStartBlocked(execution);
|
|
344
|
+
const lifecycleActions = allowedActionsFor(task, execution);
|
|
345
|
+
const actions = startBlocked && !lifecycleActions.includes('start')
|
|
346
|
+
? ['start', ...lifecycleActions]
|
|
347
|
+
: lifecycleActions;
|
|
348
|
+
return actions.map((action) => {
|
|
349
|
+
const refs = [tableSource('tasks', task.task_id)];
|
|
350
|
+
if (action === 'start' && startBlocked && execution) {
|
|
351
|
+
const blockedRefs = [
|
|
352
|
+
...refs,
|
|
353
|
+
tableSource('executions', execution.execution_id),
|
|
354
|
+
...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
|
|
355
|
+
ruleSource('START_BLOCKED_BY_UNSETTLED_GOVERNED_EXECUTION'),
|
|
356
|
+
];
|
|
357
|
+
return {
|
|
358
|
+
action,
|
|
359
|
+
lifecycleAllowed: false,
|
|
360
|
+
executable: false,
|
|
361
|
+
disabledReason: attentionReason(execution.state === 'RECOVERING' ? 'EXECUTION_RECOVERING' : 'ILLEGAL_EXECUTION_STATE', blockedRefs),
|
|
362
|
+
sourceRefs: boundedSourceRefs(blockedRefs),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (OWNER_ONLY_ACTIONS.includes(action)) {
|
|
366
|
+
return {
|
|
367
|
+
action,
|
|
368
|
+
lifecycleAllowed: true,
|
|
369
|
+
executable: false,
|
|
370
|
+
disabledReason: attentionReason('DIRECT_SLASH_REQUIRED', [...refs, ruleSource('OWNER_DIRECT_SLASH_ONLY')]),
|
|
371
|
+
sourceRefs: boundedSourceRefs([...refs, ruleSource('OWNER_DIRECT_SLASH_ONLY')]),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const missing = firstMissingActionContext(store, task, action, security);
|
|
375
|
+
if (missing) {
|
|
376
|
+
return {
|
|
377
|
+
action,
|
|
378
|
+
lifecycleAllowed: true,
|
|
379
|
+
executable: false,
|
|
380
|
+
disabledReason: attentionReason(missing.reason, missing.refs),
|
|
381
|
+
sourceRefs: boundedSourceRefs(missing.refs),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (action.startsWith('execution:') && execution?.execution_contract === 'GOVERNED_PERSISTENT') {
|
|
385
|
+
const unavailableRefs = [
|
|
386
|
+
...refs,
|
|
387
|
+
tableSource('executions', execution.execution_id),
|
|
388
|
+
...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
|
|
389
|
+
ruleSource('GOVERNED_RUNTIME_CONTROL_UNAVAILABLE'),
|
|
390
|
+
];
|
|
391
|
+
return {
|
|
392
|
+
action,
|
|
393
|
+
lifecycleAllowed: true,
|
|
394
|
+
executable: false,
|
|
395
|
+
disabledReason: attentionReason('GOVERNED_RUNTIME_CONTROL_UNAVAILABLE', unavailableRefs),
|
|
396
|
+
sourceRefs: boundedSourceRefs(unavailableRefs),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
action,
|
|
401
|
+
lifecycleAllowed: true,
|
|
402
|
+
executable: true,
|
|
403
|
+
disabledReason: null,
|
|
404
|
+
sourceRefs: boundedSourceRefs([...refs, ruleSource('ACTION_CONTEXT_VERIFIED')]),
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
}
|
|
20
408
|
export function toEventView(row) {
|
|
21
409
|
return {
|
|
22
410
|
seq: row.seq,
|
|
@@ -26,7 +414,7 @@ export function toEventView(row) {
|
|
|
26
414
|
actorId: row.actor_id,
|
|
27
415
|
targetType: row.target_type,
|
|
28
416
|
targetId: row.target_id,
|
|
29
|
-
payload: parseJson(row.payload_json),
|
|
417
|
+
payload: sanitizePublicJsonObject(parseJson(row.payload_json)),
|
|
30
418
|
createdAt: row.created_at,
|
|
31
419
|
};
|
|
32
420
|
}
|
|
@@ -39,43 +427,105 @@ export function toBindingView(row) {
|
|
|
39
427
|
runtimeType: row.runtime_type,
|
|
40
428
|
sessionDisplay: maskSessionId(row.session_id),
|
|
41
429
|
sessionBound: Boolean(row.session_id),
|
|
42
|
-
modelName: row.model_name,
|
|
43
|
-
agentName: row.agent_name,
|
|
44
|
-
sessionMeta: row.session_meta ? parseJson(row.session_meta) : null,
|
|
45
|
-
executionProfile: profile ? {
|
|
430
|
+
modelName: row.model_name === null ? null : sanitizePublicJsonString(row.model_name, 'private-config'),
|
|
431
|
+
agentName: row.agent_name === null ? null : sanitizePublicJsonString(row.agent_name, 'private-config'),
|
|
432
|
+
sessionMeta: row.session_meta ? sanitizePublicJsonObject(parseJson(row.session_meta)) : null,
|
|
433
|
+
executionProfile: profile ? {
|
|
434
|
+
provider: profile.provider === undefined ? null : sanitizePublicJsonString(profile.provider, 'private-config'),
|
|
435
|
+
model: profile.model === undefined ? null : sanitizePublicJsonString(profile.model, 'private-config'),
|
|
436
|
+
} : null,
|
|
46
437
|
createdAt: row.created_at,
|
|
47
438
|
};
|
|
48
439
|
}
|
|
49
440
|
/**
|
|
50
|
-
* v0.5.2(M1-B/P0-C
|
|
51
|
-
*
|
|
441
|
+
* v0.5.2(M1-B/P0-C):会话标识脱敏——公共 GUI 读面只暴露尾部(如 `…8f21`)。
|
|
442
|
+
* Event payload 和 Binding session metadata 也必须经过递归有界脱敏。
|
|
52
443
|
*/
|
|
53
444
|
function maskSessionId(id) {
|
|
54
445
|
if (!id)
|
|
55
446
|
return null;
|
|
56
|
-
return id.length > 8 ? `…${id.slice(-8)}` :
|
|
447
|
+
return id.length > 8 ? `…${id.slice(-8)}` : REDACTED_JSON_VALUE;
|
|
57
448
|
}
|
|
58
449
|
export function toTerritoryView(row) {
|
|
59
450
|
return {
|
|
60
451
|
territoryId: row.territory_id,
|
|
61
452
|
name: row.name,
|
|
62
|
-
workspacePath: row.workspace_path,
|
|
63
|
-
summary: row.summary,
|
|
453
|
+
workspacePath: row.workspace_path === null ? null : sanitizePublicJsonString(row.workspace_path),
|
|
454
|
+
summary: row.summary === null ? null : sanitizePublicJsonString(row.summary),
|
|
64
455
|
status: row.status,
|
|
65
456
|
createdAt: row.created_at,
|
|
66
457
|
};
|
|
67
458
|
}
|
|
459
|
+
function toTaskAssignmentHistoryView(row) {
|
|
460
|
+
return {
|
|
461
|
+
assignmentId: publicReferenceId(row.assignment_id) ?? 'unknown',
|
|
462
|
+
workerBindingId: publicReferenceId(row.worker_binding_id) ?? 'unknown',
|
|
463
|
+
assignedByBindingId: publicReferenceId(row.assigned_by) ?? 'unknown',
|
|
464
|
+
assignedAt: safeDate(row.assigned_at),
|
|
465
|
+
endedAt: safeDate(row.ended_at),
|
|
466
|
+
endReason: row.end_reason ? boundedText(row.end_reason) || null : null,
|
|
467
|
+
previousAssignmentId: publicReferenceId(row.previous_assignment_id),
|
|
468
|
+
handoffReason: row.handoff_reason ? boundedText(row.handoff_reason) || null : null,
|
|
469
|
+
sourceRefs: boundedSourceRefs([tableSource('task_assignments', row.assignment_id)]),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function toSupervisorDecisionView(row) {
|
|
473
|
+
const payload = parseJson(row.payload_json);
|
|
474
|
+
const payloadReference = (value) => typeof value === 'string' ? publicReferenceId(value) : null;
|
|
475
|
+
const payloadText = (value) => {
|
|
476
|
+
if (typeof value !== 'string')
|
|
477
|
+
return null;
|
|
478
|
+
return boundedText(value) || null;
|
|
479
|
+
};
|
|
480
|
+
const handoff = row.event_type === 'TASK_HANDED_OFF';
|
|
481
|
+
const eventSupervisorId = row.actor_role === 'SUPERVISOR' ? publicReferenceId(row.actor_id) : null;
|
|
482
|
+
const reviewerBindingId = handoff
|
|
483
|
+
? eventSupervisorId
|
|
484
|
+
: payloadReference(payload.reviewer_binding_id) ?? eventSupervisorId;
|
|
485
|
+
const fromAssignmentId = handoff ? payloadReference(payload.from_assignment_id) : null;
|
|
486
|
+
const fromWorkerBindingId = handoff ? payloadReference(payload.from_worker_binding_id) : null;
|
|
487
|
+
const toAssignmentId = handoff ? payloadReference(payload.to_assignment_id) : null;
|
|
488
|
+
const toWorkerBindingId = handoff ? payloadReference(payload.to_worker_binding_id) : null;
|
|
489
|
+
const reviewedAttemptNo = typeof payload.reviewed_attempt_no === 'number'
|
|
490
|
+
&& Number.isSafeInteger(payload.reviewed_attempt_no)
|
|
491
|
+
&& payload.reviewed_attempt_no >= 0
|
|
492
|
+
? payload.reviewed_attempt_no
|
|
493
|
+
: null;
|
|
494
|
+
const refs = [
|
|
495
|
+
eventSource(row.seq),
|
|
496
|
+
tableSource('tasks', row.target_id),
|
|
497
|
+
...(reviewerBindingId ? [tableSource('role_bindings', reviewerBindingId)] : []),
|
|
498
|
+
...(fromAssignmentId ? [tableSource('task_assignments', fromAssignmentId)] : []),
|
|
499
|
+
...(fromWorkerBindingId ? [tableSource('role_bindings', fromWorkerBindingId)] : []),
|
|
500
|
+
...(toAssignmentId ? [tableSource('task_assignments', toAssignmentId)] : []),
|
|
501
|
+
...(toWorkerBindingId ? [tableSource('role_bindings', toWorkerBindingId)] : []),
|
|
502
|
+
];
|
|
503
|
+
return {
|
|
504
|
+
seq: row.seq,
|
|
505
|
+
decision: handoff ? 'HANDOFF' : payloadText(payload.decision) ?? (boundedText(row.event_type) || 'UNKNOWN'),
|
|
506
|
+
reason: payloadText(handoff ? payload.handoff_reason : payload.reason),
|
|
507
|
+
reviewerBindingId,
|
|
508
|
+
reviewedAttemptNo,
|
|
509
|
+
claimedOutcome: handoff ? null : payloadText(payload.claimed_outcome),
|
|
510
|
+
fromAssignmentId,
|
|
511
|
+
fromWorkerBindingId,
|
|
512
|
+
toAssignmentId,
|
|
513
|
+
toWorkerBindingId,
|
|
514
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
515
|
+
createdAt: row.created_at,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
68
518
|
export function toClaimView(row) {
|
|
69
519
|
const payload = parseJson(row.result_json);
|
|
70
520
|
return {
|
|
71
521
|
resultId: row.result_id,
|
|
72
522
|
attemptNo: row.attempt_no,
|
|
73
523
|
workerBindingId: row.worker_binding_id,
|
|
74
|
-
sessionId: row.session_id,
|
|
524
|
+
sessionId: maskSessionId(row.session_id),
|
|
75
525
|
claimedOutcome: row.outcome,
|
|
76
|
-
summary: typeof payload.summary === 'string' ? payload.summary : null,
|
|
77
|
-
artifacts:
|
|
78
|
-
risks:
|
|
526
|
+
summary: typeof payload.summary === 'string' ? sanitizePublicJsonString(payload.summary) : null,
|
|
527
|
+
artifacts: sanitizePublicStringList(payload.artifacts),
|
|
528
|
+
risks: sanitizePublicStringList(payload.risks),
|
|
79
529
|
createdAt: row.created_at,
|
|
80
530
|
};
|
|
81
531
|
}
|
|
@@ -85,9 +535,9 @@ export function toExecutionView(row) {
|
|
|
85
535
|
taskId: row.task_id,
|
|
86
536
|
attemptNo: row.attempt_no,
|
|
87
537
|
workerBindingId: row.worker_binding_id,
|
|
88
|
-
sessionId: row.session_id,
|
|
538
|
+
sessionId: maskSessionId(row.session_id),
|
|
89
539
|
state: row.state,
|
|
90
|
-
detail: row.detail,
|
|
540
|
+
detail: row.detail === null ? null : sanitizePublicJsonString(row.detail),
|
|
91
541
|
startedAt: row.started_at,
|
|
92
542
|
heartbeatAt: row.heartbeat_at,
|
|
93
543
|
endedAt: row.ended_at,
|
|
@@ -142,8 +592,9 @@ function toDecisionView(row) {
|
|
|
142
592
|
createdAt: row.created_at,
|
|
143
593
|
};
|
|
144
594
|
}
|
|
145
|
-
function toDispatchView(row) {
|
|
595
|
+
function toDispatchView(row, store) {
|
|
146
596
|
return {
|
|
597
|
+
usage: readDispatchUsage(store, row.dispatch_id),
|
|
147
598
|
dispatchId: row.dispatch_id,
|
|
148
599
|
leaseId: row.lease_id,
|
|
149
600
|
executionId: row.execution_id,
|
|
@@ -165,7 +616,7 @@ export function buildGovernance(store, kingdomId) {
|
|
|
165
616
|
workerSessions: store.listAffinities(kingdomId).map(toAffinityView),
|
|
166
617
|
leases: store.listLeases(kingdomId).map(toLeaseView),
|
|
167
618
|
decisions: store.listCapabilityDecisions(kingdomId).map(toDecisionView),
|
|
168
|
-
dispatches: store.listDispatches(kingdomId).map(toDispatchView),
|
|
619
|
+
dispatches: store.listDispatches(kingdomId).map(row => toDispatchView(row, store)),
|
|
169
620
|
};
|
|
170
621
|
}
|
|
171
622
|
/** 任务级 Runtime Governance 投影(TaskDetail 用)。 */
|
|
@@ -176,7 +627,7 @@ export function buildTaskGovernance(store, taskId) {
|
|
|
176
627
|
workerSessions: [],
|
|
177
628
|
leases: store.listLeases(store.getDefaultKingdom()?.kingdom_id ?? '').filter(l => l.task_id === taskId).map(toLeaseView),
|
|
178
629
|
decisions: store.listCapabilityDecisions(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(toDecisionView),
|
|
179
|
-
dispatches: store.listDispatches(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(toDispatchView),
|
|
630
|
+
dispatches: store.listDispatches(store.getDefaultKingdom()?.kingdom_id ?? '').filter(d => d.task_id === taskId).map(row => toDispatchView(row, store)),
|
|
180
631
|
};
|
|
181
632
|
}
|
|
182
633
|
/**
|
|
@@ -187,27 +638,33 @@ export function buildTaskGovernance(store, taskId) {
|
|
|
187
638
|
*/
|
|
188
639
|
export function allowedActionsFor(task, execution) {
|
|
189
640
|
const status = asTaskStatus(task.status);
|
|
190
|
-
const
|
|
641
|
+
const executionState = execution ? asExecutionState(execution.state) : null;
|
|
642
|
+
const live = executionState !== null && isLiveExecutionState(executionState);
|
|
191
643
|
switch (status) {
|
|
192
644
|
case 'CREATED':
|
|
193
645
|
return ['assign'];
|
|
194
646
|
case 'ASSIGNED':
|
|
195
|
-
return ['start'];
|
|
647
|
+
return executionState === 'RECOVERING' && execution?.execution_contract === 'GOVERNED_PERSISTENT' ? ['reconcile'] : isGovernedStartBlocked(execution) ? [] : ['start'];
|
|
196
648
|
case 'RUNNING': {
|
|
197
|
-
|
|
649
|
+
// RECOVERING means the Runtime outcome is unknown. Exposing start here would
|
|
650
|
+
// invite a duplicate attempt before reconciliation, so the projection fails closed.
|
|
651
|
+
if (executionState === 'RECOVERING')
|
|
652
|
+
return execution?.execution_contract === 'GOVERNED_PERSISTENT' ? ['reconcile'] : [];
|
|
653
|
+
if (execution === null || !live) {
|
|
198
654
|
// REWORK 之后:任务已回 RUNNING,但还没有新的 Execution。
|
|
199
655
|
return ['start'];
|
|
200
656
|
}
|
|
201
|
-
const state = asExecutionState(execution.state);
|
|
202
657
|
const actions = ['execution:abort'];
|
|
203
|
-
if (
|
|
658
|
+
if (executionState === 'PAUSED')
|
|
204
659
|
actions.unshift('execution:resume');
|
|
205
660
|
else if (execution.pause_requested_at === null)
|
|
206
661
|
actions.unshift('execution:pause');
|
|
662
|
+
else if (execution.execution_contract === 'LEGACY_COMPAT')
|
|
663
|
+
actions.unshift('execution:resume');
|
|
207
664
|
return actions;
|
|
208
665
|
}
|
|
209
666
|
case 'REVIEW':
|
|
210
|
-
return ['review:accept', 'review:rework', 'review:fail'];
|
|
667
|
+
return ['review:accept', 'review:rework', 'review:fail', 'review:handoff'];
|
|
211
668
|
default:
|
|
212
669
|
return [];
|
|
213
670
|
}
|
|
@@ -223,7 +680,7 @@ export function toTaskView(store, task) {
|
|
|
223
680
|
acceptanceCriteria: task.acceptance_criteria,
|
|
224
681
|
status: task.status,
|
|
225
682
|
assignedBindingId: task.assigned_binding_id,
|
|
226
|
-
resultSummary: task.result_summary,
|
|
683
|
+
resultSummary: task.result_summary === null ? null : sanitizePublicJsonString(task.result_summary),
|
|
227
684
|
attemptCount: store.maxAttemptNo(task.task_id),
|
|
228
685
|
latestClaim: claim ? toClaimView(claim) : null,
|
|
229
686
|
latestExecution: execution ? toExecutionView(execution) : null,
|
|
@@ -232,17 +689,94 @@ export function toTaskView(store, task) {
|
|
|
232
689
|
updatedAt: task.updated_at,
|
|
233
690
|
};
|
|
234
691
|
}
|
|
692
|
+
function eventBindingId(input, event, role) {
|
|
693
|
+
const payload = parseJson(event.payload_json);
|
|
694
|
+
const payloadBindingId = (key) => {
|
|
695
|
+
const value = payload[key];
|
|
696
|
+
return typeof value === 'string' ? value : null;
|
|
697
|
+
};
|
|
698
|
+
if (role === 'WORKER') {
|
|
699
|
+
const explicit = payloadBindingId('worker_binding_id')
|
|
700
|
+
?? payloadBindingId('to_worker_binding_id');
|
|
701
|
+
if (explicit)
|
|
702
|
+
return explicit;
|
|
703
|
+
// TASK_ACCEPTED is emitted by the Supervisor and carries no worker field;
|
|
704
|
+
// the task's current assigned_binding_id is the exact binding relation for
|
|
705
|
+
// that task. Do not use Task.RUNNING as a live execution signal here.
|
|
706
|
+
if (event.event_type === 'TASK_ACCEPTED') {
|
|
707
|
+
return input.tasks.find(task => task.task_id === event.target_id)?.assigned_binding_id ?? null;
|
|
708
|
+
}
|
|
709
|
+
return event.actor_role === 'WORKER' ? event.actor_id : null;
|
|
710
|
+
}
|
|
711
|
+
if (role === 'SUPERVISOR') {
|
|
712
|
+
return event.actor_role === 'SUPERVISOR'
|
|
713
|
+
? event.actor_id
|
|
714
|
+
: payloadBindingId('reviewer_binding_id') ?? payloadBindingId('supervisor_binding_id');
|
|
715
|
+
}
|
|
716
|
+
if (role === 'CHANCELLOR') {
|
|
717
|
+
return event.actor_role === 'CHANCELLOR'
|
|
718
|
+
? event.actor_id
|
|
719
|
+
: payloadBindingId('chancellor_binding_id');
|
|
720
|
+
}
|
|
721
|
+
return event.actor_role === 'OWNER' ? event.actor_id : null;
|
|
722
|
+
}
|
|
723
|
+
function activeBindingFor(input, bindingId) {
|
|
724
|
+
return input.bindings.find(binding => binding.binding_id === bindingId && binding.status === 'ACTIVE') ?? null;
|
|
725
|
+
}
|
|
726
|
+
function eventTask(input, event) {
|
|
727
|
+
if (!event.target_id)
|
|
728
|
+
return null;
|
|
729
|
+
if (event.target_type !== null && event.target_type !== 'task')
|
|
730
|
+
return null;
|
|
731
|
+
return input.tasks.find(task => task.task_id === event.target_id) ?? null;
|
|
732
|
+
}
|
|
235
733
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* ——而不是"往前翻到第一条没过期的"。因此调用方必须把一组互斥的候选类型
|
|
240
|
-
* 一次性传进来,再按 `event_type` 分派;分成多次调用会让较早的动作
|
|
241
|
-
* 盖住较晚的动作(例如刚 ACCEPT 完却还在播派发)。
|
|
734
|
+
* A transient event is usable only when its actor, role, task assignment, and
|
|
735
|
+
* territory scope all agree. This prevents one recent event from broadcasting
|
|
736
|
+
* to every Supervisor/Worker in the kingdom.
|
|
242
737
|
*/
|
|
243
|
-
function
|
|
738
|
+
function eventMatchesBinding(input, event, role, bindingId) {
|
|
739
|
+
const binding = activeBindingFor(input, bindingId);
|
|
740
|
+
if (!binding || binding.role_type !== role)
|
|
741
|
+
return false;
|
|
742
|
+
if (eventBindingId(input, event, role) !== bindingId)
|
|
743
|
+
return false;
|
|
744
|
+
if (role === 'OWNER')
|
|
745
|
+
return true;
|
|
746
|
+
const task = eventTask(input, event);
|
|
747
|
+
if (!task)
|
|
748
|
+
return false;
|
|
749
|
+
const territory = input.territories.find(item => item.territory_id === task.territory_id);
|
|
750
|
+
if (!territory)
|
|
751
|
+
return false;
|
|
752
|
+
if (role === 'CHANCELLOR') {
|
|
753
|
+
return event.actor_role === 'CHANCELLOR';
|
|
754
|
+
}
|
|
755
|
+
if (role === 'SUPERVISOR') {
|
|
756
|
+
return territory.supervisor_binding_id === bindingId
|
|
757
|
+
&& event.actor_role === 'SUPERVISOR';
|
|
758
|
+
}
|
|
759
|
+
// Worker transient evidence must point to the task's exact current
|
|
760
|
+
// assignment. The canonical WORKER_EXECUTION_FAILED event is emitted by
|
|
761
|
+
// the Supervisor and carries the affected Worker in payload, so both actor
|
|
762
|
+
// scope and payload binding must agree.
|
|
763
|
+
if (task.assigned_binding_id !== bindingId)
|
|
764
|
+
return false;
|
|
765
|
+
if (event.event_type === 'WORKER_EXECUTION_FAILED') {
|
|
766
|
+
if (event.actor_role !== 'SUPERVISOR' || !event.actor_id)
|
|
767
|
+
return false;
|
|
768
|
+
const supervisorBinding = activeBindingFor(input, event.actor_id);
|
|
769
|
+
return supervisorBinding?.role_type === 'SUPERVISOR'
|
|
770
|
+
&& supervisorBinding.binding_id === territory.supervisor_binding_id;
|
|
771
|
+
}
|
|
772
|
+
if (event.event_type === 'TASK_ASSIGNED' || event.event_type === 'TASK_ACCEPTED') {
|
|
773
|
+
return event.actor_role === 'SUPERVISOR' && territory.supervisor_binding_id === event.actor_id;
|
|
774
|
+
}
|
|
775
|
+
return event.actor_role === 'WORKER';
|
|
776
|
+
}
|
|
777
|
+
function latestWithinFor(input, types, role, bindingId) {
|
|
244
778
|
for (const event of input.events) {
|
|
245
|
-
if (!types.includes(event.event_type))
|
|
779
|
+
if (!types.includes(event.event_type) || !eventMatchesBinding(input, event, role, bindingId))
|
|
246
780
|
continue;
|
|
247
781
|
const age = input.nowMs - Date.parse(event.created_at);
|
|
248
782
|
if (Number.isNaN(age))
|
|
@@ -266,6 +800,7 @@ function actor(role, binding, state, activity, extra = {}) {
|
|
|
266
800
|
remainingMs: null,
|
|
267
801
|
fallbackState: 'idle',
|
|
268
802
|
sourceSeq: null,
|
|
803
|
+
indeterminate: false,
|
|
269
804
|
...extra,
|
|
270
805
|
};
|
|
271
806
|
}
|
|
@@ -285,7 +820,7 @@ function transientFrom(input, event) {
|
|
|
285
820
|
function chancellorActor(input, binding) {
|
|
286
821
|
if (!binding)
|
|
287
822
|
return actor('CHANCELLOR', null, 'absent', null);
|
|
288
|
-
const planned =
|
|
823
|
+
const planned = latestWithinFor(input, ['TASK_PLANNED'], 'CHANCELLOR', binding.binding_id);
|
|
289
824
|
if (planned) {
|
|
290
825
|
return actor('CHANCELLOR', binding, 'planning', 'plan', {
|
|
291
826
|
taskId: planned.target_id,
|
|
@@ -305,7 +840,7 @@ function chancellorActor(input, binding) {
|
|
|
305
840
|
function supervisorActor(input, binding) {
|
|
306
841
|
if (!binding)
|
|
307
842
|
return actor('SUPERVISOR', null, 'absent', null);
|
|
308
|
-
const recent =
|
|
843
|
+
const recent = latestWithinFor(input, ['TASK_ASSIGNED', 'TASK_REWORK_REQUESTED', 'TASK_ACCEPTED', 'TASK_FAILED'], 'SUPERVISOR', binding.binding_id);
|
|
309
844
|
if (recent) {
|
|
310
845
|
const common = { taskId: recent.target_id, fallbackState: 'idle', ...transientFrom(input, recent) };
|
|
311
846
|
switch (recent.event_type) {
|
|
@@ -320,7 +855,12 @@ function supervisorActor(input, binding) {
|
|
|
320
855
|
}
|
|
321
856
|
}
|
|
322
857
|
// 持续状态:有待审 Claim 就一直在复核。
|
|
323
|
-
const pending = input.tasks.find(
|
|
858
|
+
const pending = input.tasks.find(task => {
|
|
859
|
+
if (task.status !== 'REVIEW')
|
|
860
|
+
return false;
|
|
861
|
+
const territory = input.territories.find(item => item.territory_id === task.territory_id);
|
|
862
|
+
return territory?.supervisor_binding_id === binding.binding_id;
|
|
863
|
+
});
|
|
324
864
|
if (pending) {
|
|
325
865
|
return actor('SUPERVISOR', binding, 'reviewing', 'review', {
|
|
326
866
|
taskId: pending.task_id,
|
|
@@ -339,7 +879,25 @@ function supervisorActor(input, binding) {
|
|
|
339
879
|
function workerActor(input, binding) {
|
|
340
880
|
if (!binding)
|
|
341
881
|
return actor('WORKER', null, 'absent', null);
|
|
342
|
-
const
|
|
882
|
+
const territoryIds = new Set(input.tasks
|
|
883
|
+
.filter(task => task.assigned_binding_id === binding.binding_id)
|
|
884
|
+
.map(task => task.territory_id)
|
|
885
|
+
.filter(territoryId => input.territories.some(territory => territory.territory_id === territoryId)));
|
|
886
|
+
const territoryIndeterminate = territoryIds.size !== 1;
|
|
887
|
+
const liveExecutions = input.executions.filter(execution => {
|
|
888
|
+
if (execution.worker_binding_id !== binding.binding_id || !isLiveExecutionState(asExecutionState(execution.state)))
|
|
889
|
+
return false;
|
|
890
|
+
const task = input.tasks.find(item => item.task_id === execution.task_id);
|
|
891
|
+
return task?.assigned_binding_id === binding.binding_id
|
|
892
|
+
&& input.territories.some(territory => territory.territory_id === task.territory_id);
|
|
893
|
+
});
|
|
894
|
+
if (liveExecutions.length > 1) {
|
|
895
|
+
return actor('WORKER', binding, 'confused', null, {
|
|
896
|
+
fallbackState: 'idle',
|
|
897
|
+
indeterminate: true,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
const live = liveExecutions[0] ?? null;
|
|
343
901
|
if (live) {
|
|
344
902
|
const state = asExecutionState(live.state);
|
|
345
903
|
const common = {
|
|
@@ -347,6 +905,7 @@ function workerActor(input, binding) {
|
|
|
347
905
|
executionId: live.execution_id,
|
|
348
906
|
attemptNo: live.attempt_no,
|
|
349
907
|
since: live.started_at,
|
|
908
|
+
indeterminate: territoryIndeterminate,
|
|
350
909
|
};
|
|
351
910
|
if (state === 'PAUSED') {
|
|
352
911
|
return actor('WORKER', binding, 'sleeping', null, { ...common, fallbackState: 'sleeping' });
|
|
@@ -357,7 +916,7 @@ function workerActor(input, binding) {
|
|
|
357
916
|
}
|
|
358
917
|
// 一次性表演:庆祝(任务刚被 ACCEPT)与困惑(宿主观察到执行没跑起来)。
|
|
359
918
|
// 同样一次性取最新再分派,避免旧事件盖住新事件。
|
|
360
|
-
const recent =
|
|
919
|
+
const recent = latestWithinFor(input, ['TASK_ACCEPTED', 'WORKER_EXECUTION_FAILED'], 'WORKER', binding.binding_id);
|
|
361
920
|
if (recent) {
|
|
362
921
|
const common = { taskId: recent.target_id, fallbackState: 'idle', ...transientFrom(input, recent) };
|
|
363
922
|
return recent.event_type === 'TASK_ACCEPTED'
|
|
@@ -370,21 +929,734 @@ function workerActor(input, binding) {
|
|
|
370
929
|
return actor('WORKER', binding, 'waiting', null, {
|
|
371
930
|
taskId: awaiting.task_id,
|
|
372
931
|
since: awaiting.updated_at,
|
|
932
|
+
indeterminate: territoryIndeterminate,
|
|
373
933
|
});
|
|
374
934
|
}
|
|
375
|
-
return actor('WORKER', binding, 'idle', null);
|
|
935
|
+
return actor('WORKER', binding, 'idle', null, { indeterminate: territoryIndeterminate });
|
|
376
936
|
}
|
|
377
937
|
/** 计算全部角色此刻的表演语义。 */
|
|
378
938
|
export function projectStage(input) {
|
|
379
|
-
const
|
|
380
|
-
const
|
|
939
|
+
const bindingsFor = (role) => input.bindings.filter(binding => binding.role_type === role && binding.status === 'ACTIVE');
|
|
940
|
+
const withAbsentFallback = (role, bindings, project) => bindings.length > 0
|
|
941
|
+
? bindings.map(project)
|
|
942
|
+
: [project(null)];
|
|
381
943
|
return [
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
944
|
+
...withAbsentFallback('OWNER', bindingsFor('OWNER'), binding => binding ? actor('OWNER', binding, 'idle', null) : actor('OWNER', null, 'absent', null)),
|
|
945
|
+
...withAbsentFallback('CHANCELLOR', bindingsFor('CHANCELLOR'), binding => chancellorActor(input, binding)),
|
|
946
|
+
...withAbsentFallback('SUPERVISOR', bindingsFor('SUPERVISOR'), binding => supervisorActor(input, binding)),
|
|
947
|
+
...withAbsentFallback('WORKER', bindingsFor('WORKER'), binding => workerActor(input, binding)),
|
|
386
948
|
];
|
|
387
949
|
}
|
|
950
|
+
function attentionItem(id, severity, entityRef, code, summary, refs) {
|
|
951
|
+
const reason = attentionReason(code, refs);
|
|
952
|
+
return {
|
|
953
|
+
id,
|
|
954
|
+
severity,
|
|
955
|
+
entityRef,
|
|
956
|
+
reason,
|
|
957
|
+
summary: boundedText(summary),
|
|
958
|
+
sourceRefs: reason.sourceRefs,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function claimExecutionMismatch(claim, execution) {
|
|
962
|
+
const expected = claim.outcome === 'COMPLETED' || claim.outcome === 'FAILED' ? claim.outcome : null;
|
|
963
|
+
return expected !== null
|
|
964
|
+
&& execution.attempt_no === claim.attempt_no
|
|
965
|
+
&& executionTerminality(execution.state) === 'TERMINAL'
|
|
966
|
+
&& execution.state !== expected;
|
|
967
|
+
}
|
|
968
|
+
/** 固定 reason code 的 Attention 派生;只读,不修复、不重试、不推进状态。 */
|
|
969
|
+
export function buildAttention(store, kingdomId) {
|
|
970
|
+
const kingdom = store.getDefaultKingdom();
|
|
971
|
+
if (!kingdom || kingdom.kingdom_id !== kingdomId) {
|
|
972
|
+
return [attentionItem('kingdom:missing', 'UNKNOWN', null, 'CONFIGURATION_INCOMPLETE', 'Kingdom is not initialized; projection remains UNKNOWN', [ruleSource('KINGDOM_PRESENT')])];
|
|
973
|
+
}
|
|
974
|
+
const items = [];
|
|
975
|
+
const tasks = store.listTasks(kingdomId);
|
|
976
|
+
for (const task of tasks) {
|
|
977
|
+
const claim = store.latestWorkerResult(task.task_id);
|
|
978
|
+
const execution = store.latestExecution(task.task_id);
|
|
979
|
+
if (claim && execution && claimExecutionMismatch(claim, execution)) {
|
|
980
|
+
items.push(attentionItem(timelineId('mismatch', task.task_id), 'CRITICAL', publicEntityRef('task', task.task_id), 'CLAIM_EXECUTION_MISMATCH', `Claim ${boundedText(claim.outcome)} does not match terminal Execution ${boundedText(execution.state)}`, [tableSource('worker_results', claim.result_id), tableSource('executions', execution.execution_id)]));
|
|
981
|
+
}
|
|
982
|
+
if (execution?.state === 'RECOVERING') {
|
|
983
|
+
items.push(attentionItem(timelineId('execution-recovering', execution.execution_id), 'UNKNOWN', publicEntityRef('task', task.task_id), 'EXECUTION_RECOVERING', 'Execution is RECOVERING; reconcile Runtime evidence before any new attempt', [tableSource('executions', execution.execution_id), ruleSource('RECOVERING_FAIL_CLOSED')]));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
// v4 是只读地标;非 v4 不访问这些表,也不伪造治理状态。
|
|
987
|
+
if (store.isSchemaV4) {
|
|
988
|
+
for (const decision of store.listCapabilityDecisions(kingdomId)) {
|
|
989
|
+
if (decision.decision !== 'DENIED')
|
|
990
|
+
continue;
|
|
991
|
+
items.push(attentionItem(timelineId('decision', decision.decision_id), 'ATTENTION', publicEntityRef('task', decision.task_id), 'CAPABILITY_DENIED', `Capability decision DENIED${decision.reason_code ? ` (${boundedText(decision.reason_code)})` : ''}`, [tableSource('capability_decisions', decision.decision_id)]));
|
|
992
|
+
}
|
|
993
|
+
for (const lease of store.listLeases(kingdomId)) {
|
|
994
|
+
if (lease.state !== 'RECOVERING' && !(lease.released_at === null && lease.state === 'TERMINAL'))
|
|
995
|
+
continue;
|
|
996
|
+
items.push(attentionItem(timelineId('lease', lease.lease_id), 'UNKNOWN', publicEntityRef('task', lease.task_id), 'LEASE_NOT_RELEASED', `Lease ${boundedText(lease.state)} has no confirmed release`, [tableSource('execution_leases', lease.lease_id)]));
|
|
997
|
+
}
|
|
998
|
+
for (const dispatch of store.listDispatches(kingdomId)) {
|
|
999
|
+
if (dispatch.state === 'RECOVERING') {
|
|
1000
|
+
items.push(attentionItem(timelineId('dispatch', dispatch.dispatch_id), 'UNKNOWN', publicEntityRef('task', dispatch.task_id), 'LEASE_NOT_RELEASED', 'Dispatch is RECOVERING; no automatic retry is inferred', [tableSource('dispatch_records', dispatch.dispatch_id)]));
|
|
1001
|
+
}
|
|
1002
|
+
else if (dispatch.receipt_json !== null && !dispatch.terminal_evidence_json
|
|
1003
|
+
&& dispatch.state !== 'TERMINAL' && dispatch.state !== 'FAILED') {
|
|
1004
|
+
items.push(attentionItem(timelineId('terminal', dispatch.dispatch_id), 'UNKNOWN', publicEntityRef('task', dispatch.task_id), 'TERMINAL_EVIDENCE_MISSING', 'Dispatch receipt exists but terminal evidence is missing', [tableSource('dispatch_records', dispatch.dispatch_id), ruleSource('TERMINAL_EVIDENCE_REQUIRED')]));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return items.slice(0, MAX_PROJECTION_TIMELINE_ITEMS);
|
|
1009
|
+
}
|
|
1010
|
+
function timelineAttention(attentions, entityRef) {
|
|
1011
|
+
if (!entityRef)
|
|
1012
|
+
return null;
|
|
1013
|
+
return attentions.find(item => item.entityRef?.id === entityRef.id)?.reason ?? null;
|
|
1014
|
+
}
|
|
1015
|
+
function timelineSourceRefKey(ref) {
|
|
1016
|
+
return JSON.stringify([ref.sourceType, ref.entityType, ref.entityId, ref.eventSeq, ref.ruleCode]);
|
|
1017
|
+
}
|
|
1018
|
+
function sortTimeline(items) {
|
|
1019
|
+
return [...items].sort((a, b) => {
|
|
1020
|
+
const aTime = a.occurredAt ? Date.parse(a.occurredAt) : Number.POSITIVE_INFINITY;
|
|
1021
|
+
const bTime = b.occurredAt ? Date.parse(b.occurredAt) : Number.POSITIVE_INFINITY;
|
|
1022
|
+
return (aTime - bTime) || a.id.localeCompare(b.id);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function mismatchEvidenceTime(explanation, items) {
|
|
1026
|
+
const sourceKeys = new Set(explanation.sourceRefs
|
|
1027
|
+
.filter(ref => ref.sourceType === 'table-row'
|
|
1028
|
+
&& (ref.entityType === 'worker_results' || ref.entityType === 'executions'))
|
|
1029
|
+
.map(timelineSourceRefKey));
|
|
1030
|
+
const evidenceTimes = items
|
|
1031
|
+
.filter(item => item.id !== explanation.id
|
|
1032
|
+
&& item.sourceRefs.some(ref => sourceKeys.has(timelineSourceRefKey(ref))))
|
|
1033
|
+
.map(item => item.occurredAt ? Date.parse(item.occurredAt) : Number.NaN)
|
|
1034
|
+
.filter(Number.isFinite);
|
|
1035
|
+
return evidenceTimes.length > 0 ? Math.max(...evidenceTimes) : Number.NEGATIVE_INFINITY;
|
|
1036
|
+
}
|
|
1037
|
+
function mismatchEvidenceGroups(items) {
|
|
1038
|
+
const ordered = sortTimeline(items);
|
|
1039
|
+
const newestFirst = [...ordered].reverse();
|
|
1040
|
+
return newestFirst
|
|
1041
|
+
.filter(item => item.kind === 'DERIVED_EXPLANATION'
|
|
1042
|
+
&& item.attentionReason?.code === 'CLAIM_EXECUTION_MISMATCH')
|
|
1043
|
+
.map(explanation => {
|
|
1044
|
+
const sourceKeys = new Set(explanation.sourceRefs
|
|
1045
|
+
.filter(ref => ref.sourceType === 'table-row'
|
|
1046
|
+
&& (ref.entityType === 'worker_results' || ref.entityType === 'executions'))
|
|
1047
|
+
.map(timelineSourceRefKey));
|
|
1048
|
+
const related = newestFirst.filter(item => item.id !== explanation.id
|
|
1049
|
+
&& item.sourceRefs.some(ref => sourceKeys.has(timelineSourceRefKey(ref))));
|
|
1050
|
+
return {
|
|
1051
|
+
explanation,
|
|
1052
|
+
group: [explanation, ...related],
|
|
1053
|
+
evidenceTime: mismatchEvidenceTime(explanation, items),
|
|
1054
|
+
};
|
|
1055
|
+
})
|
|
1056
|
+
.sort((a, b) => (b.evidenceTime - a.evidenceTime)
|
|
1057
|
+
// ID is only a deterministic tie-break after actual evidence time; it never represents recency.
|
|
1058
|
+
|| a.explanation.id.localeCompare(b.explanation.id))
|
|
1059
|
+
.map(entry => entry.group);
|
|
1060
|
+
}
|
|
1061
|
+
function isMismatchExplanation(item) {
|
|
1062
|
+
return item.kind === 'DERIVED_EXPLANATION'
|
|
1063
|
+
&& item.attentionReason?.code === 'CLAIM_EXECUTION_MISMATCH';
|
|
1064
|
+
}
|
|
1065
|
+
/** 超限时保留最新证据;mismatch 的解释与其 Claim/Execution 成组保留。 */
|
|
1066
|
+
function retainTimelineEvidence(items) {
|
|
1067
|
+
const ordered = sortTimeline(items);
|
|
1068
|
+
if (ordered.length <= MAX_PROJECTION_TIMELINE_ITEMS)
|
|
1069
|
+
return ordered;
|
|
1070
|
+
const selected = [];
|
|
1071
|
+
const selectedIds = new Set();
|
|
1072
|
+
const add = (item) => {
|
|
1073
|
+
if (selected.length >= MAX_PROJECTION_TIMELINE_ITEMS || selectedIds.has(item.id))
|
|
1074
|
+
return;
|
|
1075
|
+
selected.push(item);
|
|
1076
|
+
selectedIds.add(item.id);
|
|
1077
|
+
};
|
|
1078
|
+
const addNonMismatch = (item) => {
|
|
1079
|
+
// Mismatch explanations may only enter through addGroup(), never as an orphan.
|
|
1080
|
+
if (isMismatchExplanation(item))
|
|
1081
|
+
return;
|
|
1082
|
+
add(item);
|
|
1083
|
+
};
|
|
1084
|
+
const addGroup = (group) => {
|
|
1085
|
+
const unique = group.filter(item => !selectedIds.has(item.id));
|
|
1086
|
+
if (selected.length + unique.length > MAX_PROJECTION_TIMELINE_ITEMS)
|
|
1087
|
+
return;
|
|
1088
|
+
for (const item of unique)
|
|
1089
|
+
add(item);
|
|
1090
|
+
};
|
|
1091
|
+
const newestFirst = [...ordered].reverse();
|
|
1092
|
+
// A negative explanation is only traceable when its two source rows remain visible too.
|
|
1093
|
+
for (const group of mismatchEvidenceGroups(items))
|
|
1094
|
+
addGroup(group);
|
|
1095
|
+
// Keep each existing projection layer represented when it has candidates.
|
|
1096
|
+
for (const kind of ['GOVERNANCE_FACT', 'RUNTIME_OBSERVATION', 'WORKER_CLAIM', 'DERIVED_EXPLANATION']) {
|
|
1097
|
+
const newest = newestFirst.find(item => item.kind === kind);
|
|
1098
|
+
if (newest)
|
|
1099
|
+
addNonMismatch(newest);
|
|
1100
|
+
}
|
|
1101
|
+
// Fill the remaining bounded capacity from newest to oldest, then restore display order.
|
|
1102
|
+
for (const item of newestFirst)
|
|
1103
|
+
addNonMismatch(item);
|
|
1104
|
+
return sortTimeline(selected);
|
|
1105
|
+
}
|
|
1106
|
+
/** 从现有 row/event/observation 重建 timeline;不是新 Ledger。 */
|
|
1107
|
+
export function buildTimeline(store, kingdomId, options = {}) {
|
|
1108
|
+
const attentions = buildAttention(store, kingdomId);
|
|
1109
|
+
const items = [];
|
|
1110
|
+
const events = store.listEvents(kingdomId, Math.min(options.eventLimit ?? 50, MAX_PROJECTION_TIMELINE_ITEMS));
|
|
1111
|
+
for (const event of events) {
|
|
1112
|
+
const usageObservation = event.event_type === 'DISPATCH_USAGE_OBSERVED';
|
|
1113
|
+
const refs = [eventSource(event.seq)];
|
|
1114
|
+
const entityRef = event.target_type ? publicEntityRef(event.target_type, event.target_id) : null;
|
|
1115
|
+
items.push({
|
|
1116
|
+
id: `event:${event.seq}`,
|
|
1117
|
+
kind: usageObservation ? 'RUNTIME_OBSERVATION' : 'GOVERNANCE_FACT',
|
|
1118
|
+
occurredAt: safeDate(event.created_at),
|
|
1119
|
+
entityRef,
|
|
1120
|
+
authoritativeState: stateValue(event.event_type, usageObservation ? 'RUNTIME_OBSERVATION' : 'GOVERNANCE_FACT', refs),
|
|
1121
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1122
|
+
allowedActions: null,
|
|
1123
|
+
attentionReason: timelineAttention(attentions, entityRef),
|
|
1124
|
+
terminality: 'UNKNOWN',
|
|
1125
|
+
summary: usageObservation ? 'Provider usage observation (coverage may be partial)' : `Governance Fact: ${boundedText(event.event_type)}`,
|
|
1126
|
+
requiresOwnerAction: false,
|
|
1127
|
+
rawEvidenceAvailable: false,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
for (const task of store.listTasks(kingdomId)) {
|
|
1131
|
+
for (const claim of store.listWorkerResults(task.task_id).slice(-10)) {
|
|
1132
|
+
const refs = [tableSource('worker_results', claim.result_id)];
|
|
1133
|
+
const entityRef = publicEntityRef('task', task.task_id);
|
|
1134
|
+
items.push({
|
|
1135
|
+
id: timelineId('claim', claim.result_id),
|
|
1136
|
+
kind: 'WORKER_CLAIM',
|
|
1137
|
+
occurredAt: safeDate(claim.created_at),
|
|
1138
|
+
entityRef,
|
|
1139
|
+
authoritativeState: null,
|
|
1140
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1141
|
+
allowedActions: null,
|
|
1142
|
+
attentionReason: timelineAttention(attentions, entityRef),
|
|
1143
|
+
terminality: 'UNKNOWN',
|
|
1144
|
+
summary: `Worker Claim: ${boundedText(claim.outcome) || 'UNKNOWN'}`,
|
|
1145
|
+
requiresOwnerAction: false,
|
|
1146
|
+
rawEvidenceAvailable: false,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
for (const execution of store.listExecutions(task.task_id).slice(-10)) {
|
|
1150
|
+
const refs = [tableSource('executions', execution.execution_id)];
|
|
1151
|
+
const entityRef = publicEntityRef('execution', execution.execution_id);
|
|
1152
|
+
items.push({
|
|
1153
|
+
id: timelineId('execution', execution.execution_id),
|
|
1154
|
+
kind: 'RUNTIME_OBSERVATION',
|
|
1155
|
+
occurredAt: safeDate(execution.ended_at ?? execution.started_at),
|
|
1156
|
+
entityRef,
|
|
1157
|
+
authoritativeState: stateValue(execution.state, 'RUNTIME_OBSERVATION', refs),
|
|
1158
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1159
|
+
allowedActions: null,
|
|
1160
|
+
attentionReason: timelineAttention(attentions, publicEntityRef('task', task.task_id)),
|
|
1161
|
+
terminality: executionTerminality(execution.state),
|
|
1162
|
+
summary: `Runtime Observation: ${boundedText(execution.state)} (${boundedText(execution.execution_contract)})`,
|
|
1163
|
+
requiresOwnerAction: false,
|
|
1164
|
+
rawEvidenceAvailable: false,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (store.isSchemaV4) {
|
|
1169
|
+
for (const decision of store.listCapabilityDecisions(kingdomId)) {
|
|
1170
|
+
const refs = [tableSource('capability_decisions', decision.decision_id)];
|
|
1171
|
+
items.push({
|
|
1172
|
+
id: timelineId('decision', decision.decision_id),
|
|
1173
|
+
kind: 'GOVERNANCE_FACT',
|
|
1174
|
+
occurredAt: safeDate(decision.created_at),
|
|
1175
|
+
entityRef: publicEntityRef('task', decision.task_id),
|
|
1176
|
+
authoritativeState: stateValue(`${decision.decision}/${decision.enforcement_status}`, 'GOVERNANCE_FACT', refs),
|
|
1177
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1178
|
+
allowedActions: null,
|
|
1179
|
+
attentionReason: timelineAttention(attentions, publicEntityRef('task', decision.task_id)),
|
|
1180
|
+
terminality: 'UNKNOWN',
|
|
1181
|
+
summary: `Governance Fact: Decision ${boundedText(decision.decision)}`,
|
|
1182
|
+
requiresOwnerAction: decision.decision === 'DENIED',
|
|
1183
|
+
rawEvidenceAvailable: decision.enforcement_evidence_json !== null,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
for (const lease of store.listLeases(kingdomId)) {
|
|
1187
|
+
const refs = [tableSource('execution_leases', lease.lease_id)];
|
|
1188
|
+
items.push({
|
|
1189
|
+
id: timelineId('lease', lease.lease_id),
|
|
1190
|
+
kind: 'GOVERNANCE_FACT',
|
|
1191
|
+
occurredAt: safeDate(lease.updated_at ?? lease.acquired_at),
|
|
1192
|
+
entityRef: publicEntityRef('task', lease.task_id),
|
|
1193
|
+
authoritativeState: stateValue(lease.state, 'GOVERNANCE_FACT', refs),
|
|
1194
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1195
|
+
allowedActions: null,
|
|
1196
|
+
attentionReason: timelineAttention(attentions, publicEntityRef('task', lease.task_id)),
|
|
1197
|
+
terminality: lease.state === 'RELEASED' ? 'TERMINAL' : lease.state === 'RECOVERING' ? 'UNKNOWN' : 'NON_TERMINAL',
|
|
1198
|
+
summary: `Governance Fact: Lease ${boundedText(lease.state)}`,
|
|
1199
|
+
requiresOwnerAction: lease.state === 'RECOVERING',
|
|
1200
|
+
rawEvidenceAvailable: lease.release_evidence_json !== null,
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
for (const dispatch of store.listDispatches(kingdomId)) {
|
|
1204
|
+
const refs = [tableSource('dispatch_records', dispatch.dispatch_id)];
|
|
1205
|
+
items.push({
|
|
1206
|
+
id: timelineId('dispatch', dispatch.dispatch_id),
|
|
1207
|
+
kind: 'GOVERNANCE_FACT',
|
|
1208
|
+
occurredAt: safeDate(dispatch.updated_at ?? dispatch.created_at),
|
|
1209
|
+
entityRef: publicEntityRef('task', dispatch.task_id),
|
|
1210
|
+
authoritativeState: stateValue(dispatch.state, 'GOVERNANCE_FACT', refs),
|
|
1211
|
+
sourceRefs: boundedSourceRefs(refs),
|
|
1212
|
+
allowedActions: null,
|
|
1213
|
+
attentionReason: timelineAttention(attentions, publicEntityRef('task', dispatch.task_id)),
|
|
1214
|
+
terminality: dispatch.state === 'TERMINAL' || dispatch.state === 'FAILED'
|
|
1215
|
+
? 'TERMINAL' : dispatch.state === 'RECOVERING' ? 'UNKNOWN' : 'NON_TERMINAL',
|
|
1216
|
+
summary: `Governance Fact: Dispatch ${boundedText(dispatch.state)}`,
|
|
1217
|
+
requiresOwnerAction: dispatch.state === 'RECOVERING' || !dispatch.terminal_evidence_json,
|
|
1218
|
+
rawEvidenceAvailable: dispatch.receipt_json !== null || dispatch.terminal_evidence_json !== null,
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
for (const item of attentions) {
|
|
1223
|
+
items.push({
|
|
1224
|
+
id: `attention:${item.id}`,
|
|
1225
|
+
kind: 'DERIVED_EXPLANATION',
|
|
1226
|
+
occurredAt: null,
|
|
1227
|
+
entityRef: item.entityRef,
|
|
1228
|
+
authoritativeState: null,
|
|
1229
|
+
sourceRefs: item.sourceRefs,
|
|
1230
|
+
allowedActions: null,
|
|
1231
|
+
attentionReason: item.reason,
|
|
1232
|
+
terminality: 'UNKNOWN',
|
|
1233
|
+
summary: `Derived Explanation: ${boundedText(item.reason.code)} — ${item.summary}`,
|
|
1234
|
+
requiresOwnerAction: item.severity !== 'UNKNOWN',
|
|
1235
|
+
rawEvidenceAvailable: false,
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
return retainTimelineEvidence(items);
|
|
1239
|
+
}
|
|
1240
|
+
function ownerActionAvailability() {
|
|
1241
|
+
return OWNER_ONLY_ACTIONS.map(action => {
|
|
1242
|
+
const refs = boundedSourceRefs([ruleSource('OWNER_DIRECT_SLASH_ONLY')]);
|
|
1243
|
+
return {
|
|
1244
|
+
action,
|
|
1245
|
+
lifecycleAllowed: true,
|
|
1246
|
+
executable: false,
|
|
1247
|
+
disabledReason: attentionReason('DIRECT_SLASH_REQUIRED', refs),
|
|
1248
|
+
sourceRefs: refs,
|
|
1249
|
+
};
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
function taskAttention(attentions, taskId) {
|
|
1253
|
+
return attentions.find(item => item.entityRef?.type === 'task' && item.entityRef.id === publicReferenceId(taskId))?.reason ?? null;
|
|
1254
|
+
}
|
|
1255
|
+
/** Task Detail 的只读派生 Envelope;保留旧 TaskDetailView 字段,不复刻状态机。 */
|
|
1256
|
+
export function buildTaskProjection(store, kingdomId, task, options = {}) {
|
|
1257
|
+
const refreshedAt = new Date(options.nowMs ?? Date.now()).toISOString();
|
|
1258
|
+
const taskRef = publicEntityRef('task', task.task_id) ?? { type: 'task', id: 'unknown' };
|
|
1259
|
+
const statusRefs = [tableSource('tasks', task.task_id)];
|
|
1260
|
+
const claim = store.latestWorkerResult(task.task_id);
|
|
1261
|
+
const execution = store.latestExecution(task.task_id);
|
|
1262
|
+
const actions = buildActionAvailability(store, task, execution, options.security);
|
|
1263
|
+
const attentions = buildAttention(store, kingdomId);
|
|
1264
|
+
const refs = [...statusRefs];
|
|
1265
|
+
if (claim)
|
|
1266
|
+
refs.push(tableSource('worker_results', claim.result_id));
|
|
1267
|
+
if (execution)
|
|
1268
|
+
refs.push(tableSource('executions', execution.execution_id));
|
|
1269
|
+
const taskAttentionReason = taskAttention(attentions, task.task_id);
|
|
1270
|
+
return projectionEnvelope({
|
|
1271
|
+
revision: store.revision(kingdomId),
|
|
1272
|
+
refreshedAt,
|
|
1273
|
+
entityRef: taskRef,
|
|
1274
|
+
authoritativeState: stateValue(task.status, 'GOVERNANCE_FACT', statusRefs),
|
|
1275
|
+
sourceRefs: refs,
|
|
1276
|
+
allowedActions: actions,
|
|
1277
|
+
attentionReason: taskAttentionReason,
|
|
1278
|
+
data: {
|
|
1279
|
+
taskRef,
|
|
1280
|
+
status: stateValue(task.status, 'GOVERNANCE_FACT', statusRefs),
|
|
1281
|
+
claim: claim ? { outcome: boundedText(claim.outcome) || 'UNKNOWN', sourceRefs: boundedSourceRefs([tableSource('worker_results', claim.result_id)]) } : null,
|
|
1282
|
+
execution: execution ? {
|
|
1283
|
+
state: boundedText(execution.state) || 'UNKNOWN',
|
|
1284
|
+
executionContract: boundedText(execution.execution_contract) || 'UNKNOWN',
|
|
1285
|
+
terminality: executionTerminality(execution.state),
|
|
1286
|
+
sourceRefs: boundedSourceRefs([tableSource('executions', execution.execution_id)]),
|
|
1287
|
+
} : null,
|
|
1288
|
+
actionAvailability: actions,
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function buildOrganizationProjectionData(store, kingdomId, tasks) {
|
|
1293
|
+
const kingdom = store.getDefaultKingdom();
|
|
1294
|
+
if (!kingdom || kingdom.kingdom_id !== kingdomId) {
|
|
1295
|
+
return {
|
|
1296
|
+
kingdomName: null,
|
|
1297
|
+
bindingCount: 0,
|
|
1298
|
+
territoryCount: 0,
|
|
1299
|
+
roles: [],
|
|
1300
|
+
territories: [],
|
|
1301
|
+
rolesTruncated: false,
|
|
1302
|
+
territoriesTruncated: false,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
// Organization is a live role projection, not a historical binding ledger.
|
|
1306
|
+
// Retired bindings may remain referenced by old Tasks/Territories, but they
|
|
1307
|
+
// must not acquire a territoryRef or enter the console organogram.
|
|
1308
|
+
const bindings = store.listBindings(kingdomId).filter(binding => binding.status === 'ACTIVE');
|
|
1309
|
+
const territoryRows = store.listTerritories(kingdomId);
|
|
1310
|
+
const territoryRefByBinding = new Map();
|
|
1311
|
+
const registerTerritory = (bindingId, territoryId) => {
|
|
1312
|
+
const territoryRef = publicEntityRef('territory', territoryId);
|
|
1313
|
+
if (!territoryRef)
|
|
1314
|
+
return;
|
|
1315
|
+
if (!territoryRefByBinding.has(bindingId)) {
|
|
1316
|
+
territoryRefByBinding.set(bindingId, territoryRef);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const previous = territoryRefByBinding.get(bindingId);
|
|
1320
|
+
if (previous === undefined) {
|
|
1321
|
+
territoryRefByBinding.set(bindingId, territoryRef);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (previous === null || previous.id !== territoryRef.id)
|
|
1325
|
+
territoryRefByBinding.set(bindingId, null);
|
|
1326
|
+
};
|
|
1327
|
+
for (const territory of territoryRows) {
|
|
1328
|
+
if (territory.supervisor_binding_id)
|
|
1329
|
+
registerTerritory(territory.supervisor_binding_id, territory.territory_id);
|
|
1330
|
+
}
|
|
1331
|
+
for (const task of tasks) {
|
|
1332
|
+
if (task.assigned_binding_id)
|
|
1333
|
+
registerTerritory(task.assigned_binding_id, task.territory_id);
|
|
1334
|
+
}
|
|
1335
|
+
for (const binding of bindings) {
|
|
1336
|
+
if (binding.role_type !== 'WORKER')
|
|
1337
|
+
continue;
|
|
1338
|
+
const affinity = store.getCurrentAffinityForWorker(kingdomId, binding.binding_id);
|
|
1339
|
+
if (affinity)
|
|
1340
|
+
registerTerritory(binding.binding_id, affinity.territory_id);
|
|
1341
|
+
}
|
|
1342
|
+
const orderedBindings = [...bindings].sort((left, right) => {
|
|
1343
|
+
const statusOrder = Number(right.status === 'ACTIVE') - Number(left.status === 'ACTIVE');
|
|
1344
|
+
return statusOrder
|
|
1345
|
+
|| left.role_type.localeCompare(right.role_type)
|
|
1346
|
+
|| left.role_name.localeCompare(right.role_name)
|
|
1347
|
+
|| left.binding_id.localeCompare(right.binding_id);
|
|
1348
|
+
});
|
|
1349
|
+
const roles = [];
|
|
1350
|
+
for (const binding of orderedBindings) {
|
|
1351
|
+
if (roles.length >= MAX_PROJECTION_ORGANIZATION_ITEMS)
|
|
1352
|
+
break;
|
|
1353
|
+
const bindingRef = publicEntityRef('binding', binding.binding_id);
|
|
1354
|
+
if (!bindingRef)
|
|
1355
|
+
continue;
|
|
1356
|
+
const refs = boundedSourceRefs([tableSource('role_bindings', binding.binding_id)]);
|
|
1357
|
+
roles.push({
|
|
1358
|
+
bindingRef,
|
|
1359
|
+
roleType: boundedText(binding.role_type) || 'UNKNOWN',
|
|
1360
|
+
roleName: boundedText(binding.role_name) || 'UNKNOWN',
|
|
1361
|
+
territoryRef: territoryRefByBinding.get(binding.binding_id) ?? null,
|
|
1362
|
+
status: stateValue(binding.status, 'GOVERNANCE_FACT', refs),
|
|
1363
|
+
sessionBound: binding.session_id !== null,
|
|
1364
|
+
sourceRefs: refs,
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
const taskCounts = new Map();
|
|
1368
|
+
for (const task of tasks)
|
|
1369
|
+
taskCounts.set(task.territory_id, (taskCounts.get(task.territory_id) ?? 0) + 1);
|
|
1370
|
+
const territories = [];
|
|
1371
|
+
for (const territory of territoryRows) {
|
|
1372
|
+
if (territories.length >= MAX_PROJECTION_ORGANIZATION_ITEMS)
|
|
1373
|
+
break;
|
|
1374
|
+
const territoryRef = publicEntityRef('territory', territory.territory_id);
|
|
1375
|
+
if (!territoryRef)
|
|
1376
|
+
continue;
|
|
1377
|
+
const refs = boundedSourceRefs([
|
|
1378
|
+
tableSource('territories', territory.territory_id),
|
|
1379
|
+
...(territory.supervisor_binding_id
|
|
1380
|
+
? [tableSource('role_bindings', territory.supervisor_binding_id)]
|
|
1381
|
+
: [ruleSource('TERRITORY_SUPERVISOR_MISSING')]),
|
|
1382
|
+
]);
|
|
1383
|
+
territories.push({
|
|
1384
|
+
territoryRef,
|
|
1385
|
+
name: boundedText(territory.name) || 'UNKNOWN',
|
|
1386
|
+
status: stateValue(territory.status, 'GOVERNANCE_FACT', refs),
|
|
1387
|
+
supervisorBindingRef: publicEntityRef('binding', territory.supervisor_binding_id),
|
|
1388
|
+
taskCount: taskCounts.get(territory.territory_id) ?? 0,
|
|
1389
|
+
sourceRefs: refs,
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
kingdomName: boundedText(kingdom.name) || 'UNKNOWN',
|
|
1394
|
+
bindingCount: bindings.length,
|
|
1395
|
+
territoryCount: territoryRows.length,
|
|
1396
|
+
roles,
|
|
1397
|
+
territories,
|
|
1398
|
+
rolesTruncated: bindings.length > roles.length,
|
|
1399
|
+
territoriesTruncated: territoryRows.length > territories.length,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function executionEvidenceTime(execution) {
|
|
1403
|
+
const timestamp = Date.parse(execution.ended_at ?? execution.heartbeat_at ?? execution.started_at);
|
|
1404
|
+
return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
|
|
1405
|
+
}
|
|
1406
|
+
function buildExecutionProjectionData(store, tasks, attentions, security) {
|
|
1407
|
+
const candidates = [];
|
|
1408
|
+
for (const task of tasks) {
|
|
1409
|
+
const executions = store.listExecutions(task.task_id);
|
|
1410
|
+
for (let index = 0; index < executions.length; index += 1) {
|
|
1411
|
+
candidates.push({ task, execution: executions[index], latest: index === executions.length - 1 });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
candidates.sort((left, right) => executionEvidenceTime(right.execution) - executionEvidenceTime(left.execution)
|
|
1415
|
+
|| right.execution.attempt_no - left.execution.attempt_no
|
|
1416
|
+
|| left.execution.execution_id.localeCompare(right.execution.execution_id));
|
|
1417
|
+
const items = [];
|
|
1418
|
+
for (const candidate of candidates) {
|
|
1419
|
+
if (items.length >= MAX_PROJECTION_EXECUTION_SUMMARIES)
|
|
1420
|
+
break;
|
|
1421
|
+
const { task, execution, latest } = candidate;
|
|
1422
|
+
const executionRef = publicEntityRef('execution', execution.execution_id);
|
|
1423
|
+
const taskRef = publicEntityRef('task', task.task_id);
|
|
1424
|
+
if (!executionRef || !taskRef)
|
|
1425
|
+
continue;
|
|
1426
|
+
const refs = boundedSourceRefs([
|
|
1427
|
+
tableSource('executions', execution.execution_id),
|
|
1428
|
+
tableSource('tasks', task.task_id),
|
|
1429
|
+
...(execution.worker_binding_id ? [tableSource('role_bindings', execution.worker_binding_id)] : []),
|
|
1430
|
+
...(execution.lease_id ? [tableSource('execution_leases', execution.lease_id)] : []),
|
|
1431
|
+
...(execution.capability_decision_id
|
|
1432
|
+
? [tableSource('capability_decisions', execution.capability_decision_id)]
|
|
1433
|
+
: []),
|
|
1434
|
+
]);
|
|
1435
|
+
const actionAvailability = latest
|
|
1436
|
+
? buildActionAvailability(store, task, execution, security)
|
|
1437
|
+
.filter(action => action.action.startsWith('execution:'))
|
|
1438
|
+
: [];
|
|
1439
|
+
const state = boundedText(execution.state) || 'UNKNOWN';
|
|
1440
|
+
items.push({
|
|
1441
|
+
executionId: executionRef.id,
|
|
1442
|
+
taskId: taskRef.id,
|
|
1443
|
+
executionRef,
|
|
1444
|
+
taskRef,
|
|
1445
|
+
workerBindingRef: publicEntityRef('binding', execution.worker_binding_id),
|
|
1446
|
+
attemptNo: Number.isSafeInteger(execution.attempt_no) && execution.attempt_no >= 0
|
|
1447
|
+
? execution.attempt_no
|
|
1448
|
+
: 0,
|
|
1449
|
+
state,
|
|
1450
|
+
authoritativeState: stateValue(state, 'RUNTIME_OBSERVATION', [tableSource('executions', execution.execution_id)]),
|
|
1451
|
+
executionContract: boundedText(execution.execution_contract) || 'UNKNOWN',
|
|
1452
|
+
terminality: executionTerminality(execution.state),
|
|
1453
|
+
pausePending: execution.pause_requested_at !== null && execution.state === 'RUNNING',
|
|
1454
|
+
startedAt: safeDate(execution.started_at),
|
|
1455
|
+
endedAt: safeDate(execution.ended_at),
|
|
1456
|
+
actionAvailability,
|
|
1457
|
+
attentionReason: taskAttention(attentions, task.task_id),
|
|
1458
|
+
sourceRefs: refs,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return {
|
|
1462
|
+
totalExecutionCount: candidates.length,
|
|
1463
|
+
items,
|
|
1464
|
+
truncated: candidates.length > items.length,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
/** Keep runtime references and prompt text outside the public cost projection. */
|
|
1468
|
+
function buildWorkbenchCost(store, kingdomId, present, runtime) {
|
|
1469
|
+
const number = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : null;
|
|
1470
|
+
const rows = present ? readLatestPromptCosts(store, kingdomId) : [];
|
|
1471
|
+
const parts = (value) => Array.isArray(value)
|
|
1472
|
+
? value.slice(0, 24).map(item => ({ name: boundedText(typeof item?.name === 'string' ? item.name : 'UNKNOWN'), bytes: number(item?.bytes) })) : [];
|
|
1473
|
+
return {
|
|
1474
|
+
additionalRoles: present ? readAdditionalRoleCost(store, kingdomId) : null,
|
|
1475
|
+
budget: present ? readBudgetView(store, kingdomId) : null,
|
|
1476
|
+
prompts: { totalCount: rows.length, truncated: rows.length > 40, items: rows.slice(0, 40).map(row => ({
|
|
1477
|
+
bindingId: publicReferenceId(row.bindingId), roleType: boundedText(row.roleType),
|
|
1478
|
+
toolsBefore: number(row.toolsBefore), toolsAfter: number(row.toolsAfter), toolsBytesBefore: number(row.toolsBytesBefore), toolsBytesAfter: number(row.toolsBytesAfter),
|
|
1479
|
+
sections: parts(row.sections), contexts: parts(row.contexts),
|
|
1480
|
+
partsTruncated: (Array.isArray(row.sections) && row.sections.length > 24) || (Array.isArray(row.contexts) && row.contexts.length > 24),
|
|
1481
|
+
historyBytes: null, tokenEstimate: null, mode: row.mode === 'off' || row.mode === 'pilot' ? row.mode : 'UNKNOWN',
|
|
1482
|
+
reasonCode: typeof row.reasonCode === 'string' ? boundedText(row.reasonCode) : null,
|
|
1483
|
+
})) },
|
|
1484
|
+
runtime: { toolDisclosureMode: runtime?.toolDisclosureMode === 'off' || runtime?.toolDisclosureMode === 'pilot' ? runtime.toolDisclosureMode : 'UNKNOWN',
|
|
1485
|
+
observerAvailable: typeof runtime?.observerAvailable === 'boolean' ? runtime.observerAvailable : null },
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
/** Apply the bound after exact task selection, so unrelated activity cannot erase task history. */
|
|
1489
|
+
function readTaskEvents(store, kingdomId, taskId, limit) {
|
|
1490
|
+
return store.db.prepare('SELECT * FROM events WHERE kingdom_id = ? AND target_type = ? AND target_id = ? ORDER BY seq DESC LIMIT ?')
|
|
1491
|
+
.all(kingdomId, 'task', taskId, limit);
|
|
1492
|
+
}
|
|
1493
|
+
function readTaskReviewEvents(store, kingdomId, taskId, limit) {
|
|
1494
|
+
return store.db.prepare("SELECT * FROM events WHERE kingdom_id = ? AND target_type = 'task' AND target_id = ? AND actor_role = 'SUPERVISOR' AND event_type IN ('TASK_ACCEPTED', 'TASK_REWORK_REQUESTED', 'TASK_FAILED', 'TASK_HANDED_OFF') ORDER BY seq DESC LIMIT ?")
|
|
1495
|
+
.all(kingdomId, taskId, limit);
|
|
1496
|
+
}
|
|
1497
|
+
/** Canonical reads remain here; the workbench builder only receives public projection values. */
|
|
1498
|
+
function buildWorkbenchCollaboration(store, kingdomId, present) {
|
|
1499
|
+
const publicId = (id) => publicReferenceId(id) ?? 'redacted';
|
|
1500
|
+
const plans = present ? listCollaborationPlans(store, kingdomId) : [];
|
|
1501
|
+
const resources = present && store.isSchemaV4 ? readWorkspaceReservations(store, kingdomId) : [];
|
|
1502
|
+
const readiness = (value) => value ? {
|
|
1503
|
+
ready: value.ready, reasonCode: value.reasonCode, blockingTaskIds: value.blockingTaskIds.map(publicId),
|
|
1504
|
+
acceptedResults: value.handoffs.map(item => ({ taskId: publicId(item.taskId), attemptNo: item.attemptNo,
|
|
1505
|
+
resultId: publicId(item.resultId), resultDigest: item.resultDigest, acceptEventId: publicId(item.acceptEventId) })),
|
|
1506
|
+
} : null;
|
|
1507
|
+
return {
|
|
1508
|
+
pendingAdoptionCount: plans.filter(plan => plan.state === 'PROPOSED').length,
|
|
1509
|
+
plans: { totalCount: plans.length, truncated: plans.length > 40, items: plans.slice(0, 40).map(plan => ({
|
|
1510
|
+
planId: publicId(plan.planId), parentTaskId: publicId(plan.parentTaskId), title: boundedText(store.getTask(plan.parentTaskId)?.title),
|
|
1511
|
+
version: plan.version, digest: plan.digest, state: plan.state, mode: plan.mode, reason: boundedText(plan.reason),
|
|
1512
|
+
integratorBindingId: publicId(plan.integratorBindingId), integratorName: boundedText(store.getBindingById(plan.integratorBindingId)?.role_name),
|
|
1513
|
+
parentStatus: store.getTask(plan.parentTaskId)?.status ?? 'UNKNOWN', budgetTokens: plan.budgetTokens, reserveTokens: plan.reserveTokens,
|
|
1514
|
+
adoptedAt: plan.adoptedAt, integration: readiness(readCollaborationReadiness(store, kingdomId, plan.parentTaskId)),
|
|
1515
|
+
budget: readPlanBudgetView(store, kingdomId, plan.planId),
|
|
1516
|
+
items: plan.items.map(item => ({ key: boundedText(item.key), taskId: publicId(item.taskId), title: boundedText(item.title),
|
|
1517
|
+
description: boundedText(item.description), acceptanceCriteria: boundedText(item.acceptanceCriteria),
|
|
1518
|
+
territoryId: publicId(item.territoryId), territoryName: boundedText(store.getTerritoryById(item.territoryId)?.name),
|
|
1519
|
+
workerBindingId: publicId(item.workerBindingId), workerName: boundedText(store.getBindingById(item.workerBindingId)?.role_name),
|
|
1520
|
+
access: item.access, dependsOn: item.dependsOn.map(boundedText), expectedArtifact: boundedText(item.expectedArtifact),
|
|
1521
|
+
status: store.getTask(item.taskId)?.status ?? 'NOT_CREATED', readiness: readiness(readCollaborationReadiness(store, kingdomId, item.taskId)) })),
|
|
1522
|
+
})) },
|
|
1523
|
+
resources: { totalCount: resources.length, truncated: resources.length > 80, items: resources.slice(0, 80).map(item => ({
|
|
1524
|
+
taskId: publicId(item.taskId), attemptNo: item.attemptNo, access: item.access, state: boundedText(item.state), recovery: item.recovery,
|
|
1525
|
+
})) },
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function buildWorkbenchData(store, kingdomId, tasks, security, costRuntime) {
|
|
1529
|
+
const kingdom = store.getDefaultKingdom();
|
|
1530
|
+
const present = kingdom?.kingdom_id === kingdomId;
|
|
1531
|
+
const bindings = present ? store.listBindings(kingdomId) : [];
|
|
1532
|
+
const territories = present ? store.listTerritories(kingdomId) : [];
|
|
1533
|
+
const publicId = (id) => publicReferenceId(id) ?? 'redacted';
|
|
1534
|
+
const taskInputs = tasks.map(task => {
|
|
1535
|
+
const view = toTaskView(store, task);
|
|
1536
|
+
const claim = store.latestWorkerResult(task.task_id);
|
|
1537
|
+
const execution = store.latestExecution(task.task_id);
|
|
1538
|
+
const review = readTaskReviewEvents(store, kingdomId, task.task_id, 1)[0];
|
|
1539
|
+
return {
|
|
1540
|
+
task: {
|
|
1541
|
+
taskId: publicId(task.task_id), territoryId: publicId(task.territory_id), title: boundedText(task.title),
|
|
1542
|
+
status: boundedText(task.status), assignedBindingId: publicReferenceId(task.assigned_binding_id), updatedAt: safeDate(task.updated_at) ?? '',
|
|
1543
|
+
latestClaim: view.latestClaim ? { ...view.latestClaim, resultId: publicId(view.latestClaim.resultId),
|
|
1544
|
+
workerBindingId: publicReferenceId(view.latestClaim.workerBindingId), sessionId: null } : null,
|
|
1545
|
+
latestExecution: view.latestExecution ? { ...view.latestExecution, executionId: publicId(view.latestExecution.executionId),
|
|
1546
|
+
taskId: publicId(task.task_id), workerBindingId: publicReferenceId(view.latestExecution.workerBindingId), sessionId: null,
|
|
1547
|
+
leaseId: publicReferenceId(view.latestExecution.leaseId), capabilityDecisionId: publicReferenceId(view.latestExecution.capabilityDecisionId) } : null,
|
|
1548
|
+
},
|
|
1549
|
+
actionAvailability: buildActionAvailability(store, task, execution, security),
|
|
1550
|
+
latestReview: review ? toSupervisorDecisionView(review) : null,
|
|
1551
|
+
claimExecutionMismatch: Boolean(claim && execution && claimExecutionMismatch(claim, execution)),
|
|
1552
|
+
};
|
|
1553
|
+
});
|
|
1554
|
+
const governance = present ? buildGovernance(store, kingdomId) : emptyGovernance();
|
|
1555
|
+
// Only public IDs, reason codes and typed token observations can be consumed by the workbench.
|
|
1556
|
+
governance.decisions = governance.decisions.map(decision => ({ ...decision, decisionId: publicId(decision.decisionId), taskId: publicId(decision.taskId), reasonCode: boundedText(decision.reasonCode) || null }));
|
|
1557
|
+
governance.dispatches = governance.dispatches.map(dispatch => ({ ...dispatch, dispatchId: publicId(dispatch.dispatchId), taskId: publicId(dispatch.taskId),
|
|
1558
|
+
executionId: publicId(dispatch.executionId), leaseId: publicId(dispatch.leaseId), runtimeDispatchRef: null, runtimeExecutionRef: null }));
|
|
1559
|
+
governance.leases = governance.leases.map(lease => ({ ...lease, leaseId: publicId(lease.leaseId), taskId: publicId(lease.taskId) }));
|
|
1560
|
+
return buildPersonalWorkbench({
|
|
1561
|
+
kingdomPresent: present,
|
|
1562
|
+
bindings: bindings.map(binding => ({ bindingId: publicId(binding.binding_id), roleType: boundedText(binding.role_type), roleName: boundedText(binding.role_name),
|
|
1563
|
+
status: boundedText(binding.status), sessionBound: binding.session_id !== null })),
|
|
1564
|
+
territories: territories.map(territory => ({ territoryId: publicId(territory.territory_id), name: boundedText(territory.name), status: boundedText(territory.status),
|
|
1565
|
+
supervisorBindingId: publicReferenceId(territory.supervisor_binding_id) })),
|
|
1566
|
+
tasks: taskInputs,
|
|
1567
|
+
executions: tasks.flatMap(task => store.listExecutions(task.task_id).map(execution => ({ executionId: publicId(execution.execution_id), taskId: publicId(task.task_id),
|
|
1568
|
+
workerBindingId: publicReferenceId(execution.worker_binding_id), state: boundedText(execution.state) }))),
|
|
1569
|
+
governance,
|
|
1570
|
+
cost: buildWorkbenchCost(store, kingdomId, present, costRuntime),
|
|
1571
|
+
collaboration: buildWorkbenchCollaboration(store, kingdomId, present),
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
/** S1 顶层只读 Projection;没有 kingdom 时显式返回 UNKNOWN/空数据。 */
|
|
1575
|
+
export function buildReadonlySnapshotProjection(store, kingdomId, options = {}) {
|
|
1576
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
1577
|
+
const refreshedAt = new Date(nowMs).toISOString();
|
|
1578
|
+
const kingdom = store.getDefaultKingdom();
|
|
1579
|
+
const actualKingdomId = kingdom?.kingdom_id ?? kingdomId ?? '';
|
|
1580
|
+
const revision = actualKingdomId ? store.revision(actualKingdomId) : 0;
|
|
1581
|
+
const kingdomRef = publicEntityRef('kingdom', actualKingdomId);
|
|
1582
|
+
const sourceRefs = kingdomRef ? [tableSource('kingdoms', actualKingdomId)] : [ruleSource('KINGDOM_PRESENT')];
|
|
1583
|
+
const attentions = actualKingdomId
|
|
1584
|
+
? buildAttention(store, actualKingdomId)
|
|
1585
|
+
: [attentionItem('kingdom:missing', 'UNKNOWN', null, 'CONFIGURATION_INCOMPLETE', 'Kingdom is not initialized', [ruleSource('KINGDOM_PRESENT')])];
|
|
1586
|
+
const timeline = actualKingdomId
|
|
1587
|
+
? buildTimeline(store, actualKingdomId, options)
|
|
1588
|
+
: [];
|
|
1589
|
+
const tasks = actualKingdomId ? store.listTasks(actualKingdomId) : [];
|
|
1590
|
+
const activeExecutionCount = actualKingdomId ? store.listLiveExecutions(actualKingdomId).length : 0;
|
|
1591
|
+
const statusCounts = {};
|
|
1592
|
+
for (const task of tasks)
|
|
1593
|
+
statusCounts[task.status] = (statusCounts[task.status] ?? 0) + 1;
|
|
1594
|
+
const organizationData = buildOrganizationProjectionData(store, actualKingdomId, tasks);
|
|
1595
|
+
const executionData = buildExecutionProjectionData(store, tasks, attentions, options.security);
|
|
1596
|
+
const health = attentions.some(item => item.severity === 'CRITICAL')
|
|
1597
|
+
? 'CRITICAL'
|
|
1598
|
+
: attentions.some(item => item.severity === 'ATTENTION')
|
|
1599
|
+
? 'ATTENTION'
|
|
1600
|
+
: kingdom ? 'OK' : 'UNKNOWN';
|
|
1601
|
+
const topAttention = attentions[0]?.reason ?? null;
|
|
1602
|
+
const authoritative = kingdom
|
|
1603
|
+
? stateValue('PRESENT', 'GOVERNANCE_FACT', sourceRefs)
|
|
1604
|
+
: null;
|
|
1605
|
+
return {
|
|
1606
|
+
overview: projectionEnvelope({
|
|
1607
|
+
revision,
|
|
1608
|
+
refreshedAt,
|
|
1609
|
+
entityRef: kingdomRef,
|
|
1610
|
+
authoritativeState: authoritative,
|
|
1611
|
+
sourceRefs,
|
|
1612
|
+
attentionReason: topAttention,
|
|
1613
|
+
data: { health, taskCount: tasks.length, activeExecutionCount, statusCounts, ownerActions: ownerActionAvailability() },
|
|
1614
|
+
}),
|
|
1615
|
+
organization: projectionEnvelope({
|
|
1616
|
+
revision,
|
|
1617
|
+
refreshedAt,
|
|
1618
|
+
entityRef: kingdomRef,
|
|
1619
|
+
authoritativeState: authoritative,
|
|
1620
|
+
sourceRefs: [...sourceRefs, ruleSource('ORGANIZATION_SUMMARY_DERIVED')],
|
|
1621
|
+
attentionReason: topAttention,
|
|
1622
|
+
data: organizationData,
|
|
1623
|
+
}),
|
|
1624
|
+
executions: projectionEnvelope({
|
|
1625
|
+
revision,
|
|
1626
|
+
refreshedAt,
|
|
1627
|
+
entityRef: kingdomRef,
|
|
1628
|
+
authoritativeState: null,
|
|
1629
|
+
sourceRefs: [...sourceRefs, ruleSource('EXECUTION_SUMMARY_DERIVED')],
|
|
1630
|
+
attentionReason: topAttention,
|
|
1631
|
+
data: executionData,
|
|
1632
|
+
}),
|
|
1633
|
+
timeline: projectionEnvelope({
|
|
1634
|
+
revision,
|
|
1635
|
+
refreshedAt,
|
|
1636
|
+
entityRef: kingdomRef,
|
|
1637
|
+
authoritativeState: null,
|
|
1638
|
+
sourceRefs: [...sourceRefs, ruleSource('TIMELINE_RECONSTRUCTED')],
|
|
1639
|
+
data: timeline,
|
|
1640
|
+
}),
|
|
1641
|
+
attention: projectionEnvelope({
|
|
1642
|
+
revision,
|
|
1643
|
+
refreshedAt,
|
|
1644
|
+
entityRef: kingdomRef,
|
|
1645
|
+
authoritativeState: null,
|
|
1646
|
+
sourceRefs: [...sourceRefs, ruleSource('ATTENTION_DERIVED')],
|
|
1647
|
+
attentionReason: topAttention,
|
|
1648
|
+
data: attentions,
|
|
1649
|
+
}),
|
|
1650
|
+
workbench: projectionEnvelope({
|
|
1651
|
+
revision,
|
|
1652
|
+
refreshedAt,
|
|
1653
|
+
entityRef: kingdomRef,
|
|
1654
|
+
authoritativeState: null,
|
|
1655
|
+
sourceRefs: [...sourceRefs, ruleSource('PERSONAL_WORKBENCH_DERIVED')],
|
|
1656
|
+
data: buildWorkbenchData(store, actualKingdomId, tasks, options.security, options.costRuntime),
|
|
1657
|
+
}),
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
388
1660
|
export function buildSnapshot(store, options) {
|
|
389
1661
|
const kingdom = store.getDefaultKingdom();
|
|
390
1662
|
const nowMs = options.nowMs ?? Date.now();
|
|
@@ -403,10 +1675,17 @@ export function buildSnapshot(store, options) {
|
|
|
403
1675
|
stage: [],
|
|
404
1676
|
recentEvents: [],
|
|
405
1677
|
governance: emptyGovernance(),
|
|
1678
|
+
projection: buildReadonlySnapshotProjection(store, null, {
|
|
1679
|
+
eventLimit: options.eventLimit,
|
|
1680
|
+
nowMs,
|
|
1681
|
+
security: options.security,
|
|
1682
|
+
costRuntime: options.costRuntime,
|
|
1683
|
+
}),
|
|
406
1684
|
};
|
|
407
1685
|
}
|
|
408
1686
|
const kingdomId = kingdom.kingdom_id;
|
|
409
1687
|
const bindings = store.listBindings(kingdomId);
|
|
1688
|
+
const territories = store.listTerritories(kingdomId);
|
|
410
1689
|
const tasks = store.listTasks(kingdomId);
|
|
411
1690
|
const events = store.listEvents(kingdomId, options.eventLimit ?? 50);
|
|
412
1691
|
const liveExecutions = store.listLiveExecutions(kingdomId);
|
|
@@ -423,11 +1702,12 @@ export function buildSnapshot(store, options) {
|
|
|
423
1702
|
},
|
|
424
1703
|
auth: options.auth,
|
|
425
1704
|
bindings: bindings.map(toBindingView),
|
|
426
|
-
territories:
|
|
1705
|
+
territories: territories.map(toTerritoryView),
|
|
427
1706
|
tasks: tasks.map(t => toTaskView(store, t)),
|
|
428
1707
|
liveExecutions: liveExecutions.map(toExecutionView),
|
|
429
1708
|
stage: projectStage({
|
|
430
1709
|
bindings,
|
|
1710
|
+
territories,
|
|
431
1711
|
tasks,
|
|
432
1712
|
executions: liveExecutions,
|
|
433
1713
|
events,
|
|
@@ -436,9 +1716,15 @@ export function buildSnapshot(store, options) {
|
|
|
436
1716
|
}),
|
|
437
1717
|
recentEvents: events.map(toEventView),
|
|
438
1718
|
governance: buildGovernance(store, kingdomId),
|
|
1719
|
+
projection: buildReadonlySnapshotProjection(store, kingdomId, {
|
|
1720
|
+
eventLimit: options.eventLimit,
|
|
1721
|
+
nowMs,
|
|
1722
|
+
security: options.security,
|
|
1723
|
+
costRuntime: options.costRuntime,
|
|
1724
|
+
}),
|
|
439
1725
|
};
|
|
440
1726
|
}
|
|
441
|
-
export function buildTaskDetail(store, kingdomId, taskId) {
|
|
1727
|
+
export function buildTaskDetail(store, kingdomId, taskId, options = {}) {
|
|
442
1728
|
const task = store.getTask(taskId);
|
|
443
1729
|
if (!task)
|
|
444
1730
|
return null;
|
|
@@ -446,25 +1732,12 @@ export function buildTaskDetail(store, kingdomId, taskId) {
|
|
|
446
1732
|
if (!territory || territory.kingdom_id !== kingdomId)
|
|
447
1733
|
return null;
|
|
448
1734
|
const binding = task.assigned_binding_id ? store.getBindingById(task.assigned_binding_id) : null;
|
|
1735
|
+
const assignments = store.listTaskAssignments(taskId);
|
|
449
1736
|
const executions = store.listExecutions(taskId);
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const reviews = related
|
|
455
|
-
.filter(e => ['TASK_ACCEPTED', 'TASK_REWORK_REQUESTED', 'TASK_FAILED'].includes(e.event_type))
|
|
456
|
-
.map((e) => {
|
|
457
|
-
const payload = parseJson(e.payload_json);
|
|
458
|
-
return {
|
|
459
|
-
seq: e.seq,
|
|
460
|
-
decision: typeof payload.decision === 'string' ? payload.decision : e.event_type,
|
|
461
|
-
reason: typeof payload.reason === 'string' ? payload.reason : null,
|
|
462
|
-
reviewerBindingId: typeof payload.reviewer_binding_id === 'string' ? payload.reviewer_binding_id : null,
|
|
463
|
-
reviewedAttemptNo: typeof payload.reviewed_attempt_no === 'number' ? payload.reviewed_attempt_no : null,
|
|
464
|
-
claimedOutcome: typeof payload.claimed_outcome === 'string' ? payload.claimed_outcome : null,
|
|
465
|
-
createdAt: e.created_at,
|
|
466
|
-
};
|
|
467
|
-
});
|
|
1737
|
+
const taskEvents = readTaskEvents(store, kingdomId, taskId, 501);
|
|
1738
|
+
const related = taskEvents.slice(0, 500).sort((a, b) => a.seq - b.seq);
|
|
1739
|
+
const reviewEvents = readTaskReviewEvents(store, kingdomId, taskId, 501);
|
|
1740
|
+
const reviews = reviewEvents.slice(0, 500).sort((a, b) => a.seq - b.seq).map(toSupervisorDecisionView);
|
|
468
1741
|
const view = toTaskView(store, task);
|
|
469
1742
|
return {
|
|
470
1743
|
schemaVersion: GUI_SCHEMA_VERSION,
|
|
@@ -472,12 +1745,18 @@ export function buildTaskDetail(store, kingdomId, taskId) {
|
|
|
472
1745
|
task: view,
|
|
473
1746
|
territory: toTerritoryView(territory),
|
|
474
1747
|
assignedBinding: binding ? toBindingView(binding) : null,
|
|
1748
|
+
assignments: assignments.map(toTaskAssignmentHistoryView),
|
|
475
1749
|
claims: store.listWorkerResults(taskId).map(toClaimView),
|
|
476
1750
|
executions: executions.map(toExecutionView),
|
|
477
1751
|
reviews,
|
|
1752
|
+
reviewsTruncated: reviewEvents.length > reviews.length,
|
|
478
1753
|
relatedEvents: related.map(toEventView),
|
|
1754
|
+
relatedEventsTruncated: taskEvents.length > related.length,
|
|
1755
|
+
humanAcceptance: 'NOT_RECORDED',
|
|
479
1756
|
allowedActions: view.allowedActions,
|
|
480
1757
|
governance: buildTaskGovernance(store, taskId),
|
|
1758
|
+
additionalRoleCost: readAdditionalRoleCost(store, kingdomId, taskId),
|
|
1759
|
+
projection: buildTaskProjection(store, kingdomId, task, options),
|
|
481
1760
|
};
|
|
482
1761
|
}
|
|
483
1762
|
//# sourceMappingURL=snapshot.js.map
|