Archived
harness 与工作台同日修:数据工单确认即入库 · 叫了人不再自动推进(两道结构闸)· 审核卡 = 终局验收卡结构 · 验收清单人点达标 · 迟到守卫不丢放行裁决 · 画面跟着这一拍发出去的卡走 · 成果详情只翻页 · 占位居中 · 进度条
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、 data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task (ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest; 迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。 逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
DraftWrite,
|
||||
LedgerService,
|
||||
ObjectAddress,
|
||||
ReleaseScope,
|
||||
ReleaseWriteResult,
|
||||
SchemaApplyResult,
|
||||
SystemWrite,
|
||||
@@ -50,6 +51,20 @@ function groupDeclarations(declarations: readonly ObjectDeclaration[]): Map<stri
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** 范围过滤:不给范围 = 全收 */
|
||||
function inScope(only: readonly ObjectAddress[] | undefined): (draft: ObjectRef) => boolean {
|
||||
if (only === undefined) return () => true;
|
||||
const wanted = new Set(only.map((a) => `${a.domain}.${a.object}`));
|
||||
return (draft) => wanted.has(`${draft.domain}.${draft.object}`);
|
||||
}
|
||||
|
||||
/** 落地后的基线:只取这一笔碰过的那几列,值取表里那一份 */
|
||||
function baselineOf(stored: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const field of Object.keys(patch)) out[field] = stored[field];
|
||||
return out;
|
||||
}
|
||||
|
||||
export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
function dataEvent(
|
||||
ref: ObjectRef,
|
||||
@@ -181,13 +196,29 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
return true;
|
||||
},
|
||||
|
||||
/** ③ 放行写:冲突检查 → 原子合并 → 此刻抛事件 */
|
||||
async releaseWrite(taskId): Promise<ReleaseWriteResult> {
|
||||
const conflicts = await this.checkConflicts(taskId);
|
||||
/**
|
||||
* ③ 放行写:冲突检查 → 原子合并 → 此刻抛事件。
|
||||
*
|
||||
* `scope` 给了就是**数据工单确认那一道门**(2026-08-27 用户定:「数据工单里填的数据,
|
||||
* 确认这个数据工单后就应该入库」):只并那几张表,并过的行留在草稿层打「已落地」记号 ——
|
||||
* 它仍是本单写下的行(`mine:` / `_rows` / 任务视图照旧认它,不然 P-08 那张件当场变空、
|
||||
* S1 的哨兵 `parts_profile_rows >= created` 永远开不了),只是整单放行时不再写它。
|
||||
*/
|
||||
async releaseWrite(taskId, scope?: ReleaseScope): Promise<ReleaseWriteResult> {
|
||||
const conflicts = await this.checkConflicts(taskId, scope?.only);
|
||||
if (conflicts.length > 0) throw new DraftConflictError(conflicts);
|
||||
|
||||
const drafts = await deps.drafts.list(taskId);
|
||||
if (drafts.length === 0) return { mergedRows: 0, emittedEvents: 0, simulatedRows: 0, merged: [] };
|
||||
const drafts = (await deps.drafts.list(taskId)).filter(inScope(scope?.only));
|
||||
const nothing: ReleaseWriteResult = { mergedRows: 0, emittedEvents: 0, landedEarlierRows: 0, simulatedRows: 0, merged: [] };
|
||||
if (drafts.length === 0) return nothing;
|
||||
const scoped = scope !== undefined;
|
||||
const causeRef = scoped ? scope.cause : `release:${taskId}`;
|
||||
/*
|
||||
* 事件指不指回本单:整单放行指回(它正要收尾,与从前一样);数据工单那一道**不指回** ——
|
||||
* 本单此刻正等着自己的人事件(卡的回传)叫醒,再喂它七八条 data 事件只会在日志里结巴,
|
||||
* 而它对这些行的「读己之写」从没断过。别单的监听照旧按正式版事件认(跨任务只认过门的事实)。
|
||||
*/
|
||||
const target = scoped ? undefined : taskId;
|
||||
|
||||
/*
|
||||
* **试跑单不落主数据** —— 挡在这一处,是因为这里是草稿层通往世界的唯一一道口子。
|
||||
@@ -208,6 +239,7 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
const heldBack = (draft: { domain: string; object: string }): boolean =>
|
||||
simulated && masters.has(`${draft.domain}.${draft.object}`);
|
||||
let simulatedRows = 0;
|
||||
let landedEarlierRows = 0;
|
||||
|
||||
const pending: EnvelopeDraft[] = [];
|
||||
/*
|
||||
@@ -224,6 +256,17 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
await deps.store.transaction(async (store, draftStore) => {
|
||||
for (const draft of drafts) {
|
||||
const ref: ObjectRef = { domain: draft.domain, object: draft.object };
|
||||
if (draft.landed === true) {
|
||||
/*
|
||||
* 早已在数据工单确认那一道门上落过地:这次不写、不抛事件、不计入合并数;
|
||||
* 只把表里那一份读回来当事实(出口载荷要它)。表里读不到(别单后来删了)就不摆。
|
||||
*/
|
||||
landedEarlierRows += 1;
|
||||
if (draft.tombstone) continue;
|
||||
const current = (await store.selectByKeys(ref, [draft.key]))[0];
|
||||
if (current !== undefined) landed.push({ domain: draft.domain, object: draft.object, key: draft.key, values: current });
|
||||
continue;
|
||||
}
|
||||
if (heldBack(draft)) {
|
||||
simulatedRows += 1;
|
||||
if (draft.tombstone) continue; // 试跑不删主数据,连一行也不许少
|
||||
@@ -243,9 +286,8 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
}
|
||||
if (draft.tombstone) {
|
||||
await store.deleteRow(ref, draft.key);
|
||||
pending.push(
|
||||
dataEvent(ref, draft.key, [], 'delete', taskId, { kind: 'ledger-write', ref: `release:${taskId}` }),
|
||||
);
|
||||
pending.push(dataEvent(ref, draft.key, [], 'delete', target, { kind: 'ledger-write', ref: causeRef }));
|
||||
if (scoped) await draftStore.markLanded(taskId, ref, draft.key, {});
|
||||
continue;
|
||||
}
|
||||
const existing = await store.selectByKeys(ref, [draft.key]);
|
||||
@@ -264,18 +306,24 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
draft.key,
|
||||
Object.keys(draft.patch),
|
||||
existing.length === 0 ? 'insert' : 'update',
|
||||
taskId,
|
||||
{ kind: 'ledger-write', ref: `release:${taskId}` },
|
||||
target,
|
||||
{ kind: 'ledger-write', ref: causeRef },
|
||||
),
|
||||
);
|
||||
/*
|
||||
* 数据工单那一道门:条目留着、打记号,基线换成落地后表里的值(PG 归型后的那一份)——
|
||||
* 之后再往这一行写一笔,冲突检查比的才是「我改之前表里是什么」。
|
||||
*/
|
||||
if (scoped) await draftStore.markLanded(taskId, ref, draft.key, baselineOf(stored ?? draft.patch, draft.patch));
|
||||
}
|
||||
await draftStore.clear(taskId);
|
||||
if (!scoped) await draftStore.clear(taskId);
|
||||
});
|
||||
|
||||
for (const envelope of pending) await deps.events.publish(envelope);
|
||||
return {
|
||||
mergedRows: drafts.length - simulatedRows,
|
||||
mergedRows: drafts.length - simulatedRows - landedEarlierRows,
|
||||
emittedEvents: pending.length,
|
||||
landedEarlierRows,
|
||||
simulatedRows,
|
||||
merged: landed,
|
||||
};
|
||||
@@ -299,11 +347,11 @@ export function createLedgerService(deps: LedgerServiceDeps): LedgerService {
|
||||
|
||||
async taskDrafts(taskId) {
|
||||
const drafts = await deps.drafts.list(taskId);
|
||||
return drafts.map((d) => ({ domain: d.domain, object: d.object, key: d.key, patch: d.patch }));
|
||||
return drafts.map((d) => ({ domain: d.domain, object: d.object, key: d.key, patch: d.patch, landed: d.landed === true }));
|
||||
},
|
||||
|
||||
async checkConflicts(taskId) {
|
||||
const drafts = await deps.drafts.list(taskId);
|
||||
async checkConflicts(taskId, only) {
|
||||
const drafts = (await deps.drafts.list(taskId)).filter(inScope(only));
|
||||
if (drafts.length === 0) return [];
|
||||
const byObject = new Map<string, Array<(typeof drafts)[number]>>();
|
||||
for (const d of drafts) {
|
||||
|
||||
Reference in New Issue
Block a user