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:
xcs
2026-08-27 23:20:16 +08:00
co-authored by Claude Fable 5
parent 909b1fe4b7
commit 7b85a79323
79 changed files with 3903 additions and 1297 deletions
@@ -117,6 +117,15 @@ export type HumanToolService = {
/** 全部工单(含已办结)—— 工作成果要回看定稿的报告,只给 open 就永远是空的 */
allWorkOrders(taskId: TaskId): Promise<WorkOrder[]>;
chatCards(taskId: TaskId): Promise<ChatCard[]>;
/**
* **这一单正等人交回的卡**(2026-08-27):没办结的工单 + 没作答、没被顶替的对话卡。
* 引擎每拍开头问它 —— 有卡还没答,这一拍就不该问模型(叫了人就等人,不许自己往下推)。
*/
pendingCards(taskId: TaskId): Promise<Array<{
id: string;
title: string;
kind: 'work-order' | 'chat-card';
}>>;
/** org 停用前置检查要问的「待答卡」 */
openCardsOf(staffId: StaffId): Promise<Array<{
cardId: string;
+2
View File
@@ -26,6 +26,8 @@ export type CardStore = {
recipientId: StaffId;
} | undefined>;
listChatCards(taskId: TaskId): Promise<ChatCard[]>;
/** 还开着的对话卡(没被新卡顶替、没作废)—— 「这一单有没有卡在等人」问的是它,不是全部历史 */
listOpenChatCards(taskId: TaskId): Promise<ChatCard[]>;
updateChatCard(card: ChatCard): Promise<void>;
closeChatCard(id: string, supersededBy: string, reason: string): Promise<void>;
/** 调人次数 —— 打扰率与预算的计数面 */
+31 -3
View File
@@ -48,9 +48,29 @@ export type DraftWrite = {
readonly patch: LedgerRow;
readonly tombstone?: boolean;
};
/**
* **放行写的范围**(2026-08-27,数据工单确认即入库)。
*
* 不给 = 整单放行:草稿层全部并进正式表、清空。
* 给了 = 只并这几张表 —— 它们的门不是放行卡,是**数据工单的确认**(那张卡的提交闸
* 盯的就是这几张表:必填齐了才按得下确认,确认那一刻就入库)。并过的行**留在草稿层、
* 打上已落地记号**:它仍是本单写下的行(任务视图 / `mine:` / `_rows` 照旧认它),
* 只是整单放行时不再写它、不再为它抛事件。
* `cause` 落进数据事件的归因(`ledger-write:<cause>`),回看时认得出是哪张卡签下去的。
*/
export type ReleaseScope = {
readonly only: readonly ObjectAddress[];
readonly cause: string;
};
export type ReleaseWriteResult = {
readonly mergedRows: number;
readonly emittedEvents: number;
/**
* **早已落地、这次不再写的行数**(2026-08-27)—— 数据工单确认时并进去的那几行,
* 整单放行时只把它们当事实读回来(出口载荷要它们),不再写、不再抛事件。
* 放行痕上要分开说:「合并 N 行」里若混着这几行,那句话就多报了。
*/
readonly landedEarlierRows: number;
/**
* **试跑单被挡下的主数据行数** —— 合到出参里去,是因为界面不撒谎:
* 放行痕上写「合并 N 行」而其中几行根本没进表,那句话就是假的。
@@ -100,7 +120,13 @@ export type LedgerService = {
address: ObjectAddress;
key: Record<string, unknown>;
}): Promise<boolean>;
releaseWrite(taskId: TaskId): Promise<ReleaseWriteResult>;
/**
* 放行写。`scope` 见 `ReleaseScope`:不给 = 整单;给了 = 只并那几张表(数据工单确认那一道门),
* 并过的行留在草稿层打记号。**不是第四种写**,也不是绕门:同一条冲突检查 → 原子合并 → 抛事件的路,
* 只是门换成了那张卡的确认闸 —— 落哪几张表由发卡的人说了算(`gateLedgers`),调用方指不了别的表进来
* (清单没声明的表这一单本就写不进草稿层)。
*/
releaseWrite(taskId: TaskId, scope?: ReleaseScope): Promise<ReleaseWriteResult>;
/** 正式版查询 —— 跨任务只见过门的事实 */
query(query: LedgerQuery): Promise<LedgerPage>;
/** 任务视图 = 正式版 + 本任务草稿层 */
@@ -115,8 +141,10 @@ export type LedgerService = {
object: string;
key: Record<string, unknown>;
patch: Record<string, unknown>;
/** 已在数据工单确认那一道门上并进正式表(见 `ReleaseScope`);仍是本单写下的行 */
landed: boolean;
}>>;
/** 落地预检第五项:数据冲突检查 */
checkConflicts(taskId: TaskId): Promise<ConflictFinding[]>;
/** 落地预检第五项:数据冲突检查;`only` 给了就只查那几张表(与 `releaseWrite` 的 scope 同一口径) */
checkConflicts(taskId: TaskId, only?: readonly ObjectAddress[]): Promise<ConflictFinding[]>;
discardDrafts(taskId: TaskId): Promise<void>;
};
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* module: ledger —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
*/
export type { ConflictFinding, DraftWrite, LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, LedgerService, MaterializedObject, MergeConflict, MergeNotice, MergedObject, ObjectAddress, ObjectDeclaration, ReleaseWriteResult, SchemaApplyResult, SystemWrite, WriteKind, } from './contract.ts';
export type { ConflictFinding, DraftWrite, LedgerAddress, LedgerKey, LedgerPage, LedgerQuery, LedgerRow, LedgerService, MaterializedObject, MergeConflict, MergeNotice, MergedObject, ObjectAddress, ObjectDeclaration, ReleaseScope, ReleaseWriteResult, SchemaApplyResult, SystemWrite, WriteKind, } from './contract.ts';
export { DraftConflictError, ObjectNotMaterializedError, SchemaConflictError } from './contract.ts';
export type { Clock, DraftStore, EventPublisher, IdGenerator, LedgerStore, ObjectRef } from './port.ts';
export type { LedgerServiceDeps } from './internal/usecase/ledger-service.ts';
+9
View File
@@ -77,5 +77,14 @@ export type DraftStore = {
* (唯一能改的是那个数,改了别的判据又不对),13 次拒回、23 拍、234k token 才熔断。
*/
drop(taskId: string, ref: ObjectRef, key: LedgerKey): Promise<void>;
/**
* 给本单草稿层的一行打上「已落地」记号(2026-08-27,数据工单确认即入库)。
*
* 那一行刚经一道更早的门并进了正式表:条目**留着**(它仍是本单写下的行,
* 任务视图 / `mine:` / `_rows` 照旧认它),只是放行时不再写它。
* `baseline` 换成落地后正式表里的值 —— 之后再往这一行写一笔(记号随之擦掉),
* 冲突检查比的才是「我改之前表里是什么」,而不是落地前那份早已过时的基线。
*/
markLanded(taskId: string, ref: ObjectRef, key: LedgerKey, baseline: LedgerRow): Promise<void>;
clear(taskId: string): Promise<void>;
};
+21
View File
@@ -207,6 +207,8 @@ export type CardGateway = {
ownerId: StaffId;
humanCallCap: number;
workOrder: 'execution' | 'acceptance';
/** 这张卡挂在哪个计划项上 —— 引擎按它查剧本声明(如 `cardVariant`),不靠模型抄卡骨架;引擎自己发的卡(验收/兜底)没有项 */
itemId?: string | undefined;
card: Record<string, unknown>;
}): Promise<{
id: string;
@@ -258,9 +260,28 @@ export type CardGateway = {
v: string;
}>;
allowCustom?: boolean;
/** 这张卡挂在哪个计划项上 —— 组装层按它查剧本声明(`cardVariant: 'review'` = 摆成验收工单) */
itemId?: string | undefined;
/**
* **这张卡穿哪副面孔,引擎已经按剧本定好**(2026-08-27):模型没给 itemId、或给的项挂不上时,
* 组装层按 itemId 查剧本就查不到,卡就退成「需要你定」—— 而这一步的剧本明明写着 `cardVariant: 'review'`。
* 引擎知道当下在办哪一步,由它把面孔定下来递过去;组装层只在这里没给时才自己按 itemId 查。
*/
variant?: 'review' | undefined;
/** 人工验收清单(剧本 `acceptanceChecklist`)—— 审核卡上一条一枚「达标 / 不达标」开关 */
checklist?: ReadonlyArray<string> | undefined;
}): Promise<{
id: string;
}>;
/**
* **这一单还有哪几张卡在等人交回**(2026-08-27,用户:「你叫了人我都没填你就直接推进了」)。
* 每拍开头问一次:有卡没答,这一拍不问模型 —— 叫了人就等人;
* 任何多余的唤醒(迟到的回传事件、另一台后端的推进器)都推不动它。
*/
unanswered(taskId: TaskId): Promise<ReadonlyArray<{
id: string;
title: string;
}>>;
};
export type ListenerRegistrar = {
register(input: {
+2
View File
@@ -26,6 +26,8 @@ export type SubmitForReleaseInput = {
}>;
readonly deliverables: readonly DeliverableCheck[];
readonly criteria: readonly CriterionCheck[];
/** 人工验收清单(剧本声明,固定):卡上一条一枚开关,全部达标才能通过 */
readonly checklist?: ReadonlyArray<string>;
/** 报告可修订:AI 拟值已填的表单字段 */
readonly fields?: ReadonlyArray<Record<string, unknown>>;
/**
+4
View File
@@ -25,6 +25,8 @@ export type LedgerGateway = {
emittedEvents: number;
/** 试跑单被挡在正式表外的主数据行数(非试跑恒为 0)—— 放行痕要如实说 */
simulatedRows: number;
/** 早已在数据工单确认时入库、这次不再写的行数 —— 放行痕上分开说,「合并 N 行」不多报 */
landedEarlierRows: number;
/** 合并进正式版的行 —— 输出口发射的载荷取自这里 */
merged: ReadonlyArray<{
domain: string;
@@ -89,6 +91,8 @@ export type CardGateway = {
effect: 'read' | 'write';
detail: string;
}>;
/** 人工验收清单 —— 原样带上卡 */
checklist?: ReadonlyArray<string> | undefined;
/**
* 可修订字段 —— 验收报告「AI 已填好、可编辑」改的就是它们(带 `ai` 拟值与 `ledger` 落回地址)。
* **端口不声明它,值就在装配层被丢掉**:上游一路传下来,而 TypeScript 对条件展开
+5
View File
@@ -77,10 +77,15 @@ export type TaskService = {
/** 推进一拍:**预算在环外先扣先判**,再交范式引擎 */
tick(taskId: TaskId): Promise<TickOutcome>;
/** 事件驱动续跑 —— 挂起期间事件不丢,恢复即补投 */
/**
* 投递一条事件并推一拍。`occurredAt` = 事件发生时刻(信封的 occurredAt):人事件要拿它与任务的
* 最近推进时刻比 —— 回传那一刻已经 wake、推进器抢先跑了那一拍的,这条事件只是迟到的回声,不再推。
*/
deliver(taskId: TaskId, event: {
kind: 'data' | 'time' | 'human' | 'control';
ref: string;
payload: Record<string, unknown>;
occurredAt?: string;
}): Promise<TickOutcome>;
/** 控制三语义:暂停 / 叫停 / 终止 */
control(taskId: TaskId, semantic: ControlSemantic, by: StaffId, reason?: string): Promise<ControlOutcome>;