/** * 汇报聚合与度量八指标 —— 纯投影(编码规范 §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>; }; 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>((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, ); 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((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, }, }; }