Archived
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:
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 汇报聚合与度量八指标 —— 纯投影(编码规范 §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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user