Archived
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
162 lines
5.8 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
}
|