This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/modules/report/internal/domain/aggregate.ts
T
ChaiHaoandClaude Opus 5 a60acb2e02 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
2026-07-27 03:39:10 +08:00

162 lines
5.8 KiB
TypeScript

/**
* 汇报聚合与度量八指标 —— 纯投影(编码规范 §10.2;语义出处 OS 8.1 / 8.3)。
*
* 「口径全部挂在**归侧标签与事件流**上——零埋点,统计即查询。」
* 所以这里的输入只有两样:归侧过的日志行、任务的终态与纠正信号。没有第三种埋点。
*/
import type { AttributionSide, TerminalState } from '@contracts';
import { TERMINAL_STATE_LABEL } from '@contracts';
import type { Window } from './period.ts';
import { withinWindow } from './period.ts';
export type LogFact = {
readonly taskId: string;
readonly at: string;
readonly side: AttributionSide;
readonly title: string;
/** 人类工具发卡 —— 打扰率的计量对象(四卡合计) */
readonly isHumanToolCall: boolean;
/** 判断次数中由 DMN / 机检确定性求值的 */
readonly isDeterministicJudgement: boolean;
readonly isJudgement: boolean;
/** loop 断点:当下可为与目标所需交集为空 */
readonly isBreakpoint: boolean;
};
export type TaskFact = {
readonly taskId: string;
readonly title: string;
readonly activityId: string;
readonly startedAt: string;
readonly finishedAt?: string | undefined;
readonly terminal?: TerminalState | undefined;
/** 提交批次:每次放行提交 = 一个产出批次 */
readonly submissions: number;
/** 被纠正批次:驳回重做 / 驳回迭代 / 叫停 / 裁决推翻 */
readonly corrections: number;
readonly tokens: number;
readonly activeMs: number;
readonly humanCalls: number;
};
/** 度量八指标(OS 8.3)—— 自主率与纠正率必须成对读 */
export type Metrics = {
readonly autonomy: number;
readonly correction: number;
readonly effectiveAutonomy: number;
readonly interruption: number;
readonly breakpoint: number;
readonly dmnCoverage: number;
/** 单位成本:总消耗 ÷ **放行通过**的任务数 —— 分母只认成功终态 */
readonly unitCost: { readonly tokens: number; readonly minutes: number; readonly humanCalls: number };
/** 终态分布:五个终态的占比 */
readonly terminalMix: Readonly<Record<TerminalState, number>>;
};
const ratio = (num: number, den: number): number => (den === 0 ? 0 : num / den);
export function computeMetrics(logs: readonly LogFact[], tasks: readonly TaskFact[]): Metrics {
const machine = logs.filter((l) => l.side === 'machine').length;
const human = logs.filter((l) => l.side === 'human').length;
// 裁决与等待不进分母
const autonomy = ratio(machine, machine + human);
const submissions = tasks.reduce((s, t) => s + t.submissions, 0);
const corrections = tasks.reduce((s, t) => s + t.corrections, 0);
const correction = ratio(corrections, submissions);
const judgements = logs.filter((l) => l.isJudgement).length;
const deterministic = logs.filter((l) => l.isDeterministicJudgement).length;
const released = tasks.filter((t) => t.terminal === 'released');
const terminalCounts = tasks.reduce<Record<string, number>>((acc, t) => {
if (t.terminal !== undefined) acc[t.terminal] = (acc[t.terminal] ?? 0) + 1;
return acc;
}, {});
const terminalTotal = Object.values(terminalCounts).reduce((s, n) => s + n, 0);
const terminalMix = (Object.keys(TERMINAL_STATE_LABEL) as TerminalState[]).reduce(
(acc, state) => ({ ...acc, [state]: ratio(terminalCounts[state] ?? 0, terminalTotal) }),
{} as Record<TerminalState, number>,
);
return {
autonomy,
correction,
effectiveAutonomy: autonomy * (1 - correction),
interruption: ratio(
tasks.reduce((s, t) => s + t.humanCalls, 0),
tasks.length,
),
breakpoint: ratio(logs.filter((l) => l.isBreakpoint).length, logs.length),
dmnCoverage: ratio(deterministic, judgements),
unitCost: {
// 环是否真在产出:分母只认放行通过,否则「跑得多」会被算成「产出多」
tokens: ratio(
tasks.reduce((s, t) => s + t.tokens, 0),
released.length,
),
minutes: ratio(
tasks.reduce((s, t) => s + t.activeMs, 0) / 60_000,
released.length,
),
humanCalls: ratio(
tasks.reduce((s, t) => s + t.humanCalls, 0),
released.length,
),
},
terminalMix,
};
}
export type ReportSection = {
readonly taskId: string;
readonly title: string;
readonly activityId: string;
readonly terminalLabel?: string | undefined;
/** 该窗口内这单任务的日志摘句 —— 汇报正文的骨架,零 LLM 即可成篇 */
readonly lines: readonly string[];
};
export type ReportDraft = {
readonly sections: readonly ReportSection[];
readonly metrics: Metrics;
readonly counts: { readonly touched: number; readonly released: number; readonly logLines: number };
};
/**
* 汇报草案:按任务分段,段内按时间列日志摘句。
* 「零手工汇报」的含义是**员工不写**,不是「系统编」——所以草案的每一句都取自任务日志,
* 叙述性小结(若配了系统用途模型)是锦上添花,不是事实来源。
*/
export function draftReport(
logs: readonly LogFact[],
tasks: readonly TaskFact[],
window: Window,
): ReportDraft {
const inWindow = logs.filter((l) => withinWindow(l.at, window));
const touchedIds = new Set(inWindow.map((l) => l.taskId));
const touched = tasks.filter((t) => touchedIds.has(t.taskId));
const sections = touched.map<ReportSection>((t) => ({
taskId: t.taskId,
title: t.title,
activityId: t.activityId,
terminalLabel: t.terminal === undefined ? undefined : TERMINAL_STATE_LABEL[t.terminal],
lines: inWindow
.filter((l) => l.taskId === t.taskId)
.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0))
.map((l) => l.title),
}));
return {
sections,
metrics: computeMetrics(inWindow, touched),
counts: {
touched: touched.length,
released: touched.filter((t) => t.terminal === 'released').length,
logLines: inWindow.length,
},
};
}