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,26 @@
|
||||
# module: minute
|
||||
|
||||
职责: 纪要的沉淀与维护(团队主动上传);决议 → 登记触发起单
|
||||
不负责: 起单本身的流转(见 process —— 本模块经 `RegistrationGateway` 端口调它)、
|
||||
触发器配置(见 trigger —— 登记触发是三类触发之一,但纪要不配置触发器)
|
||||
|
||||
公开出口: index.ts
|
||||
- 类型:`MinuteService` `Minute` `MinuteResolution` `ResolutionState`
|
||||
`CreateMinuteInput` `RegisterInput`
|
||||
- 错误:`MinuteNotFoundError` `ResolutionAlreadyRegisteredError`
|
||||
- 端口类型:`MinuteStore` `RegistrationGateway`
|
||||
- 用例工厂:`createMinuteService(deps): MinuteService`
|
||||
- 纯函数:`newMinute` `addResolution` `markRegistered` `registeredWorkflowOf`
|
||||
- adapter:`createMinuteStore`;Drizzle 表:`minutes` `minuteResolutions`
|
||||
|
||||
层级: L8(编码规范 §4)
|
||||
依赖: @contracts;@shared/db;port:`RegistrationGateway`(process,L7,由 app 注入)
|
||||
被依赖: app(路由组装)
|
||||
|
||||
不变量(OS 2.1 / 3.1):
|
||||
- **纪要本身不产生责任**:没有负责人、没有终态、没有预算 —— 它是团队沉淀,不是任务
|
||||
- **唯一通往责任的路是登记起单**:决议经登记触发调起流程包,「**登记人即来源**」
|
||||
- **同一决议不重复起单**:已登记的决议不再迁移,由用例层抛
|
||||
`ResolutionAlreadyRegisteredError` —— 起单一刻已有一条带来源的工作流在跑
|
||||
- **只增不改**:正文可续、决议只增;纪要是沉淀,不是可覆写的文档
|
||||
- 登记不可撤销:要否掉那条流,走治理台的控制事件,而不是回改纪要
|
||||
@@ -0,0 +1,50 @@
|
||||
/** 纪要领域:只增不改 + 决议登记不重复 —— domain 测试零 mock(OS 2.1 / 3.1) */
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { addResolution, markRegistered, newMinute, registeredWorkflowOf } from '../internal/domain/minute.ts';
|
||||
|
||||
const base = newMinute({
|
||||
id: 'm1',
|
||||
title: '7 月报价例会',
|
||||
body: '讨论 Q3 报价口径',
|
||||
authorId: 's-pm',
|
||||
participants: ['s-pm', 's-bd'],
|
||||
occurredOn: '2026-07-27',
|
||||
resolutions: [{ id: 'r1', text: '统一 TYPE-B 的寻源时限为 3 天', ownerId: 's-prm' }],
|
||||
at: '2026-07-27T02:00:00.000Z',
|
||||
});
|
||||
|
||||
describe('纪要 —— 团队主动沉淀,本身不产生责任', () => {
|
||||
it('新建的决议一律待登记', () => {
|
||||
expect(base.resolutions.map((r) => r.state)).toEqual(['open']);
|
||||
expect(registeredWorkflowOf(base, 'r1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('决议只增不改 —— 追加不动既有条目', () => {
|
||||
const next = addResolution(base, { id: 'r2', text: '新增一条' }, '2026-07-27T03:00:00.000Z');
|
||||
expect(next.resolutions.map((r) => r.id)).toEqual(['r1', 'r2']);
|
||||
expect(next.resolutions[0]).toEqual(base.resolutions[0]);
|
||||
expect(next.updatedAt).toBe('2026-07-27T03:00:00.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('决议 → 登记起单:登记人即来源', () => {
|
||||
const at = '2026-07-27T04:00:00.000Z';
|
||||
const registered = markRegistered(base, 'r1', { workflowId: 'wf-9', byId: 's-pm', at })!;
|
||||
|
||||
it('登记落笔记下工作流与登记人', () => {
|
||||
expect(registered.resolutions[0]).toMatchObject({
|
||||
state: 'registered',
|
||||
workflowId: 'wf-9',
|
||||
registeredById: 's-pm',
|
||||
registeredAt: at,
|
||||
});
|
||||
});
|
||||
|
||||
it('已登记的决议可被查出它起的那条流 —— 用例层据此拒绝重复起单', () => {
|
||||
expect(registeredWorkflowOf(registered, 'r1')).toBe('wf-9');
|
||||
});
|
||||
|
||||
it('不存在的决议返回 undefined,不静默改别的条目', () => {
|
||||
expect(markRegistered(base, 'r-nope', { workflowId: 'wf-9', byId: 's-pm', at })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* module: minute —— 对外类型(编码规范 §10.1;语义出处 OS 2.1 / 3.1)。
|
||||
*
|
||||
* 纪要是工作台五要素里**唯一的主动动作**:团队主动维护上传的沉淀(会议纪要等)。
|
||||
* 它本身不产生责任;责任的产生只有一条路——**决议经登记触发转为工作流的启动**,
|
||||
* 「登记人即来源」。所以本模块的写面很薄,重头在「决议 → 登记起单」这一跳。
|
||||
*/
|
||||
import type { StaffId } from '@contracts';
|
||||
import type { Minute, MinuteResolution, ResolutionState } from './internal/domain/minute.ts';
|
||||
|
||||
export type { Minute, MinuteResolution, ResolutionState };
|
||||
|
||||
export class MinuteNotFoundError extends Error {
|
||||
constructor(readonly minuteId: string) {
|
||||
super(`纪要不存在:${minuteId}`);
|
||||
this.name = 'MinuteNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ResolutionAlreadyRegisteredError extends Error {
|
||||
constructor(
|
||||
readonly resolutionId: string,
|
||||
readonly workflowId: string,
|
||||
) {
|
||||
super(`决议 ${resolutionId} 已登记起单(工作流 ${workflowId})—— 同一决议不重复起单`);
|
||||
this.name = 'ResolutionAlreadyRegisteredError';
|
||||
}
|
||||
}
|
||||
|
||||
export type CreateMinuteInput = {
|
||||
readonly title: string;
|
||||
readonly body: string;
|
||||
readonly authorId: StaffId;
|
||||
/** 与会/相关人 —— 仅作沉淀标注,不产生责任 */
|
||||
readonly participants?: readonly StaffId[];
|
||||
readonly occurredOn: string;
|
||||
readonly resolutions?: ReadonlyArray<{ text: string; ownerId?: StaffId | undefined }>;
|
||||
};
|
||||
|
||||
export type RegisterInput = {
|
||||
readonly resolutionId: string;
|
||||
/** 登记人 —— 凡单必有来源,登记触发的来源就是他 */
|
||||
readonly byId: StaffId;
|
||||
readonly processPackageId: string;
|
||||
/** 按调用接口 schema 填写的输入(表单由 schema 自动渲染) */
|
||||
readonly data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type MinuteService = {
|
||||
list(limit?: number): Promise<Minute[]>;
|
||||
get(minuteId: string): Promise<Minute>;
|
||||
create(input: CreateMinuteInput): Promise<Minute>;
|
||||
/** 只增不改的编辑:正文可续,决议只增 —— 纪要是沉淀,不是可覆写的文档 */
|
||||
appendResolution(minuteId: string, text: string, ownerId?: StaffId): Promise<Minute>;
|
||||
|
||||
/** 决议 → 登记起单:登记人即来源;同一决议只起一次单 */
|
||||
register(input: RegisterInput): Promise<{ workflowId: string; resolutionId: string }>;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/** module: minute —— 唯一出口(编码规范 §10.1):纯再导出,不含实现。 */
|
||||
export type {
|
||||
CreateMinuteInput,
|
||||
Minute,
|
||||
MinuteResolution,
|
||||
MinuteService,
|
||||
RegisterInput,
|
||||
ResolutionState,
|
||||
} from './contract.ts';
|
||||
export { MinuteNotFoundError, ResolutionAlreadyRegisteredError } from './contract.ts';
|
||||
export { createMinuteService } from './internal/usecase/minute-service.ts';
|
||||
export { createMinuteStore } from './internal/adapter/minute-store.ts';
|
||||
export { minutes, minuteResolutions } from './internal/adapter/minute-schema.ts';
|
||||
export { newMinute, addResolution, markRegistered } from './internal/domain/minute.ts';
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* minute 的 sys schema 表(编码规范 §7 / ADR-002)。
|
||||
* 纪要是团队沉淀,存系统自持存储;决议单列一表 —— 它有状态(待登记 / 已登记),正文没有。
|
||||
*/
|
||||
import { index, jsonb, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { sys } from '@shared/db.ts';
|
||||
|
||||
export const minutes = sys.table(
|
||||
'minute',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
title: text('title').notNull(),
|
||||
body: text('body').notNull(),
|
||||
authorId: text('author_id').notNull(),
|
||||
participants: jsonb('participants').notNull().default([]),
|
||||
occurredOn: text('occurred_on').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index('minute_occurred_idx').on(t.occurredOn)],
|
||||
);
|
||||
|
||||
export const minuteResolutions = sys.table(
|
||||
'minute_resolution',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
minuteId: text('minute_id').notNull(),
|
||||
text: text('text').notNull(),
|
||||
ownerId: text('owner_id'),
|
||||
/** open | registered —— 登记不可撤销:起单一刻已有一条工作流在跑 */
|
||||
state: text('state').notNull().default('open'),
|
||||
workflowId: text('workflow_id'),
|
||||
registeredById: text('registered_by_id'),
|
||||
registeredAt: timestamp('registered_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [index('minute_resolution_minute_idx').on(t.minuteId)],
|
||||
);
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 纪要的领域形状与状态迁移 —— 零 IO、零框架(编码规范 §10.2)。
|
||||
*
|
||||
* 纪要是**团队主动沉淀**:不产生责任、没有终态、没有预算。
|
||||
* 唯一有状态的是它的决议:待登记 → 已登记(起单)。登记不可撤销,
|
||||
* 因为起单一刻就已经有一条工作流带着自己的来源在跑了。
|
||||
*/
|
||||
|
||||
export type ResolutionState = 'open' | 'registered';
|
||||
|
||||
export type MinuteResolution = {
|
||||
readonly id: string;
|
||||
readonly text: string;
|
||||
/** 决议的建议责任人 —— 仅是纪要里的记载,不是任务负责人 */
|
||||
readonly ownerId?: string | undefined;
|
||||
readonly state: ResolutionState;
|
||||
/** 已登记时:它起出来的那条工作流 */
|
||||
readonly workflowId?: string | undefined;
|
||||
readonly registeredById?: string | undefined;
|
||||
readonly registeredAt?: string | undefined;
|
||||
};
|
||||
|
||||
export type Minute = {
|
||||
readonly id: string;
|
||||
readonly title: string;
|
||||
readonly body: string;
|
||||
readonly authorId: string;
|
||||
readonly participants: readonly string[];
|
||||
/** 会议发生日(业务日期),与 createdAt 不是一回事 */
|
||||
readonly occurredOn: string;
|
||||
readonly resolutions: readonly MinuteResolution[];
|
||||
readonly createdAt: string;
|
||||
readonly updatedAt: string;
|
||||
};
|
||||
|
||||
export function newMinute(input: {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
authorId: string;
|
||||
participants: readonly string[];
|
||||
occurredOn: string;
|
||||
resolutions: ReadonlyArray<{ id: string; text: string; ownerId?: string | undefined }>;
|
||||
at: string;
|
||||
}): Minute {
|
||||
return {
|
||||
id: input.id,
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
authorId: input.authorId,
|
||||
participants: [...input.participants],
|
||||
occurredOn: input.occurredOn,
|
||||
resolutions: input.resolutions.map((r) => ({
|
||||
id: r.id,
|
||||
text: r.text,
|
||||
ownerId: r.ownerId,
|
||||
state: 'open' as const,
|
||||
})),
|
||||
createdAt: input.at,
|
||||
updatedAt: input.at,
|
||||
};
|
||||
}
|
||||
|
||||
/** 决议只增不改 —— 纪要是沉淀,不是可覆写的文档 */
|
||||
export function addResolution(
|
||||
minute: Minute,
|
||||
resolution: { id: string; text: string; ownerId?: string | undefined },
|
||||
at: string,
|
||||
): Minute {
|
||||
return {
|
||||
...minute,
|
||||
resolutions: [...minute.resolutions, { ...resolution, state: 'open' }],
|
||||
updatedAt: at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记起单的状态落笔。返回 `undefined` 表示该决议不在此纪要里;
|
||||
* 已登记的决议**不迁移**,由用例层抛「不重复起单」。
|
||||
*/
|
||||
export function markRegistered(
|
||||
minute: Minute,
|
||||
resolutionId: string,
|
||||
mark: { workflowId: string; byId: string; at: string },
|
||||
): Minute | undefined {
|
||||
const target = minute.resolutions.find((r) => r.id === resolutionId);
|
||||
if (target === undefined) return undefined;
|
||||
return {
|
||||
...minute,
|
||||
resolutions: minute.resolutions.map((r) =>
|
||||
r.id === resolutionId
|
||||
? {
|
||||
...r,
|
||||
state: 'registered' as const,
|
||||
workflowId: mark.workflowId,
|
||||
registeredById: mark.byId,
|
||||
registeredAt: mark.at,
|
||||
}
|
||||
: r,
|
||||
),
|
||||
updatedAt: mark.at,
|
||||
};
|
||||
}
|
||||
|
||||
/** 已登记的决议 —— 用例层据此拒绝重复起单 */
|
||||
export function registeredWorkflowOf(minute: Minute, resolutionId: string): string | undefined {
|
||||
const target = minute.resolutions.find((r) => r.id === resolutionId);
|
||||
return target?.state === 'registered' ? target.workflowId : undefined;
|
||||
}
|
||||
@@ -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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "@modules/minute", "private": true, "type": "module", "main": "index.ts" }
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* module: minute 的外部所需(编码规范 §10.1)。
|
||||
* 「port 声明本模块需要外界提供什么」—— 由 app 层注入,模块自己不认实现。
|
||||
*/
|
||||
import type { StaffId } from '@contracts';
|
||||
import type { Minute } from './internal/domain/minute.ts';
|
||||
|
||||
export type MinuteStore = {
|
||||
list(limit: number): Promise<Minute[]>;
|
||||
get(minuteId: string): Promise<Minute | undefined>;
|
||||
save(minute: Minute): Promise<void>;
|
||||
};
|
||||
|
||||
/** 登记起单 —— app 注入 `process`(L7):经调用接口调起流程包,不碰流程内部 */
|
||||
export type RegistrationGateway = {
|
||||
start(input: {
|
||||
processPackageId: string;
|
||||
subject: string;
|
||||
data: Record<string, unknown>;
|
||||
/** 来源:登记人 */
|
||||
byId: StaffId;
|
||||
}): Promise<{ workflowId: string }>;
|
||||
};
|
||||
|
||||
export type MinuteDeps = {
|
||||
readonly store: MinuteStore;
|
||||
readonly registration: RegistrationGateway;
|
||||
readonly ids: { next(): string };
|
||||
readonly clock: { now(): Date };
|
||||
};
|
||||
Reference in New Issue
Block a user