forked from ChaiHao/BPOS-MVP
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,79 @@
|
||||
/**
|
||||
* 纪要用例(编码规范 §10.2)。
|
||||
*
|
||||
* 「决议可经**登记触发**转为工作流的启动 —— 登记人即来源。」(OS 2.1 / 3.1)
|
||||
* 这是纪要与责任之间**唯一**的通路:纪要本身不产生责任。
|
||||
*/
|
||||
import type { MinuteService } from '../../contract.ts';
|
||||
import { MinuteNotFoundError, ResolutionAlreadyRegisteredError } from '../../contract.ts';
|
||||
import type { MinuteDeps } from '../../port.ts';
|
||||
import { addResolution, markRegistered, newMinute, registeredWorkflowOf } from '../domain/minute.ts';
|
||||
|
||||
export function createMinuteService(deps: MinuteDeps): MinuteService {
|
||||
async function require(minuteId: string) {
|
||||
const minute = await deps.store.get(minuteId);
|
||||
if (minute === undefined) throw new MinuteNotFoundError(minuteId);
|
||||
return minute;
|
||||
}
|
||||
|
||||
return {
|
||||
list: (limit = 50) => deps.store.list(limit),
|
||||
get: (minuteId) => require(minuteId),
|
||||
|
||||
async create(input) {
|
||||
const minute = newMinute({
|
||||
id: deps.ids.next(),
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
authorId: input.authorId,
|
||||
participants: input.participants ?? [],
|
||||
occurredOn: input.occurredOn,
|
||||
resolutions: (input.resolutions ?? []).map((r) => ({
|
||||
id: deps.ids.next(),
|
||||
text: r.text,
|
||||
ownerId: r.ownerId,
|
||||
})),
|
||||
at: deps.clock.now().toISOString(),
|
||||
});
|
||||
await deps.store.save(minute);
|
||||
return minute;
|
||||
},
|
||||
|
||||
async appendResolution(minuteId, text, ownerId) {
|
||||
const minute = await require(minuteId);
|
||||
const next = addResolution(
|
||||
minute,
|
||||
{ id: deps.ids.next(), text, ownerId },
|
||||
deps.clock.now().toISOString(),
|
||||
);
|
||||
await deps.store.save(next);
|
||||
return next;
|
||||
},
|
||||
|
||||
async register(input) {
|
||||
const all = await deps.store.list(200);
|
||||
const minute = all.find((m) => m.resolutions.some((r) => r.id === input.resolutionId));
|
||||
if (minute === undefined) throw new MinuteNotFoundError(input.resolutionId);
|
||||
|
||||
// 同一决议不重复起单:起单一刻就已经有一条带来源的工作流在跑了
|
||||
const already = registeredWorkflowOf(minute, input.resolutionId);
|
||||
if (already !== undefined) throw new ResolutionAlreadyRegisteredError(input.resolutionId, already);
|
||||
|
||||
const resolution = minute.resolutions.find((r) => r.id === input.resolutionId)!;
|
||||
const started = await deps.registration.start({
|
||||
processPackageId: input.processPackageId,
|
||||
subject: resolution.text,
|
||||
data: input.data,
|
||||
byId: input.byId,
|
||||
});
|
||||
|
||||
const next = markRegistered(minute, input.resolutionId, {
|
||||
workflowId: started.workflowId,
|
||||
byId: input.byId,
|
||||
at: deps.clock.now().toISOString(),
|
||||
})!;
|
||||
await deps.store.save(next);
|
||||
return { workflowId: started.workflowId, resolutionId: input.resolutionId };
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user