dsh-kingdom 2.0.0 → 3.1.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 -22
- package/README.md +244 -243
- package/THIRD_PARTY_NOTICES.md +92 -92
- package/docs/DSH-KINGDOM-3.0-ANNOUNCEMENT.md +13 -0
- package/docs/V2.0-RELEASE-NOTES.md +30 -30
- package/docs/V2.0-USER-GUIDE.md +74 -74
- package/docs/V3.0-RELEASE-NOTES.md +21 -0
- package/docs/V3.0-USER-GUIDE.md +33 -0
- package/docs/V3.1-RELEASE-NOTES.md +72 -0
- package/lib/core/binding.js +2 -0
- package/lib/core/binding.js.map +1 -1
- package/lib/core/db.js +546 -546
- package/lib/core/delivery-ack.js +1356 -0
- package/lib/core/delivery-ack.js.map +1 -0
- package/lib/core/delivery-change.js +1237 -0
- package/lib/core/delivery-change.js.map +1 -0
- package/lib/core/owner-window.js +592 -11
- package/lib/core/owner-window.js.map +1 -1
- package/lib/core/task-service.js +144 -5
- package/lib/core/task-service.js.map +1 -1
- package/lib/gui/assets/characters/chancellor-idle.svg +67 -67
- package/lib/gui/assets/characters/chancellor-sleeping.svg +67 -67
- package/lib/gui/assets/characters/chancellor-thinking.svg +67 -67
- package/lib/gui/assets/characters/chancellor-working.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-idle.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-sleeping.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-thinking.svg +67 -67
- package/lib/gui/assets/characters/knight-redraw-r1-working.svg +67 -67
- package/lib/gui/assets/characters/supervisor-idle.svg +67 -67
- package/lib/gui/assets/characters/supervisor-sleeping.svg +67 -67
- package/lib/gui/assets/characters/supervisor-thinking.svg +67 -67
- package/lib/gui/assets/characters/supervisor-working.svg +67 -67
- package/lib/gui/console-app.js +7 -1
- package/lib/gui/console-app.js.map +1 -1
- package/lib/gui/contract.js +6 -0
- package/lib/gui/contract.js.map +1 -1
- package/lib/gui/owner-control.js +114 -4
- package/lib/gui/owner-control.js.map +1 -1
- package/lib/gui/owner-ui.js +604 -183
- package/lib/gui/owner-ui.js.map +1 -1
- package/lib/gui/server.js +109 -109
- package/lib/gui/snapshot.js +127 -4
- package/lib/gui/snapshot.js.map +1 -1
- package/lib/gui/visual-assets.js +37 -21
- package/lib/gui/visual-assets.js.map +1 -1
- package/lib/gui/visual-system.js +269 -269
- package/lib/gui/workbench-ui.js +537 -136
- package/lib/gui/workbench-ui.js.map +1 -1
- package/lib/gui/workbench.js +123 -0
- package/lib/gui/workbench.js.map +1 -1
- package/lib/index.js +338 -26
- package/lib/index.js.map +1 -1
- package/lib/types/core/binding.d.ts +15 -0
- package/lib/types/core/delivery-ack.d.ts +688 -0
- package/lib/types/core/delivery-change.d.ts +381 -0
- package/lib/types/core/owner-window.d.ts +235 -2
- package/lib/types/core/task-service.d.ts +22 -0
- package/lib/types/gui/contract.d.ts +153 -1
- package/lib/types/gui/owner-control.d.ts +61 -1
- package/lib/types/gui/snapshot.d.ts +8 -0
- package/lib/types/gui/visual-assets.d.ts +16 -0
- package/lib/types/gui/workbench.d.ts +47 -1
- package/lib/types/index.d.ts +49 -0
- package/lib/types/worker/governed-executor.d.ts +15 -0
- package/lib/worker/governed-executor.js +21 -0
- package/lib/worker/governed-executor.js.map +1 -1
- package/package.json +79 -75
package/lib/core/owner-window.js
CHANGED
|
@@ -8,7 +8,9 @@ import { isOwnerControlCapability, issueOwnerOperationCapability, revokeOwnerOpe
|
|
|
8
8
|
import { initializeKingdomFacts } from './kingdom.js';
|
|
9
9
|
import { normalizeBudgetPolicyParameters, readBudgetPolicy, setBudgetPolicy } from './budget.js';
|
|
10
10
|
import { adoptCollaborationPlan, listCollaborationPlans, readCollaborationPlan, normalizePlanAdoptionParameters } from './collaboration.js';
|
|
11
|
-
|
|
11
|
+
import { DELIVERY_ACK_EVENT_TYPE, CHANGE_EVIDENCE_LABEL, DELIVERY_QUESTION_NOTE, DELIVERY_QUESTION_EVENT_TYPE, DELIVERY_QUESTION_TEXT_LIMIT, DELIVERY_REPLY_STATES, boundedQuestionText, classifyAcceptedDelivery, deliveryAcknowledgementView, deliveryChangeItemId, deliveryIdFor, deliveryQuestionEventId, deliveryQuestionThreadForItem, deliveryReviewerBindingId, deriveDeliveryItems, listDeliveryQuestionHistory, readDeliveryAcknowledgements, readDeliveryItemVersion, readDeliveryQuestionThread, readLatestReviewEvent, recordDeliveryAcknowledgement, recordDeliveryQuestion, redactDeliveryText, } from './delivery-ack.js';
|
|
12
|
+
import { readAcceptedChangeEvidence, readChangeEntry, resolveChangeEvidenceForDelivery, } from './delivery-change.js';
|
|
13
|
+
export const OWNER_MUTATION_ACTIONS = ['init', 'territory.create', 'territory.update', 'territory.supervisor', 'role.bind', 'role.session', 'ceiling', 'execution-profile', 'budget.policy', 'plan.adopt', 'delivery.item.ack', 'delivery.item.question'];
|
|
12
14
|
export class OwnerOperationError extends Error {
|
|
13
15
|
code;
|
|
14
16
|
constructor(code, message) {
|
|
@@ -19,6 +21,37 @@ export class OwnerOperationError extends Error {
|
|
|
19
21
|
}
|
|
20
22
|
const MANAGED_ROLES = ['CHANCELLOR', 'SUPERVISOR', 'WORKER'];
|
|
21
23
|
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
24
|
+
/** 条目提问元数据:只计数与时间,不含任何正文。 */
|
|
25
|
+
function ownerDeliveryItemQuestions(thread) {
|
|
26
|
+
const replied = thread.questions.filter(question => question.reply !== null);
|
|
27
|
+
const asked = thread.questions.map(question => question.askedAt).sort();
|
|
28
|
+
const answered = replied.map(question => question.reply.repliedAt).sort();
|
|
29
|
+
return {
|
|
30
|
+
threadId: thread.deliveryId + ':' + thread.itemId,
|
|
31
|
+
totalCount: thread.questions.length,
|
|
32
|
+
pendingCount: thread.pendingCount,
|
|
33
|
+
answeredCount: thread.answeredCount,
|
|
34
|
+
historyCount: thread.historyCount,
|
|
35
|
+
lastAskedAt: asked.length ? asked[asked.length - 1] : null,
|
|
36
|
+
lastRepliedAt: answered.length ? answered[answered.length - 1] : null,
|
|
37
|
+
latestReplyState: thread.questions.length ? thread.questions[thread.questions.length - 1].replyState : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** 固定的可达性说明;界面与报告共用,避免两处措辞漂移。 */
|
|
41
|
+
function classifyReplyStateNote(state) {
|
|
42
|
+
// `DELIVERY_REPLY_STATES` 是唯一状态清单:新增状态而忘记补说明时这里立刻失败,
|
|
43
|
+
// 而不是静默回退成一句含糊的通用文案。
|
|
44
|
+
if (!DELIVERY_REPLY_STATES.includes(state))
|
|
45
|
+
fail('DELIVERY_QUESTION_STATE_UNKNOWN', '未知的回复可达性状态。');
|
|
46
|
+
const notes = {
|
|
47
|
+
REPLY_ACCESSIBLE: '该问题仍由接受交付的主管负责:它仍是领地当前主理并持有 ACTIVE session,可读取并回复。',
|
|
48
|
+
REVIEWER_BINDING_MISSING: '接受该交付时记录的主管绑定已不存在或不再是主管,本条不可达;不会改投继任主管。',
|
|
49
|
+
REVIEWER_BINDING_RETIRED: '接受该交付的主管已退任,本条不可达;不会改投继任主管。',
|
|
50
|
+
REVIEWER_SESSION_CHANGED: '该主管当前没有可用的 ACTIVE session,本条暂时不可达。',
|
|
51
|
+
SUPERVISOR_REBOUND: '该领地已改由其他主管主理,本条仍归原接受主管,不可达;不会自动改投继任者。',
|
|
52
|
+
};
|
|
53
|
+
return notes[state];
|
|
54
|
+
}
|
|
22
55
|
function fail(code, message) { throw new OwnerOperationError(code, message); }
|
|
23
56
|
function object(value, allowed, required = allowed) {
|
|
24
57
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
@@ -129,6 +162,39 @@ function normalizeInput(raw) {
|
|
|
129
162
|
}
|
|
130
163
|
return { action: 'execution-profile', parameters: { binding_id: string(p.binding_id, '绑定 ID'), profile } };
|
|
131
164
|
}
|
|
165
|
+
case 'delivery.item.ack': {
|
|
166
|
+
const p = object(outer.parameters, ['task_id', 'delivery_id', 'item_id', 'content_hash', 'attempt_no', 'result_id']);
|
|
167
|
+
const attemptNo = p.attempt_no;
|
|
168
|
+
if (!Number.isSafeInteger(attemptNo) || attemptNo < 1 || attemptNo > 1_000_000) {
|
|
169
|
+
fail('INVALID_INPUT', '交付尝试次数必须是正整数。');
|
|
170
|
+
}
|
|
171
|
+
const contentHash = string(p.content_hash, '条目内容版本', 128);
|
|
172
|
+
if (!/^[0-9a-f]{64}$/u.test(contentHash))
|
|
173
|
+
fail('INVALID_INPUT', '条目内容版本必须是小写十六进制摘要。');
|
|
174
|
+
return { action: 'delivery.item.ack', parameters: {
|
|
175
|
+
task_id: string(p.task_id, '任务 ID'), delivery_id: string(p.delivery_id, '交付 ID'),
|
|
176
|
+
item_id: string(p.item_id, '条目 ID'), content_hash: contentHash,
|
|
177
|
+
attempt_no: attemptNo, result_id: string(p.result_id, '结果 ID')
|
|
178
|
+
} };
|
|
179
|
+
}
|
|
180
|
+
case 'delivery.item.question': {
|
|
181
|
+
const p = object(outer.parameters, ['task_id', 'delivery_id', 'item_id', 'content_hash', 'attempt_no', 'result_id', 'question_text']);
|
|
182
|
+
const attemptNo = p.attempt_no;
|
|
183
|
+
if (!Number.isSafeInteger(attemptNo) || attemptNo < 1 || attemptNo > 1_000_000) {
|
|
184
|
+
fail('INVALID_INPUT', '交付尝试次数必须是正整数。');
|
|
185
|
+
}
|
|
186
|
+
const contentHash = string(p.content_hash, '条目内容版本', 128);
|
|
187
|
+
if (!/^[0-9a-f]{64}$/u.test(contentHash))
|
|
188
|
+
fail('INVALID_INPUT', '条目内容版本必须是小写十六进制摘要。');
|
|
189
|
+
// 与知悉同款的「非空、长度受限」文本校验;正文随后在 Core 里统一脱敏并限长,
|
|
190
|
+
// 因此事实里永远不保存未脱敏正文。
|
|
191
|
+
const questionText = string(p.question_text, '问题内容', DELIVERY_QUESTION_TEXT_LIMIT);
|
|
192
|
+
return { action: 'delivery.item.question', parameters: {
|
|
193
|
+
task_id: string(p.task_id, '任务 ID'), delivery_id: string(p.delivery_id, '交付 ID'),
|
|
194
|
+
item_id: string(p.item_id, '条目 ID'), content_hash: contentHash,
|
|
195
|
+
attempt_no: attemptNo, result_id: string(p.result_id, '结果 ID'), question_text: questionText
|
|
196
|
+
} };
|
|
197
|
+
}
|
|
132
198
|
default: return fail('INVALID_ACTION', '不支持该管理动作。');
|
|
133
199
|
}
|
|
134
200
|
}
|
|
@@ -230,27 +296,294 @@ export class OwnerDecisionController {
|
|
|
230
296
|
workspaceRoots: [...s.workspaceRoots],
|
|
231
297
|
plans: kingdomId && record.view.actions.includes('plan.adopt') ? listCollaborationPlans(this.store, kingdomId).filter(plan => {
|
|
232
298
|
try {
|
|
233
|
-
this.context(record, { action: 'plan.adopt', parameters: { plan_id: plan.planId, version: plan.version, digest: plan.digest } });
|
|
299
|
+
this.context(record, { action: 'plan.adopt', parameters: { plan_id: plan.planId, version: plan.version, digest: plan.digest } }, randomUUID());
|
|
234
300
|
return true;
|
|
235
301
|
}
|
|
236
302
|
catch {
|
|
237
303
|
return false;
|
|
238
304
|
}
|
|
239
|
-
}) : []
|
|
305
|
+
}) : [],
|
|
306
|
+
deliveryItems: kingdomId && (record.view.actions.includes('delivery.item.ack') || record.view.actions.includes('delivery.item.question'))
|
|
307
|
+
? this.deliveryItems(record) : [],
|
|
308
|
+
deliveryQuestionHistory: kingdomId && record.view.actions.includes('delivery.item.question')
|
|
309
|
+
? this.deliveryQuestionHistory(record) : [] };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 本次授权范围内的**已有提问记录**条目(只读历史入口)。
|
|
313
|
+
*
|
|
314
|
+
* 只读且不依赖有界交付目录:它按权威提问事实列出精确 task/item,再逐条用
|
|
315
|
+
* `deliveryScopeAllows` 过滤,因此不含跨领地条目;不在当前目录、当前无法重验的
|
|
316
|
+
* 旧问答也能在这里被发现。它不返回任何正文,也**不**进入新提问 prepare 的来源目录。
|
|
317
|
+
*/
|
|
318
|
+
deliveryQuestionHistory(record) {
|
|
319
|
+
const kingdomId = record.view.kingdomId;
|
|
320
|
+
if (!kingdomId)
|
|
321
|
+
return [];
|
|
322
|
+
const choices = [];
|
|
323
|
+
for (const target of listDeliveryQuestionHistory(this.store, kingdomId)) {
|
|
324
|
+
const task = this.store.getTask(target.taskId);
|
|
325
|
+
if (!task)
|
|
326
|
+
continue;
|
|
327
|
+
const territory = this.store.getTerritoryById(task.territory_id);
|
|
328
|
+
if (!territory || territory.kingdom_id !== kingdomId || territory.status === 'DELETED')
|
|
329
|
+
continue;
|
|
330
|
+
if (!this.deliveryScopeAllows(record, territory))
|
|
331
|
+
continue;
|
|
332
|
+
choices.push({ taskId: target.taskId, taskTitle: redactDeliveryText(task.title), itemId: target.itemId,
|
|
333
|
+
itemLabel: target.itemLabel, lastAskedAt: target.lastAskedAt, questionCount: target.questionCount,
|
|
334
|
+
pendingCount: target.pendingCount, answeredCount: target.answeredCount, historyCount: target.historyCount,
|
|
335
|
+
unverifiableCount: target.unverifiableCount, latestItemVersion: target.latestItemVersion,
|
|
336
|
+
currentContentHash: target.currentContentHash });
|
|
337
|
+
}
|
|
338
|
+
return choices;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 交付知悉的授权范围判据。
|
|
342
|
+
*
|
|
343
|
+
* catalog 与 prepare/commit 必须共用同一判据:否则会出现「清单可见、
|
|
344
|
+
* 提交被拒」或「清单不可见、提交却可行」。`kingdomWide` 是 Owner 的最高
|
|
345
|
+
* 授权级别;其余情况要求任务所在领地在 `territoryIds` 内,或该领地由
|
|
346
|
+
* 授权 `bindingIds` 中的主管主理且本次授权声明了 SUPERVISOR 角色。
|
|
347
|
+
*/
|
|
348
|
+
deliveryScopeAllows(record, territory) {
|
|
349
|
+
const scope = record.view.scope;
|
|
350
|
+
if (scope.kingdomWide)
|
|
351
|
+
return true;
|
|
352
|
+
if (!scope.territoryIds.length && !scope.bindingIds.length && !scope.roleTypes.length)
|
|
353
|
+
return false;
|
|
354
|
+
if (scope.territoryIds.includes(territory.territory_id))
|
|
355
|
+
return true;
|
|
356
|
+
return scope.roleTypes.includes('SUPERVISOR')
|
|
357
|
+
&& !!territory.supervisor_binding_id
|
|
358
|
+
&& scope.bindingIds.includes(territory.supervisor_binding_id);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 本次授权范围内的已确认交付条目。只有同一 Task/attempt 主管 ACCEPT 的交付才出现,
|
|
362
|
+
* Worker Claim 永远不会进入该清单。
|
|
363
|
+
*/
|
|
364
|
+
deliveryItems(record) {
|
|
365
|
+
const kingdomId = record.view.kingdomId;
|
|
366
|
+
if (!kingdomId)
|
|
367
|
+
return [];
|
|
368
|
+
const ownerId = record.view.ownerId ?? '';
|
|
369
|
+
const items = [];
|
|
370
|
+
/**
|
|
371
|
+
* 与工作台「最近交付」对齐:按交付最近更新/被接受的时间排序。
|
|
372
|
+
*
|
|
373
|
+
* 主管 ACCEPT 会经 `transitionTask` 更新 `tasks.updated_at`,因此「旧建但最近
|
|
374
|
+
* 接受的交付」会排到工作台顶部;若这里仍按 `created_at` 截断,那条交付就会
|
|
375
|
+
* 在工作台可见、却在 Owner 目录之外,既无法知悉也无法看差异。只改排序键,
|
|
376
|
+
* 不新增第二套任务索引,也不改变条目上限语义。
|
|
377
|
+
*/
|
|
378
|
+
const tasks = [...this.store.listTasks(kingdomId)].sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.task_id.localeCompare(a.task_id));
|
|
379
|
+
for (const task of tasks) {
|
|
380
|
+
if (items.length >= 200)
|
|
381
|
+
break;
|
|
382
|
+
if (task.status !== 'DONE')
|
|
383
|
+
continue;
|
|
384
|
+
const territory = this.store.getTerritoryById(task.territory_id);
|
|
385
|
+
if (!territory || territory.kingdom_id !== kingdomId || territory.status === 'DELETED')
|
|
386
|
+
continue;
|
|
387
|
+
if (!this.deliveryScopeAllows(record, territory))
|
|
388
|
+
continue;
|
|
389
|
+
const claim = this.store.latestWorkerResult(task.task_id);
|
|
390
|
+
if (!claim)
|
|
391
|
+
continue;
|
|
392
|
+
const review = readLatestReviewEvent(this.store, kingdomId, task.task_id);
|
|
393
|
+
const classification = classifyAcceptedDelivery(this.store, claim, review);
|
|
394
|
+
if (!classification)
|
|
395
|
+
continue;
|
|
396
|
+
const deliveryId = deliveryIdFor(task.task_id);
|
|
397
|
+
const acknowledgements = readDeliveryAcknowledgements(this.store, kingdomId, deliveryId);
|
|
398
|
+
const questions = readDeliveryQuestionThread(this.store, kingdomId, deliveryId);
|
|
399
|
+
for (const item of deriveDeliveryItems(task.task_id, claim, this.changeEvidenceFor(kingdomId, task.task_id))) {
|
|
400
|
+
const view = deliveryAcknowledgementView(acknowledgements, item, ownerId);
|
|
401
|
+
const thread = deliveryQuestionThreadForItem(questions, item.itemId, item.contentHash);
|
|
402
|
+
items.push({
|
|
403
|
+
taskId: task.task_id,
|
|
404
|
+
taskTitle: redactDeliveryText(task.title),
|
|
405
|
+
deliveryId,
|
|
406
|
+
attemptNo: claim.attempt_no,
|
|
407
|
+
resultId: claim.result_id,
|
|
408
|
+
itemId: item.itemId,
|
|
409
|
+
contentHash: item.contentHash,
|
|
410
|
+
layer: item.content.layer,
|
|
411
|
+
label: item.content.label,
|
|
412
|
+
detail: item.content.detail,
|
|
413
|
+
changeKind: item.content.change.kind,
|
|
414
|
+
changeNote: item.content.change.note,
|
|
415
|
+
acknowledgementState: view.state,
|
|
416
|
+
acknowledgedAt: view.acknowledgedAt,
|
|
417
|
+
acceptanceEvidenceKind: classification.kind,
|
|
418
|
+
acceptanceEvidenceExact: classification.evidence.exactResultBound,
|
|
419
|
+
acceptanceEvidenceNote: classification.legacyNote,
|
|
420
|
+
questions: thread ? ownerDeliveryItemQuestions(thread) : null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return items;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* 只读返回一条已确认交付条目的完整问答线程。
|
|
428
|
+
*
|
|
429
|
+
* 有效性要求与 `readDeliveryChange` 同款:窗口 ACTIVE、本次授权包含
|
|
430
|
+
* `delivery.item.question`、任务在授权范围内、该 task 确实存在写给它的问题记录。
|
|
431
|
+
* 版本关系按该精确 task/item **独立重验**(不受有界展示目录截断影响):命中当前
|
|
432
|
+
* 派生版本即已验证当前版,命中的是旧版本即历史版;条目已无法从当前 ACCEPT/证据
|
|
433
|
+
* 重验(例如 CHANGE 证据丢失)时仍可按精确 itemId 读回,但整条标为无法重验,
|
|
434
|
+
* 不冒充当前版、也不可回复。任一不成立都失败,且不写任何事实、不自动知悉。
|
|
435
|
+
*/
|
|
436
|
+
readDeliveryQuestions(handle, input) {
|
|
437
|
+
const record = this.active(handle);
|
|
438
|
+
const kingdomId = record.view.kingdomId;
|
|
439
|
+
if (!kingdomId)
|
|
440
|
+
fail('DELIVERY_NOT_CONFIRMED', '当前窗口没有可读取交付的王国。');
|
|
441
|
+
if (!record.view.actions.includes('delivery.item.question')) {
|
|
442
|
+
fail('ACTION_NOT_AUTHORIZED', '本次管理窗口未授权交付条目提问动作,不能读取问答正文。');
|
|
443
|
+
}
|
|
444
|
+
const task = this.store.getTask(string(input.taskId, '任务 ID'));
|
|
445
|
+
if (!task)
|
|
446
|
+
fail('SCOPE_DENIED', '任务不存在或不属于当前王国。');
|
|
447
|
+
const territory = this.store.getTerritoryById(task.territory_id);
|
|
448
|
+
if (!territory || territory.kingdom_id !== kingdomId || territory.status === 'DELETED')
|
|
449
|
+
fail('SCOPE_DENIED', '任务不属于当前王国。');
|
|
450
|
+
if (!this.deliveryScopeAllows(record, territory))
|
|
451
|
+
fail('SCOPE_DENIED', '该交付不在本次授权范围内。');
|
|
452
|
+
const itemId = string(input.itemId, '条目 ID');
|
|
453
|
+
// 版本真值必须逐 Task/条目精确重验,**不能**取自有界展示目录:展示目录会截断
|
|
454
|
+
// 较旧条目,若用它判断,真实仍是当前版的问题会被误标为无法重验。
|
|
455
|
+
const currentItem = readDeliveryItemVersion(this.store, kingdomId, task.task_id, itemId);
|
|
456
|
+
const thread = deliveryQuestionThreadForItem(readDeliveryQuestionThread(this.store, kingdomId, deliveryIdFor(task.task_id)), itemId, currentItem ? currentItem.contentHash : null);
|
|
457
|
+
// 条目无法重验、也没有任何提问记录时仍是「未知条目」,不能被读成一条空线程。
|
|
458
|
+
if (!thread && !currentItem)
|
|
459
|
+
fail('DELIVERY_ITEM_UNKNOWN', '该条目已无法从当前交付重验,也没有可读取的问答记录。');
|
|
460
|
+
if (!thread)
|
|
461
|
+
fail('DELIVERY_QUESTION_UNKNOWN', '该条目当前版本还没有提问记录。');
|
|
462
|
+
const review = readLatestReviewEvent(this.store, kingdomId, task.task_id);
|
|
463
|
+
const reviewerBindingId = deliveryReviewerBindingId(review);
|
|
464
|
+
return {
|
|
465
|
+
deliveryId: thread.deliveryId,
|
|
466
|
+
taskId: task.task_id,
|
|
467
|
+
taskTitle: redactDeliveryText(task.title),
|
|
468
|
+
itemId: thread.itemId,
|
|
469
|
+
itemLabel: currentItem ? currentItem.label : thread.itemLabel,
|
|
470
|
+
contentHash: currentItem ? currentItem.contentHash : null,
|
|
471
|
+
questions: thread.questions.map(question => ({
|
|
472
|
+
questionId: question.questionId,
|
|
473
|
+
questionText: question.questionText,
|
|
474
|
+
askedAt: question.askedAt,
|
|
475
|
+
ownerId: question.ownerId,
|
|
476
|
+
reviewerBindingId: question.reviewerBindingId,
|
|
477
|
+
replyState: question.replyState,
|
|
478
|
+
replyStateNote: classifyReplyStateNote(question.replyState),
|
|
479
|
+
itemVersion: question.itemVersion,
|
|
480
|
+
currentItemContentHash: question.currentItemContentHash,
|
|
481
|
+
reply: question.reply ? { replyText: question.reply.replyText, repliedAt: question.reply.repliedAt,
|
|
482
|
+
responderBindingId: question.reply.responderBindingId } : null,
|
|
483
|
+
})),
|
|
484
|
+
answeredCount: thread.answeredCount,
|
|
485
|
+
pendingCount: thread.pendingCount,
|
|
486
|
+
historyCount: thread.historyCount,
|
|
487
|
+
unverifiableCount: thread.unverifiableCount,
|
|
488
|
+
note: reviewerBindingId ? DELIVERY_QUESTION_NOTE
|
|
489
|
+
: `${DELIVERY_QUESTION_NOTE} 另:该交付的接受事件没有记录主管绑定,无法确定接收者。`,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 已由主管在 ACCEPT 中显式选择、且本地 hash 重验通过的改动证据。
|
|
494
|
+
* 缺失、漂移或未确认时返回 null:调用方显示「不可定位」,不退回执行者自述。
|
|
495
|
+
*/
|
|
496
|
+
changeEvidenceFor(kingdomId, taskId) {
|
|
497
|
+
const review = readLatestReviewEvent(this.store, kingdomId, taskId);
|
|
498
|
+
const ref = readAcceptedChangeEvidence(review);
|
|
499
|
+
return ref ? resolveChangeEvidenceForDelivery(undefined, ref) : null;
|
|
500
|
+
}
|
|
501
|
+
acceptedChangeRef(kingdomId, taskId) {
|
|
502
|
+
return readAcceptedChangeEvidence(readLatestReviewEvent(this.store, kingdomId, taskId));
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 只读返回一条已确认改动条目的有界详情。
|
|
506
|
+
*
|
|
507
|
+
* 有效性要求:窗口 ACTIVE、**本次授权动作包含 `delivery.item.ack`**、任务在本次
|
|
508
|
+
* 授权范围内、该 task 确实存在一条主管 ACCEPT 绑定的改动证据、evidence id 与
|
|
509
|
+
* entry id 精确匹配、该条目仍需出现在当前有效交付目录中,且本地 hash 重验通过。
|
|
510
|
+
* 任一不成立都失败,且不写任何事实、不产生知悉。
|
|
511
|
+
*
|
|
512
|
+
* 只检查 ACTIVE + Territory scope 是不够的:同一领地的另一个管理窗口若没有该
|
|
513
|
+
* 动作授权,就不能凭 id 读取差异正文。这里与 `catalog` 使用同一动作判据,
|
|
514
|
+
* 不默认放行(也不新增只读动作)。
|
|
515
|
+
*/
|
|
516
|
+
readDeliveryChange(handle, input) {
|
|
517
|
+
const record = this.active(handle);
|
|
518
|
+
const kingdomId = record.view.kingdomId;
|
|
519
|
+
if (!kingdomId)
|
|
520
|
+
fail('DELIVERY_NOT_CONFIRMED', '当前窗口没有可读取交付的王国。');
|
|
521
|
+
if (!record.view.actions.includes('delivery.item.ack')) {
|
|
522
|
+
fail('ACTION_NOT_AUTHORIZED', '本次管理窗口未授权交付条目知悉动作,不能读取改动证据正文。');
|
|
523
|
+
}
|
|
524
|
+
const task = this.store.getTask(string(input.taskId, '任务 ID'));
|
|
525
|
+
if (!task)
|
|
526
|
+
fail('SCOPE_DENIED', '任务不存在或不属于当前王国。');
|
|
527
|
+
const territory = this.store.getTerritoryById(task.territory_id);
|
|
528
|
+
if (!territory || territory.kingdom_id !== kingdomId || territory.status === 'DELETED')
|
|
529
|
+
fail('SCOPE_DENIED', '任务不属于当前王国。');
|
|
530
|
+
if (!this.deliveryScopeAllows(record, territory))
|
|
531
|
+
fail('SCOPE_DENIED', '该交付不在本次授权范围内。');
|
|
532
|
+
const ref = this.acceptedChangeRef(kingdomId, task.task_id);
|
|
533
|
+
const evidenceId = string(input.evidenceId, '证据 ID', 128);
|
|
534
|
+
const entryId = string(input.entryId, '改动条目 ID', 128);
|
|
535
|
+
if (!ref || ref.evidenceId !== evidenceId || !ref.entryIds.includes(entryId)) {
|
|
536
|
+
fail('CHANGE_EVIDENCE_UNVERIFIED', '该改动引用不是本 Task 已由主管确认的证据,拒绝读取。');
|
|
537
|
+
}
|
|
538
|
+
// 必须仍属于当前有效交付目录:历史 ACCEPT 引用、已漂移证据或已不存在的条目
|
|
539
|
+
// 都不能单独开放正文。
|
|
540
|
+
const currentItem = this.deliveryItems(record).find(item => item.taskId === task.task_id
|
|
541
|
+
&& item.changeKind === 'REPO_RELATIVE_VERIFIED'
|
|
542
|
+
&& deliveryChangeItemId(item.deliveryId, entryId) === item.itemId);
|
|
543
|
+
if (!currentItem) {
|
|
544
|
+
fail('CHANGE_EVIDENCE_UNVERIFIED', '该改动条目不在当前有效交付目录内,拒绝读取。');
|
|
545
|
+
}
|
|
546
|
+
const opened = readChangeEntry(undefined, ref, entryId);
|
|
547
|
+
if (!opened.ok)
|
|
548
|
+
fail('CHANGE_EVIDENCE_UNVERIFIED', opened.reason);
|
|
549
|
+
const entry = opened.entry;
|
|
550
|
+
return {
|
|
551
|
+
kind: 'SUPERVISOR_CONFIRMED_BOUNDED_WINDOW',
|
|
552
|
+
label: CHANGE_EVIDENCE_LABEL,
|
|
553
|
+
evidenceId: ref.evidenceId,
|
|
554
|
+
entryId: entry.entryId,
|
|
555
|
+
taskId: task.task_id,
|
|
556
|
+
taskTitle: redactDeliveryText(task.title),
|
|
557
|
+
attemptNo: ref.attemptNo,
|
|
558
|
+
status: entry.status,
|
|
559
|
+
repoPath: entry.repoPath,
|
|
560
|
+
revision: `evidence:${ref.evidenceId}`,
|
|
561
|
+
repoVcs: opened.manifest.repo.vcs,
|
|
562
|
+
repoHead: opened.manifest.repo.head,
|
|
563
|
+
coverageComplete: opened.manifest.coverage.complete,
|
|
564
|
+
coverageReasons: opened.manifest.coverage.reasons.slice(0, 16),
|
|
565
|
+
labels: entry.labels.slice(0, 16),
|
|
566
|
+
note: entry.note,
|
|
567
|
+
hunks: entry.hunks.slice(0, 200).map(hunk => ({ kind: hunk.kind, beforeLine: hunk.beforeLine, afterLine: hunk.afterLine, text: hunk.text })),
|
|
568
|
+
};
|
|
240
569
|
}
|
|
241
570
|
async prepare(handle, raw, options = {}) {
|
|
242
571
|
const record = this.active(handle);
|
|
243
572
|
if (record.preparations.size >= 128)
|
|
244
573
|
fail('PREPARATION_LIMIT', '当前窗口准备次数已达上限,请重新激活。');
|
|
245
574
|
const input = normalizeInput(raw);
|
|
246
|
-
|
|
575
|
+
// 操作编号必须在预览与提交之间保持同一个:提问事实的身份以一次 Owner operation
|
|
576
|
+
// 为界(同一编号重放幂等、不同编号即使文本相同也是两条独立事实),因此它先于
|
|
577
|
+
// 预览计算生成,并同时用于指纹上下文与最终预览。
|
|
578
|
+
const operationId = randomUUID();
|
|
579
|
+
const context = this.context(record, input, operationId);
|
|
247
580
|
await this.validate(record, input, options.signal);
|
|
248
581
|
this.active(handle);
|
|
249
|
-
const checked = this.context(record, input);
|
|
582
|
+
const checked = this.context(record, input, operationId);
|
|
250
583
|
if (checked.fingerprint !== context.fingerprint)
|
|
251
584
|
fail('PREVIEW_STALE', '校验期间相关事实已改变,请重新预览。');
|
|
252
585
|
const createdAt = new Date(this.now()).toISOString();
|
|
253
|
-
const preview = { prepareId: randomUUID(), operationId
|
|
586
|
+
const preview = { prepareId: randomUUID(), operationId, decisionId: record.view.decisionId,
|
|
254
587
|
action: input.action, summary: checked.summary, changes: this.changes(input, checked),
|
|
255
588
|
affected: { territoryIds: checked.territoryIds, bindingIds: checked.bindingIds }, createdAt, expiresAt: record.view.expiresAt };
|
|
256
589
|
record.preparations.set(preview.prepareId, { input: clone(input), inputHash: ownerInputHash(input), fingerprint: checked.fingerprint, preview });
|
|
@@ -416,7 +749,7 @@ export class OwnerDecisionController {
|
|
|
416
749
|
if (!result || result.ok !== true)
|
|
417
750
|
fail(result?.ok === false ? result.code : 'VALIDATION_UNAVAILABLE', result?.ok === false ? result.message : '运行环境未提供有效校验结果。');
|
|
418
751
|
}
|
|
419
|
-
context(record, input) {
|
|
752
|
+
context(record, input, operationId) {
|
|
420
753
|
this.assertActive(record);
|
|
421
754
|
if (!record.view.actions.includes(input.action))
|
|
422
755
|
fail('SCOPE_DENIED', '该动作未获本次授权。');
|
|
@@ -474,6 +807,113 @@ export class OwnerDecisionController {
|
|
|
474
807
|
facts.push(related.map(t => t.territory_id).sort());
|
|
475
808
|
};
|
|
476
809
|
switch (input.action) {
|
|
810
|
+
case 'delivery.item.ack': {
|
|
811
|
+
const p = input.parameters;
|
|
812
|
+
const task = this.store.getTask(p.task_id);
|
|
813
|
+
if (!task)
|
|
814
|
+
fail('SCOPE_DENIED', '任务不存在或不属于当前王国。');
|
|
815
|
+
const taskTerritory = this.store.getTerritoryById(task.territory_id);
|
|
816
|
+
if (!taskTerritory || taskTerritory.kingdom_id !== kingdomId || taskTerritory.status === 'DELETED') {
|
|
817
|
+
fail('SCOPE_DENIED', '任务不属于当前王国。');
|
|
818
|
+
}
|
|
819
|
+
// 与 catalog 共用同一范围判据;知悉不改配置或责任,因此这里只登记领地事实,
|
|
820
|
+
// 不要求领地直接列在 territoryIds 中,也不参与 guardUnsettled。
|
|
821
|
+
if (!this.deliveryScopeAllows(record, taskTerritory))
|
|
822
|
+
fail('SCOPE_DENIED', '该交付不在本次授权范围内。');
|
|
823
|
+
territories.add(task.territory_id);
|
|
824
|
+
facts.push(taskTerritory);
|
|
825
|
+
const claim = this.store.latestWorkerResult(p.task_id);
|
|
826
|
+
const review = readLatestReviewEvent(this.store, kingdomId, p.task_id);
|
|
827
|
+
const classification = classifyAcceptedDelivery(this.store, claim, review);
|
|
828
|
+
if (!claim || !classification) {
|
|
829
|
+
fail('DELIVERY_NOT_CONFIRMED', '该交付尚未由同一 Task/attempt 的主管 ACCEPT 确认,不能记录知悉。');
|
|
830
|
+
}
|
|
831
|
+
if (claim.attempt_no !== p.attempt_no || claim.result_id !== p.result_id) {
|
|
832
|
+
fail('DELIVERY_VERSION_STALE', '交付的已接受版本已变化,请重新打开清单。');
|
|
833
|
+
}
|
|
834
|
+
// 接受证据强度进入 fingerprint:确认依据从强证据降为历史弱证据(或反之)时,
|
|
835
|
+
// 预览必须失效,Owner 不能在不知情的情况下按旧预览提交。
|
|
836
|
+
facts.push(claim, review, classification.kind, classification.evidence);
|
|
837
|
+
const deliveryId = deliveryIdFor(p.task_id);
|
|
838
|
+
if (deliveryId !== p.delivery_id)
|
|
839
|
+
fail('DELIVERY_VERSION_STALE', '交付标识与当前已接受版本不一致。');
|
|
840
|
+
const derived = deriveDeliveryItems(p.task_id, claim, this.changeEvidenceFor(kingdomId, p.task_id));
|
|
841
|
+
const item = derived.find(candidate => candidate.itemId === p.item_id);
|
|
842
|
+
if (!item)
|
|
843
|
+
fail('DELIVERY_ITEM_UNKNOWN', '该条目不在当前交付清单中。');
|
|
844
|
+
if (item.contentHash !== p.content_hash) {
|
|
845
|
+
fail('DELIVERY_ITEM_VERSION_STALE', '该条目内容版本已变化,请重新打开清单后再知悉。');
|
|
846
|
+
}
|
|
847
|
+
facts.push(item.contentHash);
|
|
848
|
+
const ownerId = this.store.getDefaultKingdom()?.owner_id ?? '';
|
|
849
|
+
const acknowledgements = readDeliveryAcknowledgements(this.store, kingdomId, deliveryId);
|
|
850
|
+
const view = deliveryAcknowledgementView(acknowledgements, item, ownerId);
|
|
851
|
+
if (view.acknowledged)
|
|
852
|
+
fail('DELIVERY_ITEM_ALREADY_ACKNOWLEDGED', '该条目当前版本已由 Owner 知悉;重复知悉不会新增记录。');
|
|
853
|
+
facts.push(acknowledgements);
|
|
854
|
+
before = view.acknowledgedAt;
|
|
855
|
+
after = JSON.stringify({ deliveryId, itemId: item.itemId, contentHash: item.contentHash, attemptNo: claim.attempt_no,
|
|
856
|
+
acceptanceEvidenceKind: classification.kind, acceptanceEvidenceExact: classification.evidence.exactResultBound });
|
|
857
|
+
summary = `记下已知悉「${item.content.label}」当前版本(不代表理解、质量认可、Task 完成或发布授权)`;
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
case 'delivery.item.question': {
|
|
861
|
+
const p = input.parameters;
|
|
862
|
+
const task = this.store.getTask(p.task_id);
|
|
863
|
+
if (!task)
|
|
864
|
+
fail('SCOPE_DENIED', '任务不存在或不属于当前王国。');
|
|
865
|
+
const taskTerritory = this.store.getTerritoryById(task.territory_id);
|
|
866
|
+
if (!taskTerritory || taskTerritory.kingdom_id !== kingdomId || taskTerritory.status === 'DELETED') {
|
|
867
|
+
fail('SCOPE_DENIED', '任务不属于当前王国。');
|
|
868
|
+
}
|
|
869
|
+
if (!this.deliveryScopeAllows(record, taskTerritory))
|
|
870
|
+
fail('SCOPE_DENIED', '该交付不在本次授权范围内。');
|
|
871
|
+
territories.add(task.territory_id);
|
|
872
|
+
facts.push(taskTerritory);
|
|
873
|
+
const claim = this.store.latestWorkerResult(p.task_id);
|
|
874
|
+
const review = readLatestReviewEvent(this.store, kingdomId, p.task_id);
|
|
875
|
+
const classification = classifyAcceptedDelivery(this.store, claim, review);
|
|
876
|
+
if (!claim || !classification) {
|
|
877
|
+
fail('DELIVERY_NOT_CONFIRMED', '该交付尚未由同一 Task/attempt 的主管 ACCEPT 确认,不能提问。');
|
|
878
|
+
}
|
|
879
|
+
if (claim.attempt_no !== p.attempt_no || claim.result_id !== p.result_id) {
|
|
880
|
+
fail('DELIVERY_VERSION_STALE', '交付的已接受版本已变化,请重新打开清单。');
|
|
881
|
+
}
|
|
882
|
+
facts.push(claim, review, classification.kind, classification.evidence);
|
|
883
|
+
const deliveryId = deliveryIdFor(p.task_id);
|
|
884
|
+
if (deliveryId !== p.delivery_id)
|
|
885
|
+
fail('DELIVERY_VERSION_STALE', '交付标识与当前已接受版本不一致。');
|
|
886
|
+
// 接收者在写入前就固定:接受事件没有可核对的主管绑定时直接拒绝,而不是先写问题再猜接收者。
|
|
887
|
+
const reviewerBindingId = deliveryReviewerBindingId(review);
|
|
888
|
+
if (!reviewerBindingId) {
|
|
889
|
+
fail('DELIVERY_ACCEPT_REVIEWER_UNKNOWN', '接受事件没有记录主管绑定,无法确定本问题的接收者。');
|
|
890
|
+
}
|
|
891
|
+
facts.push(reviewerBindingId);
|
|
892
|
+
const derived = deriveDeliveryItems(p.task_id, claim, this.changeEvidenceFor(kingdomId, p.task_id));
|
|
893
|
+
const item = derived.find(candidate => candidate.itemId === p.item_id);
|
|
894
|
+
if (!item)
|
|
895
|
+
fail('DELIVERY_ITEM_UNKNOWN', '该条目不在当前交付清单中。');
|
|
896
|
+
if (item.contentHash !== p.content_hash) {
|
|
897
|
+
fail('DELIVERY_ITEM_VERSION_STALE', '该条目内容版本已变化,请重新打开清单后再提问。');
|
|
898
|
+
}
|
|
899
|
+
facts.push(item.contentHash);
|
|
900
|
+
const questionText = boundedQuestionText(p.question_text, DELIVERY_QUESTION_TEXT_LIMIT);
|
|
901
|
+
if (!questionText)
|
|
902
|
+
fail('INVALID_INPUT', '问题内容必须是非空、长度受限的文本。');
|
|
903
|
+
// 幂等只以**本次 Owner operation** 为界:同一 operationId 重放命中同一事件 ID,
|
|
904
|
+
// 才会被判为「不会新增第二条事实」;不同 operation 即使条目与文本都相同,也各自
|
|
905
|
+
// 形成独立问题。绝不按正文去重,否则新操作的提问会被历史同文问题吞掉。
|
|
906
|
+
const operationEventId = deliveryQuestionEventId({ kingdomId: kingdomId, deliveryId, itemId: item.itemId,
|
|
907
|
+
contentHash: item.contentHash, operationId });
|
|
908
|
+
const duplicate = this.store.getEventById(operationEventId) !== null;
|
|
909
|
+
const thread = deliveryQuestionThreadForItem(readDeliveryQuestionThread(this.store, kingdomId, deliveryId), item.itemId, item.contentHash);
|
|
910
|
+
facts.push(thread?.questions.map(question => [question.questionId, question.reply?.eventId ?? null]).sort() ?? null);
|
|
911
|
+
before = thread ? `${thread.questions.length} 条提问(${thread.pendingCount} 待回复 / ${thread.answeredCount} 已回复)` : null;
|
|
912
|
+
after = JSON.stringify({ deliveryId, itemId: item.itemId, contentHash: item.contentHash, attemptNo: claim.attempt_no,
|
|
913
|
+
reviewerBindingId, questionText, duplicate });
|
|
914
|
+
summary = `就「${item.content.label}」当前版本向接受该交付的主管提一个具体问题(记录为一条对话事实,不是知悉,也不改变任务状态)`;
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
477
917
|
case 'plan.adopt': {
|
|
478
918
|
const p = input.parameters, plan = readCollaborationPlan(this.store, kingdomId, p.plan_id);
|
|
479
919
|
if (!plan || plan.version !== p.version || plan.digest !== p.digest || plan.state !== 'PROPOSED')
|
|
@@ -604,7 +1044,9 @@ export class OwnerDecisionController {
|
|
|
604
1044
|
}
|
|
605
1045
|
const context = { territoryIds: [...territories].sort(), bindingIds: [...bindings].sort(), sessionIds: [...sessions].sort(),
|
|
606
1046
|
fingerprint: ownerInputHash(facts), before, after, summary };
|
|
607
|
-
|
|
1047
|
+
// `delivery.item.ack` 只追加 Owner 知悉事实,不改配置与责任,因此不受
|
|
1048
|
+
// 同领地不相关未结算执行/Lease/Dispatch 阻断。
|
|
1049
|
+
if (kingdomId && input.action !== 'territory.update' && input.action !== 'territory.create' && input.action !== 'budget.policy' && input.action !== 'plan.adopt' && input.action !== 'delivery.item.ack' && input.action !== 'delivery.item.question')
|
|
608
1050
|
this.guardUnsettled(kingdomId, input.action === 'ceiling', context);
|
|
609
1051
|
return context;
|
|
610
1052
|
}
|
|
@@ -688,11 +1130,51 @@ export class OwnerDecisionController {
|
|
|
688
1130
|
const after = input.parameters.profile;
|
|
689
1131
|
return [change('请求的 Provider', before?.provider, after?.provider), change('请求的模型', before?.model, after?.model)];
|
|
690
1132
|
}
|
|
1133
|
+
case 'delivery.item.ack': {
|
|
1134
|
+
const after = JSON.parse(context.after);
|
|
1135
|
+
const task = this.store.getTask(input.parameters.task_id);
|
|
1136
|
+
const item = deriveDeliveryItems(input.parameters.task_id, this.store.latestWorkerResult(input.parameters.task_id), this.changeEvidenceFor(this.store.getDefaultKingdom()?.kingdom_id ?? '', input.parameters.task_id))
|
|
1137
|
+
.find(candidate => candidate.itemId === after.itemId);
|
|
1138
|
+
return [change('任务', null, task?.title),
|
|
1139
|
+
change('已接受尝试', null, String(after.attemptNo)),
|
|
1140
|
+
change('条目', null, item ? `${item.content.label}` : after.itemId),
|
|
1141
|
+
change('条目内容', null, item?.content.detail),
|
|
1142
|
+
change('内容版本', null, after.contentHash),
|
|
1143
|
+
change('改动定位', null, item ? `${item.content.change.kind}${item.content.change.evidenceLabel ? ` · ${item.content.change.evidenceLabel}` : ''} · ${item.content.change.note}` : null),
|
|
1144
|
+
change('接受证据强度', null, after.acceptanceEvidenceExact
|
|
1145
|
+
? 'EXACT_RESULT_BOUND:TASK_ACCEPTED 锁定了本次结果 ID 与内容摘要,且两者都与当前呈报一致。'
|
|
1146
|
+
: '历史接受证据较弱(LEGACY_ATTEMPT_ONLY):该 Task/attempt 的 TASK_ACCEPTED 是 v1.0.0 旧格式,只有尝试编号,缺少被审查结果 ID 与内容摘要;本条按真实事件字段与同 Task/attempt 的唯一 WorkerResult 判定,不是 exact result-bound 证据。知悉仍只表示已知悉该条当前版本,不代表理解、质量认可、人类验收、Task DONE 或发布授权。'),
|
|
1147
|
+
change('Owner 知悉时间(本窗口提交后)', null, '提交时记录'),
|
|
1148
|
+
change('不改动的状态', null, '任务状态、主管审查、正式 Owner acceptance 与发布状态均不变')];
|
|
1149
|
+
}
|
|
1150
|
+
case 'delivery.item.question': {
|
|
1151
|
+
const after = JSON.parse(context.after);
|
|
1152
|
+
const task = this.store.getTask(input.parameters.task_id);
|
|
1153
|
+
const item = deriveDeliveryItems(input.parameters.task_id, this.store.latestWorkerResult(input.parameters.task_id), this.changeEvidenceFor(this.store.getDefaultKingdom()?.kingdom_id ?? '', input.parameters.task_id))
|
|
1154
|
+
.find(candidate => candidate.itemId === after.itemId);
|
|
1155
|
+
const reviewer = this.store.getBindingById(after.reviewerBindingId);
|
|
1156
|
+
const territory = this.store.getTerritoryById(task?.territory_id ?? '');
|
|
1157
|
+
return [change('任务', null, task?.title),
|
|
1158
|
+
change('已接受尝试', null, String(after.attemptNo)),
|
|
1159
|
+
change('条目', null, item ? item.content.label : after.itemId),
|
|
1160
|
+
change('条目内容', null, item?.content.detail),
|
|
1161
|
+
change('内容版本', null, after.contentHash),
|
|
1162
|
+
change('接收者(接受该交付的主管)', null, reviewer ? `${reviewer.role_name}(${reviewer.binding_id})` : after.reviewerBindingId),
|
|
1163
|
+
change('该领地当前主理主管', null, territory?.supervisor_binding_id ?? '未指派'),
|
|
1164
|
+
change('本次提交', null, after.duplicate
|
|
1165
|
+
? '该条目当前版本的同一问题已有记录:不会新增第二条事实,也不会改变任何状态。'
|
|
1166
|
+
: '新增一条 Owner 提问事实;接收者仍是接受该交付的主管,当前未被读取前只显示「待领取」。'),
|
|
1167
|
+
change('问题的准确保留范围', null, after.duplicate
|
|
1168
|
+
? '与既有记录一致,不重复写入。'
|
|
1169
|
+
: '只写入提问事实本身;不自动通知、不自动派发、不自动唤醒任何 Agent。'),
|
|
1170
|
+
change('不改动的状态', null, '任务状态、主管审查、知悉、正式 Owner acceptance 与发布状态均不变;提问不是知悉。'),
|
|
1171
|
+
change('回复', null, '由接受该交付的主管在其 session-bound Agent Tool 中回复;Owner 不代答,其他主管不能代答。')];
|
|
1172
|
+
}
|
|
691
1173
|
}
|
|
692
1174
|
}
|
|
693
1175
|
async apply(handle, record, preparation, signal) {
|
|
694
1176
|
this.active(handle);
|
|
695
|
-
const context = this.context(record, preparation.input);
|
|
1177
|
+
const context = this.context(record, preparation.input, preparation.preview.operationId);
|
|
696
1178
|
if (context.fingerprint !== preparation.fingerprint)
|
|
697
1179
|
fail('PREVIEW_STALE', '相关对象已变化,请重新预览。');
|
|
698
1180
|
await this.validate(record, preparation.input, signal);
|
|
@@ -701,7 +1183,7 @@ export class OwnerDecisionController {
|
|
|
701
1183
|
fail('REQUEST_ABORTED', '请求已取消。');
|
|
702
1184
|
const result = this.store.withImmediateTransaction(() => {
|
|
703
1185
|
this.active(handle);
|
|
704
|
-
const current = this.context(record, preparation.input);
|
|
1186
|
+
const current = this.context(record, preparation.input, preparation.preview.operationId);
|
|
705
1187
|
if (current.fingerprint !== preparation.fingerprint || ownerInputHash(preparation.input) !== preparation.inputHash)
|
|
706
1188
|
fail('PREVIEW_STALE', '相关事实或准备内容已变化,请重新预览。');
|
|
707
1189
|
const source = { source_channel: 'LOCAL_OWNER_GUI', authorization_source: 'LOCAL_DIRECT_SLASH',
|
|
@@ -753,6 +1235,8 @@ export class OwnerDecisionController {
|
|
|
753
1235
|
}
|
|
754
1236
|
};
|
|
755
1237
|
let message, eventType;
|
|
1238
|
+
/** 本次是否真的追加了一条新业务事实;幂等重试时没有新事件,不能按「事件数恰好 1」判定失败。 */
|
|
1239
|
+
let appendedBusinessFact = true;
|
|
756
1240
|
switch (input.action) {
|
|
757
1241
|
case 'plan.adopt':
|
|
758
1242
|
message = run({ kingdomId, ...input.parameters }, adoptCollaborationPlan);
|
|
@@ -802,11 +1286,108 @@ export class OwnerDecisionController {
|
|
|
802
1286
|
message = '请求的执行配置已保存;这不代表模型已实际运行成功。';
|
|
803
1287
|
eventType = 'EXECUTION_PROFILE_UPDATED';
|
|
804
1288
|
break;
|
|
1289
|
+
case 'delivery.item.ack': {
|
|
1290
|
+
const claim = this.store.latestWorkerResult(input.parameters.task_id);
|
|
1291
|
+
if (!claim)
|
|
1292
|
+
fail('DELIVERY_NOT_CONFIRMED', '交付对应的结果不存在。');
|
|
1293
|
+
const review = readLatestReviewEvent(this.store, kingdomId, input.parameters.task_id);
|
|
1294
|
+
const existing = readDeliveryAcknowledgements(this.store, kingdomId, input.parameters.delivery_id);
|
|
1295
|
+
const already = existing.some(ack => ack.itemId === input.parameters.item_id && ack.contentHash === input.parameters.content_hash
|
|
1296
|
+
&& ack.ownerId === record.view.ownerId);
|
|
1297
|
+
if (!already) {
|
|
1298
|
+
const classification = classifyAcceptedDelivery(this.store, claim, review);
|
|
1299
|
+
if (!classification)
|
|
1300
|
+
fail('DELIVERY_NOT_CONFIRMED', '交付尚未由主管 ACCEPT 确认。');
|
|
1301
|
+
const changeEvidence = this.changeEvidenceFor(kingdomId, input.parameters.task_id);
|
|
1302
|
+
const item = deriveDeliveryItems(input.parameters.task_id, claim, changeEvidence).find(candidate => candidate.itemId === input.parameters.item_id);
|
|
1303
|
+
if (!item || item.contentHash !== input.parameters.content_hash)
|
|
1304
|
+
fail('DELIVERY_ITEM_VERSION_STALE', '条目内容版本已变化。');
|
|
1305
|
+
recordDeliveryAcknowledgement(this.store, {
|
|
1306
|
+
kingdomId,
|
|
1307
|
+
deliveryId: input.parameters.delivery_id,
|
|
1308
|
+
itemId: input.parameters.item_id,
|
|
1309
|
+
contentHash: input.parameters.content_hash,
|
|
1310
|
+
taskId: input.parameters.task_id,
|
|
1311
|
+
attemptNo: input.parameters.attempt_no,
|
|
1312
|
+
resultId: input.parameters.result_id,
|
|
1313
|
+
ownerId: record.view.ownerId,
|
|
1314
|
+
ownerBindingId: this.store.getBindingByRole(kingdomId, 'OWNER')?.binding_id ?? null,
|
|
1315
|
+
itemLabel: item.content.label,
|
|
1316
|
+
acknowledgedAt: new Date(this.now()).toISOString(),
|
|
1317
|
+
attribution: { ...source },
|
|
1318
|
+
changeEvidence,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
message = already
|
|
1322
|
+
? '该条目当前版本的 Owner 知悉记录已存在;未新增第二条事实。知悉不改变任务、审查与发布状态。'
|
|
1323
|
+
: '已记下 Owner 已知悉该条当前版本;这不代表理解、质量认可、Task DONE、正式验收或发布授权,也未改变任务、审查与发布状态。';
|
|
1324
|
+
eventType = DELIVERY_ACK_EVENT_TYPE;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case 'delivery.item.question': {
|
|
1328
|
+
const claim = this.store.latestWorkerResult(input.parameters.task_id);
|
|
1329
|
+
if (!claim)
|
|
1330
|
+
fail('DELIVERY_NOT_CONFIRMED', '交付对应的结果不存在。');
|
|
1331
|
+
const review = readLatestReviewEvent(this.store, kingdomId, input.parameters.task_id);
|
|
1332
|
+
// 责任归属必须在事务内重算:预览时的接受主管身份、条目版本或领地改绑都可能已变化。
|
|
1333
|
+
const reviewerBindingId = deliveryReviewerBindingId(review);
|
|
1334
|
+
if (!reviewerBindingId)
|
|
1335
|
+
fail('DELIVERY_ACCEPT_REVIEWER_UNKNOWN', '接受事件没有记录主管绑定,无法确定本问题的接收者。');
|
|
1336
|
+
// CHANGE 条目只由「主管在 ACCEPT 中显式选择、且本地 hash 重验通过」的改动证据派生:
|
|
1337
|
+
// 预览、事务内校验与写入端必须复用**同一份**已重验证据,写入端才可能重算出同一
|
|
1338
|
+
// itemId/contentHash,而不是必报 DELIVERY_ITEM_UNKNOWN。
|
|
1339
|
+
const changeEvidence = this.changeEvidenceFor(kingdomId, input.parameters.task_id);
|
|
1340
|
+
const item = deriveDeliveryItems(input.parameters.task_id, claim, changeEvidence)
|
|
1341
|
+
.find(candidate => candidate.itemId === input.parameters.item_id);
|
|
1342
|
+
if (!item || item.contentHash !== input.parameters.content_hash)
|
|
1343
|
+
fail('DELIVERY_ITEM_VERSION_STALE', '条目内容版本已变化。');
|
|
1344
|
+
const questionText = boundedQuestionText(input.parameters.question_text, DELIVERY_QUESTION_TEXT_LIMIT);
|
|
1345
|
+
if (!questionText)
|
|
1346
|
+
fail('INVALID_INPUT', '问题内容必须是非空、长度受限的文本。');
|
|
1347
|
+
const operationId = source.operation_id;
|
|
1348
|
+
if (!operationId)
|
|
1349
|
+
fail('OPERATION_MISMATCH', '本次管理操作没有操作编号,拒绝写入提问事实。');
|
|
1350
|
+
// 提问身份属于产生它的这次 Owner operation:同一编号重放由写入端的幂等分支核对
|
|
1351
|
+
// 并原样返回既有事实;不同编号即使文本相同也各自形成独立问题。这里不再按正文去重,
|
|
1352
|
+
// 「没有新事件」也不等于操作失败。
|
|
1353
|
+
const recorded = recordDeliveryQuestion(this.store, {
|
|
1354
|
+
kingdomId,
|
|
1355
|
+
deliveryId: input.parameters.delivery_id,
|
|
1356
|
+
taskId: input.parameters.task_id,
|
|
1357
|
+
itemId: input.parameters.item_id,
|
|
1358
|
+
itemLabel: item.content.label,
|
|
1359
|
+
contentHash: input.parameters.content_hash,
|
|
1360
|
+
attemptNo: input.parameters.attempt_no,
|
|
1361
|
+
resultId: input.parameters.result_id,
|
|
1362
|
+
ownerId: record.view.ownerId,
|
|
1363
|
+
ownerBindingId: this.store.getBindingByRole(kingdomId, 'OWNER')?.binding_id ?? null,
|
|
1364
|
+
operationId,
|
|
1365
|
+
questionText,
|
|
1366
|
+
askedAt: new Date(this.now()).toISOString(),
|
|
1367
|
+
changeEvidence,
|
|
1368
|
+
attribution: { ...source },
|
|
1369
|
+
});
|
|
1370
|
+
appendedBusinessFact = recorded.created;
|
|
1371
|
+
message = recorded.created
|
|
1372
|
+
? `已记录 Owner 就该条目当前版本提出的问题,接收者为接受该交付的主管。当前只显示「待领取」:在主管实际读取前不声称已通知或已阅读;这不代表知悉、验收或任务完成,也不改变任务、审查与发布状态。`
|
|
1373
|
+
: '同一 Owner 操作重放:既有提问记录与本次引用一致,未新增第二条事实,也未改变任何状态。';
|
|
1374
|
+
eventType = DELIVERY_QUESTION_EVENT_TYPE;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
805
1377
|
}
|
|
806
1378
|
const events = this.store.listEventsSince(kingdomId, beforeSeq, 20);
|
|
807
1379
|
const business = events.filter(event => event.event_type === eventType && event.actor_role === 'OWNER' && event.actor_id === record.view.ownerId
|
|
808
1380
|
&& JSON.parse(event.payload_json).operation_id === source.operation_id);
|
|
809
|
-
|
|
1381
|
+
// 幂等提问没有新增事件:此时只核对既有事实仍指向同一条交付,不要求事件数为 1。
|
|
1382
|
+
if (!appendedBusinessFact && input.action === 'delivery.item.question') {
|
|
1383
|
+
const existing = readDeliveryQuestionThread(this.store, kingdomId, input.parameters.delivery_id);
|
|
1384
|
+
const matches = existing?.questions.some(question => question.itemId === input.parameters.item_id
|
|
1385
|
+
&& question.contentHash === input.parameters.content_hash) ?? false;
|
|
1386
|
+
if (!matches)
|
|
1387
|
+
fail('MUTATION_NOT_APPLIED', '既有提问事实与本次引用不一致,事务已回滚。');
|
|
1388
|
+
return { target: { type: 'delivery', id: input.parameters.delivery_id }, message };
|
|
1389
|
+
}
|
|
1390
|
+
if (business.length !== 1 || !business[0].target_id || !['kingdom', 'territory', 'binding', 'collaboration-plan', 'delivery'].includes(business[0].target_type ?? '')) {
|
|
810
1391
|
fail('MUTATION_NOT_APPLIED', 'Core 未产生准确业务事实,事务已回滚。');
|
|
811
1392
|
}
|
|
812
1393
|
return { target: { type: business[0].target_type, id: business[0].target_id }, message };
|