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,110 @@
|
||||
/** minute 的持久化(编码规范 §10.2:框架只许出现在 adapter)。 */
|
||||
import { asc, desc, eq, inArray } from 'drizzle-orm';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { Minute, MinuteResolution, ResolutionState } from '../domain/minute.ts';
|
||||
import type { MinuteStore } from '../../port.ts';
|
||||
import { minuteResolutions, minutes } from './minute-schema.ts';
|
||||
|
||||
type MinuteRow = typeof minutes.$inferSelect;
|
||||
type ResolutionRow = typeof minuteResolutions.$inferSelect;
|
||||
|
||||
const toResolution = (row: ResolutionRow): MinuteResolution => ({
|
||||
id: row.id,
|
||||
text: row.text,
|
||||
ownerId: row.ownerId ?? undefined,
|
||||
state: row.state as ResolutionState,
|
||||
workflowId: row.workflowId ?? undefined,
|
||||
registeredById: row.registeredById ?? undefined,
|
||||
registeredAt: row.registeredAt?.toISOString(),
|
||||
});
|
||||
|
||||
const toMinute = (row: MinuteRow, resolutions: readonly ResolutionRow[]): Minute => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
body: row.body,
|
||||
authorId: row.authorId,
|
||||
participants: row.participants as string[],
|
||||
occurredOn: row.occurredOn,
|
||||
resolutions: resolutions.map(toResolution),
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
export function createMinuteStore(db: Db): MinuteStore {
|
||||
async function loadResolutions(minuteIds: readonly string[]): Promise<Map<string, ResolutionRow[]>> {
|
||||
if (minuteIds.length === 0) return new Map();
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(minuteResolutions)
|
||||
.where(inArray(minuteResolutions.minuteId, [...minuteIds]))
|
||||
.orderBy(asc(minuteResolutions.id));
|
||||
const byMinute = new Map<string, ResolutionRow[]>();
|
||||
for (const row of rows) {
|
||||
const list = byMinute.get(row.minuteId) ?? [];
|
||||
list.push(row);
|
||||
byMinute.set(row.minuteId, list);
|
||||
}
|
||||
return byMinute;
|
||||
}
|
||||
|
||||
return {
|
||||
async list(limit) {
|
||||
const rows = await db.select().from(minutes).orderBy(desc(minutes.occurredOn)).limit(limit);
|
||||
const byMinute = await loadResolutions(rows.map((r) => r.id));
|
||||
return rows.map((row) => toMinute(row, byMinute.get(row.id) ?? []));
|
||||
},
|
||||
|
||||
async get(minuteId) {
|
||||
const [row] = await db.select().from(minutes).where(eq(minutes.id, minuteId)).limit(1);
|
||||
if (row === undefined) return undefined;
|
||||
const byMinute = await loadResolutions([minuteId]);
|
||||
return toMinute(row, byMinute.get(minuteId) ?? []);
|
||||
},
|
||||
|
||||
async save(minute) {
|
||||
// 纪要与其决议同事务落笔:决议的登记状态不得与纪要正文脱节
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.insert(minutes)
|
||||
.values({
|
||||
id: minute.id,
|
||||
title: minute.title,
|
||||
body: minute.body,
|
||||
authorId: minute.authorId,
|
||||
participants: [...minute.participants],
|
||||
occurredOn: minute.occurredOn,
|
||||
createdAt: new Date(minute.createdAt),
|
||||
updatedAt: new Date(minute.updatedAt),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: minutes.id,
|
||||
set: { title: minute.title, body: minute.body, updatedAt: new Date(minute.updatedAt) },
|
||||
});
|
||||
|
||||
for (const r of minute.resolutions) {
|
||||
await tx
|
||||
.insert(minuteResolutions)
|
||||
.values({
|
||||
id: r.id,
|
||||
minuteId: minute.id,
|
||||
text: r.text,
|
||||
ownerId: r.ownerId ?? null,
|
||||
state: r.state,
|
||||
workflowId: r.workflowId ?? null,
|
||||
registeredById: r.registeredById ?? null,
|
||||
registeredAt: r.registeredAt === undefined ? null : new Date(r.registeredAt),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: minuteResolutions.id,
|
||||
set: {
|
||||
state: r.state,
|
||||
workflowId: r.workflowId ?? null,
|
||||
registeredById: r.registeredById ?? null,
|
||||
registeredAt: r.registeredAt === undefined ? null : new Date(r.registeredAt),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user