M6(3/n): minute 与 report —— L8 两个模块补齐

minute(纪要):工作台五要素里唯一的主动动作。
  纪要本身不产生责任,唯一通往责任的路是决议经登记触发起单 —— 登记人即来源。
  正文可续、决议只增;同一决议不重复起单(起单一刻已有一条带来源的流在跑)。

report(汇报):日/周/月/年对任务日志的聚合投影,零手工汇报。
  未提交的草案每次取回都重算 —— 汇报是投影不是账本,提交那刻才固定为事实快照。
  度量八指标落在这里(§4 注:度量是只读投影,不设独立模块):
  自主率 / 纠正率 / 有效自主率 / 打扰率 / 断点率 / DMN 覆盖率 / 单位成本 / 终态分布。
  口径全部挂归侧标签与事件流,零埋点;单位成本的分母只认放行通过。
  周窗口以周一为首日,按 UTC 切分(单组织单时区)。

FactSource 只读 memory 与 task,一行都不另记 —— 五要素同源。

gates: tsc / eslint / depcruise(305 模块)/ api-report / 377 测试 全绿
日报已在真实系统上由 A1 任务日志聚合出来,含治理干预两行

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 03:39:10 +08:00
co-authored by Claude Opus 5
parent a9cd6d10c7
commit a60acb2e02
37 changed files with 5495 additions and 2 deletions
+100
View File
@@ -0,0 +1,100 @@
/**
* 工作台后两个要素的装配:纪要(minute)与汇报(report)—— L8。
*
* 「五要素同源 —— 做事与留痕是同一个动作:五要素都是同一份过程事实
* (任务记忆与工作流事件)在员工视角下的投影。」(OS 2.1)
* 所以这里的 `FactSource` **只读** memory 与 task,一行都不另记。
*/
import type { StaffId } from '@contracts';
import type { MemoryService } from '@modules/memory';
import type { MinuteService } from '@modules/minute';
import { createMinuteService, createMinuteStore } from '@modules/minute';
import type { OrgService } from '@modules/org';
import type { ProcessService } from '@modules/process';
import type { LogFact, ReportService, TaskFact } from '@modules/report';
import { createReportService, createReportStore } from '@modules/report';
import type { TaskService } from '@modules/task';
import type { Db } from '@shared/db.ts';
export type DeskElementDeps = {
readonly db: Db;
readonly memory: MemoryService;
readonly task: TaskService;
readonly org: OrgService;
readonly process: ProcessService;
readonly ids: { next(): string };
readonly clock: { now(): Date };
};
export function wireDeskElements(deps: DeskElementDeps): { minute: MinuteService; report: ReportService } {
const minute = createMinuteService({
store: createMinuteStore(deps.db),
registration: {
// 登记触发:凡单必有来源 —— 纪要起单的来源就是登记人
start: async (input) => {
const started = await deps.process.start({
packageId: input.processPackageId,
subject: input.subject,
data: input.data,
source: { kind: 'registration', ref: input.processPackageId, by: input.byId },
});
return { workflowId: started.id };
},
},
ids: deps.ids,
clock: deps.clock,
});
/** 归侧日志行 → 度量事实。判断类与断点由日志标题的确定形态识别,不另设埋点 */
const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => ({
taskId,
at: line.at,
side: line.side,
title: line.title,
isHumanToolCall: line.title.startsWith('发出:'),
// DMN / 机检的确定性求值在日志里是「求值:」;LLM 的判断落在「调用:」
isDeterministicJudgement: line.title.startsWith('求值:'),
isJudgement: line.title.startsWith('求值:') || line.title.startsWith('调用:'),
isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'),
});
const report = createReportService({
store: createReportStore(deps.db),
facts: {
async logsOf(staffId, from, to) {
const owners = staffId === undefined ? (await deps.org.listStaff()).staff.map((s) => s.id) : [staffId];
const nested = await Promise.all(owners.map((id) => deps.task.listByOwner(id)));
const tasks = nested.flat();
const perTask = await Promise.all(
tasks.map(async (t) => (await deps.memory.taskLog(t.id)).map((l) => toLogFact(t.id, l))),
);
return perTask.flat().filter((l) => l.at >= from && l.at < to);
},
async tasksOf(staffId) {
const owners = staffId === undefined ? (await deps.org.listStaff()).staff.map((s) => s.id) : [staffId];
const nested = await Promise.all(owners.map((id) => deps.task.listByOwner(id)));
return nested.flat().map<TaskFact>((t) => ({
taskId: t.id,
title: t.title,
activityId: t.activityId,
startedAt: t.startedAt,
finishedAt: t.finishedAt,
terminal: t.terminal,
// 提交批次 = 放行提交次数;放行完成必有一次提交,未终态的以已发生的放行计
submissions: t.terminal === 'released' ? 1 : 0,
// 被纠正批次:驳回重做 / 驳回迭代 / 叫停 / 裁决推翻 —— 非放行终态即计一次
corrections: t.terminal !== undefined && t.terminal !== 'released' ? 1 : 0,
tokens: t.used.tokens,
activeMs: t.used.activeMs,
humanCalls: t.used.humanCalls,
}));
},
},
ids: deps.ids,
clock: deps.clock,
});
return { minute, report };
}
export type { StaffId };