From a60acb2e0209a96ffbefa8134dbae52c3ee5cb4f Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Mon, 27 Jul 2026 03:39:10 +0800 Subject: [PATCH] =?UTF-8?q?M6(3/n):=20minute=20=E4=B8=8E=20report=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20L8=20=E4=B8=A4=E4=B8=AA=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../apps/api/src/modules/minute/contract.d.ts | 51 + .../apps/api/src/modules/minute/index.d.ts | 7 + .../apps/api/src/modules/minute/port.d.ts | 33 + .../apps/api/src/modules/report/contract.d.ts | 41 + .../apps/api/src/modules/report/index.d.ts | 10 + .../apps/api/src/modules/report/port.d.ts | 29 + .../apps/api/src/modules/task/port.d.ts | 3 +- .../packages/contracts/src/api/workbench.d.ts | 379 ++ apps/api/src/app/container.ts | 18 + apps/api/src/app/routes/desk-elements.ts | 204 ++ apps/api/src/app/server.ts | 2 + apps/api/src/app/wiring/desk-elements.ts | 100 + apps/api/src/modules/minute/CONTRACT.md | 26 + .../modules/minute/__tests__/minute.test.ts | 50 + apps/api/src/modules/minute/contract.ts | 58 + apps/api/src/modules/minute/index.ts | 14 + .../minute/internal/adapter/minute-schema.ts | 37 + .../minute/internal/adapter/minute-store.ts | 110 + .../modules/minute/internal/domain/minute.ts | 109 + .../minute/internal/usecase/minute-service.ts | 79 + apps/api/src/modules/minute/package.json | 1 + apps/api/src/modules/minute/port.ts | 30 + apps/api/src/modules/report/CONTRACT.md | 28 + .../modules/report/__tests__/period.test.ts | 144 + apps/api/src/modules/report/contract.ts | 50 + apps/api/src/modules/report/index.ts | 10 + .../report/internal/adapter/report-schema.ts | 23 + .../report/internal/adapter/report-store.ts | 83 + .../report/internal/domain/aggregate.ts | 161 + .../modules/report/internal/domain/period.ts | 68 + .../report/internal/usecase/report-service.ts | 70 + apps/api/src/modules/report/package.json | 1 + apps/api/src/modules/report/port.ts | 28 + db/migrations/0008_strange_quasar.sql | 38 + db/migrations/meta/0008_snapshot.json | 3260 +++++++++++++++++ db/migrations/meta/_journal.json | 7 + packages/contracts/src/api/workbench.ts | 135 +- 37 files changed, 5495 insertions(+), 2 deletions(-) create mode 100644 api-report/apps/api/src/modules/minute/contract.d.ts create mode 100644 api-report/apps/api/src/modules/minute/index.d.ts create mode 100644 api-report/apps/api/src/modules/minute/port.d.ts create mode 100644 api-report/apps/api/src/modules/report/contract.d.ts create mode 100644 api-report/apps/api/src/modules/report/index.d.ts create mode 100644 api-report/apps/api/src/modules/report/port.d.ts create mode 100644 apps/api/src/app/routes/desk-elements.ts create mode 100644 apps/api/src/app/wiring/desk-elements.ts create mode 100644 apps/api/src/modules/minute/CONTRACT.md create mode 100644 apps/api/src/modules/minute/__tests__/minute.test.ts create mode 100644 apps/api/src/modules/minute/contract.ts create mode 100644 apps/api/src/modules/minute/index.ts create mode 100644 apps/api/src/modules/minute/internal/adapter/minute-schema.ts create mode 100644 apps/api/src/modules/minute/internal/adapter/minute-store.ts create mode 100644 apps/api/src/modules/minute/internal/domain/minute.ts create mode 100644 apps/api/src/modules/minute/internal/usecase/minute-service.ts create mode 100644 apps/api/src/modules/minute/package.json create mode 100644 apps/api/src/modules/minute/port.ts create mode 100644 apps/api/src/modules/report/CONTRACT.md create mode 100644 apps/api/src/modules/report/__tests__/period.test.ts create mode 100644 apps/api/src/modules/report/contract.ts create mode 100644 apps/api/src/modules/report/index.ts create mode 100644 apps/api/src/modules/report/internal/adapter/report-schema.ts create mode 100644 apps/api/src/modules/report/internal/adapter/report-store.ts create mode 100644 apps/api/src/modules/report/internal/domain/aggregate.ts create mode 100644 apps/api/src/modules/report/internal/domain/period.ts create mode 100644 apps/api/src/modules/report/internal/usecase/report-service.ts create mode 100644 apps/api/src/modules/report/package.json create mode 100644 apps/api/src/modules/report/port.ts create mode 100644 db/migrations/0008_strange_quasar.sql create mode 100644 db/migrations/meta/0008_snapshot.json diff --git a/api-report/apps/api/src/modules/minute/contract.d.ts b/api-report/apps/api/src/modules/minute/contract.d.ts new file mode 100644 index 0000000..1bef51e --- /dev/null +++ b/api-report/apps/api/src/modules/minute/contract.d.ts @@ -0,0 +1,51 @@ +/** + * 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 declare class MinuteNotFoundError extends Error { + readonly minuteId: string; + constructor(minuteId: string); +} +export declare class ResolutionAlreadyRegisteredError extends Error { + readonly resolutionId: string; + readonly workflowId: string; + constructor(resolutionId: string, workflowId: string); +} +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; +}; +export type MinuteService = { + list(limit?: number): Promise; + get(minuteId: string): Promise; + create(input: CreateMinuteInput): Promise; + /** 只增不改的编辑:正文可续,决议只增 —— 纪要是沉淀,不是可覆写的文档 */ + appendResolution(minuteId: string, text: string, ownerId?: StaffId): Promise; + /** 决议 → 登记起单:登记人即来源;同一决议只起一次单 */ + register(input: RegisterInput): Promise<{ + workflowId: string; + resolutionId: string; + }>; +}; diff --git a/api-report/apps/api/src/modules/minute/index.d.ts b/api-report/apps/api/src/modules/minute/index.d.ts new file mode 100644 index 0000000..135753d --- /dev/null +++ b/api-report/apps/api/src/modules/minute/index.d.ts @@ -0,0 +1,7 @@ +/** 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'; diff --git a/api-report/apps/api/src/modules/minute/port.d.ts b/api-report/apps/api/src/modules/minute/port.d.ts new file mode 100644 index 0000000..5df3251 --- /dev/null +++ b/api-report/apps/api/src/modules/minute/port.d.ts @@ -0,0 +1,33 @@ +/** + * 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; + get(minuteId: string): Promise; + save(minute: Minute): Promise; +}; +/** 登记起单 —— app 注入 `process`(L7):经调用接口调起流程包,不碰流程内部 */ +export type RegistrationGateway = { + start(input: { + processPackageId: string; + subject: string; + data: Record; + /** 来源:登记人 */ + byId: StaffId; + }): Promise<{ + workflowId: string; + }>; +}; +export type MinuteDeps = { + readonly store: MinuteStore; + readonly registration: RegistrationGateway; + readonly ids: { + next(): string; + }; + readonly clock: { + now(): Date; + }; +}; diff --git a/api-report/apps/api/src/modules/report/contract.d.ts b/api-report/apps/api/src/modules/report/contract.d.ts new file mode 100644 index 0000000..8233a84 --- /dev/null +++ b/api-report/apps/api/src/modules/report/contract.d.ts @@ -0,0 +1,41 @@ +/** + * module: report —— 对外类型(编码规范 §10.1;语义出处 OS 2.1 / 8.1 / 8.3)。 + * + * 汇报是**系统代劳的自动产出**:由本人各任务的任务日志聚合生成,确认即提交,零手工汇报。 + * 度量八指标是任务日志与终态之上的**只读投影**,不设独立模块(§4 注)。 + */ +import type { StaffId } from '@contracts'; +import type { Metrics, ReportDraft, ReportSection } from './internal/domain/aggregate.ts'; +import type { ReportPeriod, Window } from './internal/domain/period.ts'; +export type { Metrics, ReportDraft, ReportPeriod, ReportSection, Window }; +export declare class ReportNotFoundError extends Error { + readonly reportId: string; + constructor(reportId: string); +} +export declare class ReportAlreadySubmittedError extends Error { + readonly reportId: string; + constructor(reportId: string); +} +/** 汇报单:草案由系统生成,员工确认即提交 */ +export type Report = { + readonly id: string; + readonly staffId: StaffId; + readonly period: ReportPeriod; + readonly title: string; + readonly window: Window; + readonly draft: ReportDraft; + /** 员工可在确认前补一段话 —— 唯一的手工位 */ + readonly note?: string | undefined; + readonly submittedAt?: string | undefined; + readonly generatedAt: string; +}; +export type ReportService = { + /** 生成(或取回)某人某周期的汇报草案 —— 未提交的重取即重算,提交后固定 */ + draft(staffId: StaffId, period: ReportPeriod, anchor?: Date): Promise; + list(staffId: StaffId, limit?: number): Promise; + get(reportId: string): Promise; + /** 确认即提交 */ + submit(reportId: string, note?: string): Promise; + /** 度量八指标 —— 治理台记忆区的仪表读数,同源同口径 */ + metrics(staffId: StaffId | undefined, period: ReportPeriod, anchor?: Date): Promise; +}; diff --git a/api-report/apps/api/src/modules/report/index.d.ts b/api-report/apps/api/src/modules/report/index.d.ts new file mode 100644 index 0000000..ae06d2c --- /dev/null +++ b/api-report/apps/api/src/modules/report/index.d.ts @@ -0,0 +1,10 @@ +/** module: report —— 唯一出口(编码规范 §10.1):纯再导出,不含实现。 */ +export type { Metrics, Report, ReportDraft, ReportPeriod, ReportSection, ReportService, Window } from './contract.ts'; +export { ReportAlreadySubmittedError, ReportNotFoundError } from './contract.ts'; +export type { FactSource, ReportStore, ReportDeps } from './port.ts'; +export type { LogFact, TaskFact } from './internal/domain/aggregate.ts'; +export { computeMetrics, draftReport } from './internal/domain/aggregate.ts'; +export { REPORT_PERIODS, periodLabel, windowOf } from './internal/domain/period.ts'; +export { createReportService } from './internal/usecase/report-service.ts'; +export { createReportStore } from './internal/adapter/report-store.ts'; +export { reports } from './internal/adapter/report-schema.ts'; diff --git a/api-report/apps/api/src/modules/report/port.d.ts b/api-report/apps/api/src/modules/report/port.d.ts new file mode 100644 index 0000000..2c85c82 --- /dev/null +++ b/api-report/apps/api/src/modules/report/port.d.ts @@ -0,0 +1,29 @@ +/** + * module: report 的外部所需(编码规范 §10.1)。 + * 汇报**不自己记账**:事实全部来自 memory(任务日志)与 task(终态 / 预算 / 纠正信号)。 + */ +import type { StaffId } from '@contracts'; +import type { Report } from './contract.ts'; +import type { LogFact, TaskFact } from './internal/domain/aggregate.ts'; +export type ReportStore = { + list(staffId: StaffId, limit: number): Promise; + get(reportId: string): Promise; + findOpen(staffId: StaffId, period: string, windowFrom: string): Promise; + save(report: Report): Promise; +}; +/** 事实源 —— app 注入 `memory`(L2)与 `task`(L6);report 在 L8,只读不写 */ +export type FactSource = { + /** 某人(或全员)在窗口内的归侧日志行 */ + logsOf(staffId: StaffId | undefined, from: string, to: string): Promise; + tasksOf(staffId: StaffId | undefined): Promise; +}; +export type ReportDeps = { + readonly store: ReportStore; + readonly facts: FactSource; + readonly ids: { + next(): string; + }; + readonly clock: { + now(): Date; + }; +}; diff --git a/api-report/apps/api/src/modules/task/port.d.ts b/api-report/apps/api/src/modules/task/port.d.ts index 9541854..889204c 100644 --- a/api-report/apps/api/src/modules/task/port.d.ts +++ b/api-report/apps/api/src/modules/task/port.d.ts @@ -56,7 +56,8 @@ export type ParadigmEngine = { export type MemoryRecorder = { append(input: { taskId: TaskId; - kind: 'engine-transition' | 'terminal' | 'observation'; + /** control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧 */ + kind: 'engine-transition' | 'terminal' | 'observation' | 'control'; title: string; detail?: Record; }): Promise; diff --git a/api-report/packages/contracts/src/api/workbench.d.ts b/api-report/packages/contracts/src/api/workbench.d.ts index 3240f95..13cfd53 100644 --- a/api-report/packages/contracts/src/api/workbench.d.ts +++ b/api-report/packages/contracts/src/api/workbench.d.ts @@ -2024,3 +2024,382 @@ export declare const listMessagesRoute: { }, z.core.$strip>>; }, z.core.$strip>; }; +export declare const minuteResolutionSchema: z.ZodObject<{ + id: z.ZodString; + text: z.ZodString; + ownerId: z.ZodOptional>; + state: z.ZodEnum<{ + open: "open"; + registered: "registered"; + }>; + workflowId: z.ZodOptional; + registeredById: z.ZodOptional>; + registeredAt: z.ZodOptional; +}, z.core.$strip>; +export declare const minuteSchema: z.ZodObject<{ + id: z.ZodString; + title: z.ZodString; + body: z.ZodString; + authorId: z.core.$ZodBranded; + authorName: z.ZodString; + participants: z.ZodArray; + occurredOn: z.ZodString; + resolutions: z.ZodArray>; + state: z.ZodEnum<{ + open: "open"; + registered: "registered"; + }>; + workflowId: z.ZodOptional; + registeredById: z.ZodOptional>; + registeredAt: z.ZodOptional; + }, z.core.$strip>>; + createdAt: z.ZodString; + updatedAt: z.ZodString; +}, z.core.$strip>; +export type MinuteView = z.infer; +export declare const listMinutesRoute: { + readonly method: "GET"; + readonly path: "/api/minutes"; + readonly output: z.ZodObject<{ + minutes: z.ZodArray; + authorName: z.ZodString; + participants: z.ZodArray; + occurredOn: z.ZodString; + resolutions: z.ZodArray>; + state: z.ZodEnum<{ + open: "open"; + registered: "registered"; + }>; + workflowId: z.ZodOptional; + registeredById: z.ZodOptional>; + registeredAt: z.ZodOptional; + }, z.core.$strip>>; + createdAt: z.ZodString; + updatedAt: z.ZodString; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +export declare const createMinuteRoute: { + readonly method: "POST"; + readonly path: "/api/minutes"; + readonly body: z.ZodObject<{ + title: z.ZodString; + body: z.ZodString; + occurredOn: z.ZodString; + participants: z.ZodDefault>>; + resolutions: z.ZodDefault>; + }, z.core.$strip>>>; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.ZodString; + title: z.ZodString; + body: z.ZodString; + authorId: z.core.$ZodBranded; + authorName: z.ZodString; + participants: z.ZodArray; + occurredOn: z.ZodString; + resolutions: z.ZodArray>; + state: z.ZodEnum<{ + open: "open"; + registered: "registered"; + }>; + workflowId: z.ZodOptional; + registeredById: z.ZodOptional>; + registeredAt: z.ZodOptional; + }, z.core.$strip>>; + createdAt: z.ZodString; + updatedAt: z.ZodString; + }, z.core.$strip>; +}; +export declare const appendResolutionRoute: { + readonly method: "POST"; + readonly path: "/api/minutes/:minuteId/resolutions"; + readonly params: z.ZodObject<{ + minuteId: z.ZodString; + }, z.core.$strip>; + readonly body: z.ZodObject<{ + text: z.ZodString; + ownerId: z.ZodOptional>; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.ZodString; + title: z.ZodString; + body: z.ZodString; + authorId: z.core.$ZodBranded; + authorName: z.ZodString; + participants: z.ZodArray; + occurredOn: z.ZodString; + resolutions: z.ZodArray>; + state: z.ZodEnum<{ + open: "open"; + registered: "registered"; + }>; + workflowId: z.ZodOptional; + registeredById: z.ZodOptional>; + registeredAt: z.ZodOptional; + }, z.core.$strip>>; + createdAt: z.ZodString; + updatedAt: z.ZodString; + }, z.core.$strip>; +}; +/** 决议 → 登记起单:登记人即来源 */ +export declare const registerResolutionRoute: { + readonly method: "POST"; + readonly path: "/api/minutes/resolutions/:resolutionId/register"; + readonly params: z.ZodObject<{ + resolutionId: z.ZodString; + }, z.core.$strip>; + readonly body: z.ZodObject<{ + processPackageId: z.ZodString; + data: z.ZodRecord; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + workflowId: z.ZodString; + resolutionId: z.ZodString; + }, z.core.$strip>; +}; +export declare const reportPeriodSchema: z.ZodEnum<{ + day: "day"; + week: "week"; + month: "month"; + year: "year"; +}>; +export type ReportPeriodName = z.infer; +export declare const REPORT_PERIOD_LABEL: Readonly>; +export declare const reportMetricsSchema: z.ZodObject<{ + autonomy: z.ZodNumber; + correction: z.ZodNumber; + effectiveAutonomy: z.ZodNumber; + interruption: z.ZodNumber; + breakpoint: z.ZodNumber; + dmnCoverage: z.ZodNumber; + unitCost: z.ZodObject<{ + tokens: z.ZodNumber; + minutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + terminalMix: z.ZodRecord; +}, z.core.$strip>; +export declare const reportSchema: z.ZodObject<{ + id: z.ZodString; + period: z.ZodEnum<{ + day: "day"; + week: "week"; + month: "month"; + year: "year"; + }>; + title: z.ZodString; + window: z.ZodObject<{ + from: z.ZodString; + to: z.ZodString; + }, z.core.$strip>; + sections: z.ZodArray; + lines: z.ZodArray; + }, z.core.$strip>>; + metrics: z.ZodObject<{ + autonomy: z.ZodNumber; + correction: z.ZodNumber; + effectiveAutonomy: z.ZodNumber; + interruption: z.ZodNumber; + breakpoint: z.ZodNumber; + dmnCoverage: z.ZodNumber; + unitCost: z.ZodObject<{ + tokens: z.ZodNumber; + minutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + terminalMix: z.ZodRecord; + }, z.core.$strip>; + counts: z.ZodObject<{ + touched: z.ZodNumber; + released: z.ZodNumber; + logLines: z.ZodNumber; + }, z.core.$strip>; + note: z.ZodOptional; + submittedAt: z.ZodOptional; + generatedAt: z.ZodString; +}, z.core.$strip>; +export type ReportView = z.infer; +/** 未提交的草案每次取回都重算 —— 汇报是投影,不是账本 */ +export declare const draftReportRoute: { + readonly method: "GET"; + readonly path: "/api/reports/:period"; + readonly params: z.ZodObject<{ + period: z.ZodEnum<{ + day: "day"; + week: "week"; + month: "month"; + year: "year"; + }>; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.ZodString; + period: z.ZodEnum<{ + day: "day"; + week: "week"; + month: "month"; + year: "year"; + }>; + title: z.ZodString; + window: z.ZodObject<{ + from: z.ZodString; + to: z.ZodString; + }, z.core.$strip>; + sections: z.ZodArray; + lines: z.ZodArray; + }, z.core.$strip>>; + metrics: z.ZodObject<{ + autonomy: z.ZodNumber; + correction: z.ZodNumber; + effectiveAutonomy: z.ZodNumber; + interruption: z.ZodNumber; + breakpoint: z.ZodNumber; + dmnCoverage: z.ZodNumber; + unitCost: z.ZodObject<{ + tokens: z.ZodNumber; + minutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + terminalMix: z.ZodRecord; + }, z.core.$strip>; + counts: z.ZodObject<{ + touched: z.ZodNumber; + released: z.ZodNumber; + logLines: z.ZodNumber; + }, z.core.$strip>; + note: z.ZodOptional; + submittedAt: z.ZodOptional; + generatedAt: z.ZodString; + }, z.core.$strip>; +}; +export declare const listReportsRoute: { + readonly method: "GET"; + readonly path: "/api/reports"; + readonly output: z.ZodObject<{ + reports: z.ZodArray; + title: z.ZodString; + window: z.ZodObject<{ + from: z.ZodString; + to: z.ZodString; + }, z.core.$strip>; + sections: z.ZodArray; + lines: z.ZodArray; + }, z.core.$strip>>; + metrics: z.ZodObject<{ + autonomy: z.ZodNumber; + correction: z.ZodNumber; + effectiveAutonomy: z.ZodNumber; + interruption: z.ZodNumber; + breakpoint: z.ZodNumber; + dmnCoverage: z.ZodNumber; + unitCost: z.ZodObject<{ + tokens: z.ZodNumber; + minutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + terminalMix: z.ZodRecord; + }, z.core.$strip>; + counts: z.ZodObject<{ + touched: z.ZodNumber; + released: z.ZodNumber; + logLines: z.ZodNumber; + }, z.core.$strip>; + note: z.ZodOptional; + submittedAt: z.ZodOptional; + generatedAt: z.ZodString; + }, z.core.$strip>>; + }, z.core.$strip>; +}; +/** 确认即提交,提交后不再改 */ +export declare const submitReportRoute: { + readonly method: "POST"; + readonly path: "/api/reports/:reportId/submit"; + readonly params: z.ZodObject<{ + reportId: z.ZodString; + }, z.core.$strip>; + readonly body: z.ZodObject<{ + note: z.ZodOptional; + }, z.core.$strip>; + readonly output: z.ZodObject<{ + id: z.ZodString; + period: z.ZodEnum<{ + day: "day"; + week: "week"; + month: "month"; + year: "year"; + }>; + title: z.ZodString; + window: z.ZodObject<{ + from: z.ZodString; + to: z.ZodString; + }, z.core.$strip>; + sections: z.ZodArray; + lines: z.ZodArray; + }, z.core.$strip>>; + metrics: z.ZodObject<{ + autonomy: z.ZodNumber; + correction: z.ZodNumber; + effectiveAutonomy: z.ZodNumber; + interruption: z.ZodNumber; + breakpoint: z.ZodNumber; + dmnCoverage: z.ZodNumber; + unitCost: z.ZodObject<{ + tokens: z.ZodNumber; + minutes: z.ZodNumber; + humanCalls: z.ZodNumber; + }, z.core.$strip>; + terminalMix: z.ZodRecord; + }, z.core.$strip>; + counts: z.ZodObject<{ + touched: z.ZodNumber; + released: z.ZodNumber; + logLines: z.ZodNumber; + }, z.core.$strip>; + note: z.ZodOptional; + submittedAt: z.ZodOptional; + generatedAt: z.ZodString; + }, z.core.$strip>; +}; +export type ListMinutesOutput = z.infer; +export type ListReportsOutput = z.infer; diff --git a/apps/api/src/app/container.ts b/apps/api/src/app/container.ts index c5dc1fb..8263f93 100644 --- a/apps/api/src/app/container.ts +++ b/apps/api/src/app/container.ts @@ -53,8 +53,11 @@ import { createIntentStore, createReleaseService } from '@modules/release'; import type { TaskService } from '@modules/task'; import type { ProcessService } from '@modules/process'; import type { TriggerService } from '@modules/trigger'; +import type { MinuteService } from '@modules/minute'; +import type { ReportService } from '@modules/report'; import { FIRST_PARTY_TOOLS } from './tools/first-party.ts'; import { wireEngines } from './wiring/engines.ts'; +import { wireDeskElements } from './wiring/desk-elements.ts'; import type { ChannelService } from '@modules/channel'; import { createChannelRepository, @@ -92,6 +95,8 @@ export type Container = { readonly task: TaskService; readonly process: ProcessService; readonly trigger: TriggerService; + readonly minute: MinuteService; + readonly report: ReportService; close(): Promise; }; @@ -498,6 +503,17 @@ export async function createContainer(config: AppConfig): Promise { }), }); + // L8:工作台后两个要素 —— 只读 memory 与 task 的投影,一行都不另记 + const deskElements = wireDeskElements({ + db, + memory, + task: engines.task, + org, + process: engines.process, + ids, + clock: systemClock, + }); + return { config, broadcaster: createBroadcaster(), @@ -520,6 +536,8 @@ export async function createContainer(config: AppConfig): Promise { task: engines.task, process: engines.process, trigger: engines.trigger, + minute: deskElements.minute, + report: deskElements.report, async close() { await pool.end(); }, diff --git a/apps/api/src/app/routes/desk-elements.ts b/apps/api/src/app/routes/desk-elements.ts new file mode 100644 index 0000000..0db8404 --- /dev/null +++ b/apps/api/src/app/routes/desk-elements.ts @@ -0,0 +1,204 @@ +/** + * 工作台后两个要素的路由:纪要与汇报(编码规范 §3)。 + * + * 「五要素同源 —— 都是同一份过程事实在员工视角下的投影。」 + * 纪要是唯一的主动动作;汇报是系统代劳的自动产出,员工只确认。 + */ +import type { Hono } from 'hono'; +import type { ApiError, MinuteView, ReportView } from '@contracts'; +import { + appendResolutionRoute, + createMinuteRoute, + draftReportRoute, + listMinutesRoute, + listReportsRoute, + registerResolutionRoute, + reportPeriodSchema, + staffIdSchema, + submitReportRoute, +} from '@contracts'; +import type { Minute } from '@modules/minute'; +import { MinuteNotFoundError, ResolutionAlreadyRegisteredError } from '@modules/minute'; +import type { Report } from '@modules/report'; +import { ReportAlreadySubmittedError, ReportNotFoundError } from '@modules/report'; +import type { Container } from '../container.ts'; +import type { AppEnv } from '../http-env.ts'; + +export function registerDeskElementRoutes(app: Hono, container: Container): void { + async function me(account: string) { + return container.org.findByAccount(account); + } + + const unauthenticated: ApiError = { code: 'unauthenticated', message: '会话无效' }; + + /** 汇报投影:草案内容平铺,前端不必知道 draft 的嵌套 */ + const toReportView = (report: Report): ReportView => ({ + id: report.id, + period: report.period, + title: report.title, + window: report.window, + sections: report.draft.sections.map((s) => ({ + taskId: s.taskId, + title: s.title, + activityId: s.activityId, + ...(s.terminalLabel === undefined ? {} : { terminalLabel: s.terminalLabel }), + lines: [...s.lines], + })), + metrics: report.draft.metrics, + counts: report.draft.counts, + ...(report.note === undefined ? {} : { note: report.note }), + ...(report.submittedAt === undefined ? {} : { submittedAt: report.submittedAt }), + generatedAt: report.generatedAt, + }); + + /* ── 纪要 ─────────────────────────────────────────────────────────── */ + + async function toMinuteView(minute: Minute): Promise { + const author = await container.org.getStaff(staffIdSchema.parse(minute.authorId)).catch(() => undefined); + return { + id: minute.id, + title: minute.title, + body: minute.body, + authorId: staffIdSchema.parse(minute.authorId), + authorName: author?.name ?? '—', + participants: [...minute.participants], + occurredOn: minute.occurredOn, + resolutions: minute.resolutions.map((r) => ({ + id: r.id, + text: r.text, + ...(r.ownerId === undefined ? {} : { ownerId: staffIdSchema.parse(r.ownerId) }), + state: r.state, + ...(r.workflowId === undefined ? {} : { workflowId: r.workflowId }), + ...(r.registeredById === undefined ? {} : { registeredById: staffIdSchema.parse(r.registeredById) }), + ...(r.registeredAt === undefined ? {} : { registeredAt: r.registeredAt }), + })), + createdAt: minute.createdAt, + updatedAt: minute.updatedAt, + }; + } + + app.get(listMinutesRoute.path, async (c) => { + // 纪要是团队沉淀:全员可见,不按负责人过滤 + const minutes = await container.minute.list(); + return c.json({ minutes: await Promise.all(minutes.map(toMinuteView)) }); + }); + + app.post(createMinuteRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const parsed = createMinuteRoute.body.safeParse(await c.req.json()); + if (!parsed.success) { + const body: ApiError = { code: 'validation-failed', message: '纪要不合协议', detail: parsed.error.issues }; + return c.json(body, 400); + } + const minute = await container.minute.create({ + title: parsed.data.title, + body: parsed.data.body, + authorId: staff.id, + occurredOn: parsed.data.occurredOn, + participants: parsed.data.participants, + resolutions: parsed.data.resolutions, + }); + return c.json(await toMinuteView(minute)); + }); + + app.post(appendResolutionRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const parsed = appendResolutionRoute.body.safeParse(await c.req.json()); + if (!parsed.success) { + const body: ApiError = { code: 'validation-failed', message: '决议不合协议' }; + return c.json(body, 400); + } + try { + const minute = await container.minute.appendResolution( + c.req.param('minuteId'), + parsed.data.text, + parsed.data.ownerId, + ); + return c.json(await toMinuteView(minute)); + } catch (error) { + if (error instanceof MinuteNotFoundError) { + const body: ApiError = { code: 'not-found', message: error.message }; + return c.json(body, 404); + } + throw error; + } + }); + + /** 决议 → 登记起单:登记人即来源(OS 3.1) */ + app.post(registerResolutionRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const parsed = registerResolutionRoute.body.safeParse(await c.req.json()); + if (!parsed.success) { + const body: ApiError = { code: 'validation-failed', message: '登记入参不合法' }; + return c.json(body, 400); + } + try { + return c.json( + await container.minute.register({ + resolutionId: c.req.param('resolutionId'), + byId: staff.id, + processPackageId: parsed.data.processPackageId, + data: parsed.data.data, + }), + ); + } catch (error) { + if (error instanceof ResolutionAlreadyRegisteredError) { + // 同一决议不重复起单 —— 已经有一条带来源的流在跑了 + const body: ApiError = { code: 'precondition-failed', message: error.message }; + return c.json(body, 412); + } + if (error instanceof MinuteNotFoundError) { + const body: ApiError = { code: 'not-found', message: error.message }; + return c.json(body, 404); + } + throw error; + } + }); + + /* ── 汇报 ─────────────────────────────────────────────────────────── */ + + app.get(listReportsRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const reports = await container.report.list(staff.id); + return c.json({ reports: reports.map(toReportView) }); + }); + + app.get(draftReportRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const period = reportPeriodSchema.safeParse(c.req.param('period')); + if (!period.success) { + const body: ApiError = { code: 'validation-failed', message: '周期只有日 / 周 / 月 / 年四种' }; + return c.json(body, 400); + } + // 未提交的每次取回都重算 —— 汇报是投影,不是账本 + return c.json(toReportView(await container.report.draft(staff.id, period.data))); + }); + + app.post(submitReportRoute.path, async (c) => { + const staff = await me(c.get('account')); + if (staff === undefined) return c.json(unauthenticated, 401); + const parsed = submitReportRoute.body.safeParse(await c.req.json().catch(() => ({}))); + try { + const report = await container.report.submit( + c.req.param('reportId'), + parsed.success ? parsed.data.note : undefined, + ); + return c.json(toReportView(report)); + } catch (error) { + if (error instanceof ReportAlreadySubmittedError) { + const body: ApiError = { code: 'precondition-failed', message: error.message }; + return c.json(body, 412); + } + if (error instanceof ReportNotFoundError) { + const body: ApiError = { code: 'not-found', message: error.message }; + return c.json(body, 404); + } + throw error; + } + }); +} diff --git a/apps/api/src/app/server.ts b/apps/api/src/app/server.ts index bf234bf..afd1b3e 100644 --- a/apps/api/src/app/server.ts +++ b/apps/api/src/app/server.ts @@ -8,6 +8,7 @@ import { DEFAULT_LOGIN_ACCOUNT } from '@modules/org'; import type { Container } from './container.ts'; import type { AppEnv } from './http-env.ts'; import { registerDeskRoutes } from './routes/desk.ts'; +import { registerDeskElementRoutes } from './routes/desk-elements.ts'; import { registerGovernanceOpsRoutes } from './routes/governance-ops.ts'; import { registerGovernanceRoutes } from './routes/governance.ts'; import { registerIngestRoutes } from './routes/ingest.ts'; @@ -44,6 +45,7 @@ export function createServer(container: Container): Hono { registerWorkbenchRoutes(app, container); registerGovernanceRoutes(app, container); registerDeskRoutes(app, container); + registerDeskElementRoutes(app, container); registerGovernanceOpsRoutes(app, container); registerIngestRoutes(app, container); registerStreamRoute(app, container.broadcaster); diff --git a/apps/api/src/app/wiring/desk-elements.ts b/apps/api/src/app/wiring/desk-elements.ts new file mode 100644 index 0000000..a334243 --- /dev/null +++ b/apps/api/src/app/wiring/desk-elements.ts @@ -0,0 +1,100 @@ +/** + * 工作台后两个要素的装配:纪要(minute)与汇报(report)—— L8。 + * + * 「五要素同源 —— 做事与留痕是同一个动作:五要素都是同一份过程事实 + * (任务记忆与工作流事件)在员工视角下的投影。」(OS 2.1) + * 所以这里的 `FactSource` **只读** memory 与 task,一行都不另记。 + */ +import type { StaffId } from '@contracts'; +import type { MemoryService } from '@modules/memory'; +import type { MinuteService } from '@modules/minute'; +import { createMinuteService, createMinuteStore } from '@modules/minute'; +import type { OrgService } from '@modules/org'; +import type { ProcessService } from '@modules/process'; +import type { LogFact, ReportService, TaskFact } from '@modules/report'; +import { createReportService, createReportStore } from '@modules/report'; +import type { TaskService } from '@modules/task'; +import type { Db } from '@shared/db.ts'; + +export type DeskElementDeps = { + readonly db: Db; + readonly memory: MemoryService; + readonly task: TaskService; + readonly org: OrgService; + readonly process: ProcessService; + readonly ids: { next(): string }; + readonly clock: { now(): Date }; +}; + +export function wireDeskElements(deps: DeskElementDeps): { minute: MinuteService; report: ReportService } { + const minute = createMinuteService({ + store: createMinuteStore(deps.db), + registration: { + // 登记触发:凡单必有来源 —— 纪要起单的来源就是登记人 + start: async (input) => { + const started = await deps.process.start({ + packageId: input.processPackageId, + subject: input.subject, + data: input.data, + source: { kind: 'registration', ref: input.processPackageId, by: input.byId }, + }); + return { workflowId: started.id }; + }, + }, + ids: deps.ids, + clock: deps.clock, + }); + + /** 归侧日志行 → 度量事实。判断类与断点由日志标题的确定形态识别,不另设埋点 */ + const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => ({ + taskId, + at: line.at, + side: line.side, + title: line.title, + isHumanToolCall: line.title.startsWith('发出:'), + // DMN / 机检的确定性求值在日志里是「求值:」;LLM 的判断落在「调用:」 + isDeterministicJudgement: line.title.startsWith('求值:'), + isJudgement: line.title.startsWith('求值:') || line.title.startsWith('调用:'), + isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'), + }); + + const report = createReportService({ + store: createReportStore(deps.db), + facts: { + async logsOf(staffId, from, to) { + const owners = staffId === undefined ? (await deps.org.listStaff()).staff.map((s) => s.id) : [staffId]; + const nested = await Promise.all(owners.map((id) => deps.task.listByOwner(id))); + const tasks = nested.flat(); + const perTask = await Promise.all( + tasks.map(async (t) => (await deps.memory.taskLog(t.id)).map((l) => toLogFact(t.id, l))), + ); + return perTask.flat().filter((l) => l.at >= from && l.at < to); + }, + async tasksOf(staffId) { + const owners = staffId === undefined ? (await deps.org.listStaff()).staff.map((s) => s.id) : [staffId]; + const nested = await Promise.all(owners.map((id) => deps.task.listByOwner(id))); + return nested.flat().map((t) => ({ + taskId: t.id, + title: t.title, + activityId: t.activityId, + startedAt: t.startedAt, + finishedAt: t.finishedAt, + terminal: t.terminal, + // 提交批次 = 放行提交次数;放行完成必有一次提交,未终态的以已发生的放行计 + submissions: t.terminal === 'released' ? 1 : 0, + // 被纠正批次:驳回重做 / 驳回迭代 / 叫停 / 裁决推翻 —— 非放行终态即计一次 + corrections: t.terminal !== undefined && t.terminal !== 'released' ? 1 : 0, + tokens: t.used.tokens, + activeMs: t.used.activeMs, + humanCalls: t.used.humanCalls, + })); + }, + }, + ids: deps.ids, + clock: deps.clock, + }); + + return { minute, report }; +} + +export type { StaffId }; diff --git a/apps/api/src/modules/minute/CONTRACT.md b/apps/api/src/modules/minute/CONTRACT.md new file mode 100644 index 0000000..5acfa9d --- /dev/null +++ b/apps/api/src/modules/minute/CONTRACT.md @@ -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` —— 起单一刻已有一条带来源的工作流在跑 + - **只增不改**:正文可续、决议只增;纪要是沉淀,不是可覆写的文档 + - 登记不可撤销:要否掉那条流,走治理台的控制事件,而不是回改纪要 diff --git a/apps/api/src/modules/minute/__tests__/minute.test.ts b/apps/api/src/modules/minute/__tests__/minute.test.ts new file mode 100644 index 0000000..ce2dabf --- /dev/null +++ b/apps/api/src/modules/minute/__tests__/minute.test.ts @@ -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(); + }); +}); diff --git a/apps/api/src/modules/minute/contract.ts b/apps/api/src/modules/minute/contract.ts new file mode 100644 index 0000000..963396b --- /dev/null +++ b/apps/api/src/modules/minute/contract.ts @@ -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; +}; + +export type MinuteService = { + list(limit?: number): Promise; + get(minuteId: string): Promise; + create(input: CreateMinuteInput): Promise; + /** 只增不改的编辑:正文可续,决议只增 —— 纪要是沉淀,不是可覆写的文档 */ + appendResolution(minuteId: string, text: string, ownerId?: StaffId): Promise; + + /** 决议 → 登记起单:登记人即来源;同一决议只起一次单 */ + register(input: RegisterInput): Promise<{ workflowId: string; resolutionId: string }>; +}; diff --git a/apps/api/src/modules/minute/index.ts b/apps/api/src/modules/minute/index.ts new file mode 100644 index 0000000..3e6ae42 --- /dev/null +++ b/apps/api/src/modules/minute/index.ts @@ -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'; diff --git a/apps/api/src/modules/minute/internal/adapter/minute-schema.ts b/apps/api/src/modules/minute/internal/adapter/minute-schema.ts new file mode 100644 index 0000000..4e1244b --- /dev/null +++ b/apps/api/src/modules/minute/internal/adapter/minute-schema.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)], +); diff --git a/apps/api/src/modules/minute/internal/adapter/minute-store.ts b/apps/api/src/modules/minute/internal/adapter/minute-store.ts new file mode 100644 index 0000000..3b8d4df --- /dev/null +++ b/apps/api/src/modules/minute/internal/adapter/minute-store.ts @@ -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> { + 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(); + 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), + }, + }); + } + }); + }, + }; +} diff --git a/apps/api/src/modules/minute/internal/domain/minute.ts b/apps/api/src/modules/minute/internal/domain/minute.ts new file mode 100644 index 0000000..87abac4 --- /dev/null +++ b/apps/api/src/modules/minute/internal/domain/minute.ts @@ -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; +} diff --git a/apps/api/src/modules/minute/internal/usecase/minute-service.ts b/apps/api/src/modules/minute/internal/usecase/minute-service.ts new file mode 100644 index 0000000..b91bca8 --- /dev/null +++ b/apps/api/src/modules/minute/internal/usecase/minute-service.ts @@ -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 }; + }, + }; +} diff --git a/apps/api/src/modules/minute/package.json b/apps/api/src/modules/minute/package.json new file mode 100644 index 0000000..6f2b3dc --- /dev/null +++ b/apps/api/src/modules/minute/package.json @@ -0,0 +1 @@ +{ "name": "@modules/minute", "private": true, "type": "module", "main": "index.ts" } diff --git a/apps/api/src/modules/minute/port.ts b/apps/api/src/modules/minute/port.ts new file mode 100644 index 0000000..997b8e1 --- /dev/null +++ b/apps/api/src/modules/minute/port.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; + get(minuteId: string): Promise; + save(minute: Minute): Promise; +}; + +/** 登记起单 —— app 注入 `process`(L7):经调用接口调起流程包,不碰流程内部 */ +export type RegistrationGateway = { + start(input: { + processPackageId: string; + subject: string; + data: Record; + /** 来源:登记人 */ + byId: StaffId; + }): Promise<{ workflowId: string }>; +}; + +export type MinuteDeps = { + readonly store: MinuteStore; + readonly registration: RegistrationGateway; + readonly ids: { next(): string }; + readonly clock: { now(): Date }; +}; diff --git a/apps/api/src/modules/report/CONTRACT.md b/apps/api/src/modules/report/CONTRACT.md new file mode 100644 index 0000000..2df4983 --- /dev/null +++ b/apps/api/src/modules/report/CONTRACT.md @@ -0,0 +1,28 @@ +# module: report + +职责: 汇报(日/周/月/年)对任务日志的聚合投影;度量八指标的计算 +不负责: 任务日志本身(见 memory)、终态与预算(见 task)—— 本模块**只读不写**这两处; + 活动记忆与蒸馏(见 memory) + +公开出口: index.ts + - 类型:`ReportService` `Report` `ReportDraft` `ReportSection` `ReportPeriod` + `Window` `Metrics` `LogFact` `TaskFact` + - 错误:`ReportNotFoundError` `ReportAlreadySubmittedError` + - 端口类型:`ReportStore` `FactSource` + - 用例工厂:`createReportService(deps): ReportService` + - 纯函数:`windowOf` `periodLabel` `draftReport` `computeMetrics`;常量 `REPORT_PERIODS` + - adapter:`createReportStore`;Drizzle 表:`reports` + +层级: L8(编码规范 §4) +依赖: @contracts;@shared/db;port:`FactSource`(memory L2 + task L6,由 app 注入) +被依赖: app(路由组装) + +不变量(OS 2.1 / 8.1 / 8.3): + - **零手工汇报**:草案的每一句都取自任务日志;员工唯一的手工位是确认前补的一段话 + - **汇报是投影不是账本**:未提交的草案每次取回都重算;**提交那一刻**才固定为事实快照 + - **确认即提交,提交后不再改**(`ReportAlreadySubmittedError`) + - **口径挂归侧与事件流,零埋点**:输入只有归侧日志行与任务终态,没有第三种埋点 + - **自主率与纠正率成对读**,复合为有效自主率 —— 单看任一都会被优化坏 + - **单位成本的分母只认放行通过**:跑得多不等于产出多 + - 度量是**只读投影**,不设独立模块(§4 注);治理台记忆区与工作台汇报同源同口径 + - 周期窗口左闭右开、**周一为首日**、按 UTC 切分(单组织单时区) diff --git a/apps/api/src/modules/report/__tests__/period.test.ts b/apps/api/src/modules/report/__tests__/period.test.ts new file mode 100644 index 0000000..52788f3 --- /dev/null +++ b/apps/api/src/modules/report/__tests__/period.test.ts @@ -0,0 +1,144 @@ +/** 汇报周期与聚合 —— domain 测试零 mock(OS 2.1 / 8.3) */ +import { describe, expect, it } from 'vitest'; +import { periodLabel, windowOf } from '../internal/domain/period.ts'; +import { computeMetrics, draftReport } from '../internal/domain/aggregate.ts'; +import type { LogFact, TaskFact } from '../internal/domain/aggregate.ts'; + +const log = (over: Partial = {}): LogFact => ({ + taskId: 't1', + at: '2026-07-27T10:00:00.000Z', + side: 'machine', + title: '调用:某步', + isHumanToolCall: false, + isDeterministicJudgement: false, + isJudgement: false, + isBreakpoint: false, + ...over, +}); + +const task = (over: Partial = {}): TaskFact => ({ + taskId: 't1', + title: 'A1 RFQ需求分析', + activityId: 'quote-intake', + startedAt: '2026-07-27T09:00:00.000Z', + submissions: 0, + corrections: 0, + tokens: 0, + activeMs: 0, + humanCalls: 0, + ...over, +}); + +describe('周期窗口 —— 左闭右开,周一为首日', () => { + it('日窗口是当天 00:00 起 24 小时', () => { + expect(windowOf('day', new Date('2026-07-27T15:30:00Z'))).toEqual({ + from: '2026-07-27T00:00:00.000Z', + to: '2026-07-28T00:00:00.000Z', + }); + }); + + it('周窗口自周一起算 —— 周日属于它前面那一周', () => { + // 2026-07-27 是周一;2026-08-02 是周日,应落在同一周 + const monday = windowOf('week', new Date('2026-07-27T00:00:00Z')); + const sunday = windowOf('week', new Date('2026-08-02T23:59:00Z')); + expect(monday.from).toBe('2026-07-27T00:00:00.000Z'); + expect(sunday).toEqual(monday); + }); + + it('月与年按自然边界切', () => { + expect(windowOf('month', new Date('2026-07-27T00:00:00Z')).to).toBe('2026-08-01T00:00:00.000Z'); + expect(windowOf('year', new Date('2026-07-27T00:00:00Z'))).toEqual({ + from: '2026-01-01T00:00:00.000Z', + to: '2027-01-01T00:00:00.000Z', + }); + }); + + it('周期名可读', () => { + expect(periodLabel('day', new Date('2026-07-27T00:00:00Z'))).toBe('2026-07-27 日报'); + expect(periodLabel('month', new Date('2026-07-27T00:00:00Z'))).toBe('2026-07 月报'); + }); +}); + +describe('度量八指标 —— 口径挂归侧,零埋点', () => { + it('自主率不把裁决与等待计入分母', () => { + const m = computeMetrics( + [log({ side: 'machine' }), log({ side: 'machine' }), log({ side: 'human' }), log({ side: 'human-verdict' }), log({ side: 'world-wait' })], + [task()], + ); + expect(m.autonomy).toBeCloseTo(2 / 3); + }); + + it('有效自主率 = 自主率 ×(1 − 纠正率)—— 猛冲后返工不占便宜', () => { + const m = computeMetrics( + [log({ side: 'machine' }), log({ side: 'machine' }), log({ side: 'machine' }), log({ side: 'human' })], + [task({ submissions: 4, corrections: 1 })], + ); + expect(m.autonomy).toBeCloseTo(0.75); + expect(m.correction).toBeCloseTo(0.25); + expect(m.effectiveAutonomy).toBeCloseTo(0.5625); + }); + + it('单位成本的分母只认放行通过 —— 跑得多不等于产出多', () => { + const m = computeMetrics( + [log()], + [ + task({ taskId: 't1', terminal: 'released', tokens: 1000, activeMs: 120_000, humanCalls: 2 }), + task({ taskId: 't2', terminal: 'terminated', tokens: 3000, activeMs: 60_000, humanCalls: 4 }), + ], + ); + expect(m.unitCost.tokens).toBe(4000); + expect(m.unitCost.minutes).toBeCloseTo(3); + }); + + it('无提交批次时纠正率为 0,不产生 NaN', () => { + const m = computeMetrics([], []); + expect(m.autonomy).toBe(0); + expect(m.correction).toBe(0); + expect(m.effectiveAutonomy).toBe(0); + expect(m.unitCost.tokens).toBe(0); + expect(Object.values(m.terminalMix).every((v) => v === 0)).toBe(true); + }); + + it('终态分布覆盖五个终态名', () => { + const m = computeMetrics([], [task({ terminal: 'released' }), task({ taskId: 't2', terminal: 'blocked' })]); + expect(Object.keys(m.terminalMix).sort()).toEqual( + ['blocked', 'exhausted', 'released', 'stalled', 'terminated'], + ); + expect(m.terminalMix.released).toBeCloseTo(0.5); + }); +}); + +describe('汇报草案 —— 由任务日志聚合,员工不写', () => { + const window = windowOf('day', new Date('2026-07-27T12:00:00Z')); + + it('只收窗口内的日志,窗口外的任务不成段', () => { + const draft = draftReport( + [ + log({ taskId: 't1', at: '2026-07-27T10:00:00.000Z', title: '调用:步 1' }), + log({ taskId: 't2', at: '2026-07-26T10:00:00.000Z', title: '昨天的事' }), + ], + [task({ taskId: 't1' }), task({ taskId: 't2', title: '昨天那单' })], + window, + ); + expect(draft.sections.map((s) => s.taskId)).toEqual(['t1']); + expect(draft.counts.logLines).toBe(1); + }); + + it('段内按时间排序,每一句都取自任务日志', () => { + const draft = draftReport( + [ + log({ at: '2026-07-27T12:00:00.000Z', title: '返回:步 2' }), + log({ at: '2026-07-27T10:00:00.000Z', title: '调用:步 1' }), + ], + [task()], + window, + ); + expect(draft.sections[0]?.lines).toEqual(['调用:步 1', '返回:步 2']); + }); + + it('终态以中文名标注', () => { + const draft = draftReport([log()], [task({ terminal: 'released' })], window); + expect(draft.sections[0]?.terminalLabel).toBe('放行完成'); + expect(draft.counts.released).toBe(1); + }); +}); diff --git a/apps/api/src/modules/report/contract.ts b/apps/api/src/modules/report/contract.ts new file mode 100644 index 0000000..b195fe6 --- /dev/null +++ b/apps/api/src/modules/report/contract.ts @@ -0,0 +1,50 @@ +/** + * module: report —— 对外类型(编码规范 §10.1;语义出处 OS 2.1 / 8.1 / 8.3)。 + * + * 汇报是**系统代劳的自动产出**:由本人各任务的任务日志聚合生成,确认即提交,零手工汇报。 + * 度量八指标是任务日志与终态之上的**只读投影**,不设独立模块(§4 注)。 + */ +import type { StaffId } from '@contracts'; +import type { Metrics, ReportDraft, ReportSection } from './internal/domain/aggregate.ts'; +import type { ReportPeriod, Window } from './internal/domain/period.ts'; + +export type { Metrics, ReportDraft, ReportPeriod, ReportSection, Window }; + +export class ReportNotFoundError extends Error { + constructor(readonly reportId: string) { + super(`汇报不存在:${reportId}`); + this.name = 'ReportNotFoundError'; + } +} + +export class ReportAlreadySubmittedError extends Error { + constructor(readonly reportId: string) { + super(`汇报 ${reportId} 已提交 —— 确认即提交,提交后不再改`); + this.name = 'ReportAlreadySubmittedError'; + } +} + +/** 汇报单:草案由系统生成,员工确认即提交 */ +export type Report = { + readonly id: string; + readonly staffId: StaffId; + readonly period: ReportPeriod; + readonly title: string; + readonly window: Window; + readonly draft: ReportDraft; + /** 员工可在确认前补一段话 —— 唯一的手工位 */ + readonly note?: string | undefined; + readonly submittedAt?: string | undefined; + readonly generatedAt: string; +}; + +export type ReportService = { + /** 生成(或取回)某人某周期的汇报草案 —— 未提交的重取即重算,提交后固定 */ + draft(staffId: StaffId, period: ReportPeriod, anchor?: Date): Promise; + list(staffId: StaffId, limit?: number): Promise; + get(reportId: string): Promise; + /** 确认即提交 */ + submit(reportId: string, note?: string): Promise; + /** 度量八指标 —— 治理台记忆区的仪表读数,同源同口径 */ + metrics(staffId: StaffId | undefined, period: ReportPeriod, anchor?: Date): Promise; +}; diff --git a/apps/api/src/modules/report/index.ts b/apps/api/src/modules/report/index.ts new file mode 100644 index 0000000..ae06d2c --- /dev/null +++ b/apps/api/src/modules/report/index.ts @@ -0,0 +1,10 @@ +/** module: report —— 唯一出口(编码规范 §10.1):纯再导出,不含实现。 */ +export type { Metrics, Report, ReportDraft, ReportPeriod, ReportSection, ReportService, Window } from './contract.ts'; +export { ReportAlreadySubmittedError, ReportNotFoundError } from './contract.ts'; +export type { FactSource, ReportStore, ReportDeps } from './port.ts'; +export type { LogFact, TaskFact } from './internal/domain/aggregate.ts'; +export { computeMetrics, draftReport } from './internal/domain/aggregate.ts'; +export { REPORT_PERIODS, periodLabel, windowOf } from './internal/domain/period.ts'; +export { createReportService } from './internal/usecase/report-service.ts'; +export { createReportStore } from './internal/adapter/report-store.ts'; +export { reports } from './internal/adapter/report-schema.ts'; diff --git a/apps/api/src/modules/report/internal/adapter/report-schema.ts b/apps/api/src/modules/report/internal/adapter/report-schema.ts new file mode 100644 index 0000000..99c5650 --- /dev/null +++ b/apps/api/src/modules/report/internal/adapter/report-schema.ts @@ -0,0 +1,23 @@ +/** + * report 的 sys schema 表(编码规范 §7 / ADR-002)。 + * 只存**汇报单**本身;草案内容是投影的快照 —— 未提交时可重算,提交后固定为当时事实。 + */ +import { index, jsonb, text, timestamp } from 'drizzle-orm/pg-core'; +import { sys } from '@shared/db.ts'; + +export const reports = sys.table( + 'report', + { + id: text('id').primaryKey(), + staffId: text('staff_id').notNull(), + period: text('period').notNull(), + title: text('title').notNull(), + windowFrom: text('window_from').notNull(), + windowTo: text('window_to').notNull(), + draft: jsonb('draft').notNull(), + note: text('note'), + submittedAt: timestamp('submitted_at', { withTimezone: true }), + generatedAt: timestamp('generated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [index('report_staff_period_idx').on(t.staffId, t.period, t.windowFrom)], +); diff --git a/apps/api/src/modules/report/internal/adapter/report-store.ts b/apps/api/src/modules/report/internal/adapter/report-store.ts new file mode 100644 index 0000000..3a9e473 --- /dev/null +++ b/apps/api/src/modules/report/internal/adapter/report-store.ts @@ -0,0 +1,83 @@ +/** report 的持久化(编码规范 §10.2:框架只许出现在 adapter)。 */ +import { and, desc, eq, isNull } from 'drizzle-orm'; +import type { StaffId } from '@contracts'; +import type { Db } from '@shared/db.ts'; +import type { Report, ReportDraft, ReportPeriod } from '../../contract.ts'; +import type { ReportStore } from '../../port.ts'; +import { reports } from './report-schema.ts'; + +type Row = typeof reports.$inferSelect; + +const toReport = (row: Row): Report => ({ + id: row.id, + staffId: row.staffId as StaffId, + period: row.period as ReportPeriod, + title: row.title, + window: { from: row.windowFrom, to: row.windowTo }, + draft: row.draft as ReportDraft, + note: row.note ?? undefined, + submittedAt: row.submittedAt?.toISOString(), + generatedAt: row.generatedAt.toISOString(), +}); + +export function createReportStore(db: Db): ReportStore { + return { + async list(staffId, limit) { + const rows = await db + .select() + .from(reports) + .where(eq(reports.staffId, staffId)) + .orderBy(desc(reports.windowFrom)) + .limit(limit); + return rows.map(toReport); + }, + + async get(reportId) { + const [row] = await db.select().from(reports).where(eq(reports.id, reportId)).limit(1); + return row === undefined ? undefined : toReport(row); + }, + + /** 未提交的同窗汇报可重算复用;已提交的不再复用,它已是当时的事实快照 */ + async findOpen(staffId, period, windowFrom) { + const [row] = await db + .select() + .from(reports) + .where( + and( + eq(reports.staffId, staffId), + eq(reports.period, period), + eq(reports.windowFrom, windowFrom), + isNull(reports.submittedAt), + ), + ) + .limit(1); + return row === undefined ? undefined : toReport(row); + }, + + async save(report) { + await db + .insert(reports) + .values({ + id: report.id, + staffId: report.staffId, + period: report.period, + title: report.title, + windowFrom: report.window.from, + windowTo: report.window.to, + draft: report.draft, + note: report.note ?? null, + submittedAt: report.submittedAt === undefined ? null : new Date(report.submittedAt), + generatedAt: new Date(report.generatedAt), + }) + .onConflictDoUpdate({ + target: reports.id, + set: { + draft: report.draft, + note: report.note ?? null, + submittedAt: report.submittedAt === undefined ? null : new Date(report.submittedAt), + generatedAt: new Date(report.generatedAt), + }, + }); + }, + }; +} diff --git a/apps/api/src/modules/report/internal/domain/aggregate.ts b/apps/api/src/modules/report/internal/domain/aggregate.ts new file mode 100644 index 0000000..b4803ca --- /dev/null +++ b/apps/api/src/modules/report/internal/domain/aggregate.ts @@ -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>; +}; + +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>((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, + ); + + 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((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, + }, + }; +} diff --git a/apps/api/src/modules/report/internal/domain/period.ts b/apps/api/src/modules/report/internal/domain/period.ts new file mode 100644 index 0000000..8831b11 --- /dev/null +++ b/apps/api/src/modules/report/internal/domain/period.ts @@ -0,0 +1,68 @@ +/** + * 汇报周期与聚合 —— 零 IO、零框架、零 `Date.now()`(编码规范 §10.2)。 + * + * 「按日、周、月、年自动总结此员工的工作汇报——由本人各任务的**任务日志**聚合生成, + * 确认即提交,**零手工汇报**。」(OS 2.1 / 8.1) + * + * 聚合是**投影**不是记账:同一份任务日志换个时间窗就是另一份汇报, + * 所以这里只做纯函数,不落任何中间态。 + */ + +export const REPORT_PERIODS = ['day', 'week', 'month', 'year'] as const; +export type ReportPeriod = (typeof REPORT_PERIODS)[number]; + +export type Window = { readonly from: string; readonly to: string }; + +const pad = (n: number): string => String(n).padStart(2, '0'); +const iso = (d: Date): string => d.toISOString(); + +/** + * 周期窗口:左闭右开 `[from, to)`。 + * 周以**周一**为首日(ISO 8601),不是周日 —— 中文语境的「本周」按周一起算。 + * 一律按 UTC 切分:单组织单时区,不引入时区表;跨时区留待有需要时再谈。 + */ +export function windowOf(period: ReportPeriod, anchor: Date): Window { + const y = anchor.getUTCFullYear(); + const m = anchor.getUTCMonth(); + const d = anchor.getUTCDate(); + + switch (period) { + case 'day': { + const from = new Date(Date.UTC(y, m, d)); + return { from: iso(from), to: iso(new Date(Date.UTC(y, m, d + 1))) }; + } + case 'week': { + // getUTCDay(): 0=周日 → 距周一 6 天;1..6 → 距周一 day-1 天 + const day = anchor.getUTCDay(); + const backToMonday = day === 0 ? 6 : day - 1; + const from = new Date(Date.UTC(y, m, d - backToMonday)); + return { from: iso(from), to: iso(new Date(Date.UTC(y, m, d - backToMonday + 7))) }; + } + case 'month': { + return { from: iso(new Date(Date.UTC(y, m, 1))), to: iso(new Date(Date.UTC(y, m + 1, 1))) }; + } + case 'year': { + return { from: iso(new Date(Date.UTC(y, 0, 1))), to: iso(new Date(Date.UTC(y + 1, 0, 1))) }; + } + } +} + +/** 人读的周期名 —— 汇报标题用 */ +export function periodLabel(period: ReportPeriod, anchor: Date): string { + const y = anchor.getUTCFullYear(); + switch (period) { + case 'day': + return `${y}-${pad(anchor.getUTCMonth() + 1)}-${pad(anchor.getUTCDate())} 日报`; + case 'week': { + const { from } = windowOf('week', anchor); + const start = new Date(from); + return `${start.getUTCFullYear()}-${pad(start.getUTCMonth() + 1)}-${pad(start.getUTCDate())} 起当周周报`; + } + case 'month': + return `${y}-${pad(anchor.getUTCMonth() + 1)} 月报`; + case 'year': + return `${y} 年报`; + } +} + +export const withinWindow = (at: string, w: Window): boolean => at >= w.from && at < w.to; diff --git a/apps/api/src/modules/report/internal/usecase/report-service.ts b/apps/api/src/modules/report/internal/usecase/report-service.ts new file mode 100644 index 0000000..1a45549 --- /dev/null +++ b/apps/api/src/modules/report/internal/usecase/report-service.ts @@ -0,0 +1,70 @@ +/** + * 汇报用例(编码规范 §10.2)。 + * + * 「按日、周、月、年**自动**总结……确认即提交,**零手工汇报**。」(OS 2.1) + * 未提交的草案每次取回都重算 —— 汇报是投影,不是账本;提交那一刻才固定为事实快照。 + */ +import type { Report, ReportService } from '../../contract.ts'; +import { ReportAlreadySubmittedError, ReportNotFoundError } from '../../contract.ts'; +import type { ReportDeps } from '../../port.ts'; +import { computeMetrics, draftReport } from '../domain/aggregate.ts'; +import { periodLabel, windowOf } from '../domain/period.ts'; + +export function createReportService(deps: ReportDeps): ReportService { + return { + async draft(staffId, period, anchor) { + const at = anchor ?? deps.clock.now(); + const window = windowOf(period, at); + const [logs, tasks] = await Promise.all([ + deps.facts.logsOf(staffId, window.from, window.to), + deps.facts.tasksOf(staffId), + ]); + const draft = draftReport(logs, tasks, window); + + const existing = await deps.store.findOpen(staffId, period, window.from); + const report: Report = { + id: existing?.id ?? deps.ids.next(), + staffId, + period, + title: periodLabel(period, at), + window, + draft, + ...(existing?.note === undefined ? {} : { note: existing.note }), + generatedAt: deps.clock.now().toISOString(), + }; + await deps.store.save(report); + return report; + }, + + list: (staffId, limit = 20) => deps.store.list(staffId, limit), + + async get(reportId) { + const report = await deps.store.get(reportId); + if (report === undefined) throw new ReportNotFoundError(reportId); + return report; + }, + + async submit(reportId, note) { + const report = await deps.store.get(reportId); + if (report === undefined) throw new ReportNotFoundError(reportId); + // 确认即提交:提交后不再改,否则「汇报」就成了可回改的说法 + if (report.submittedAt !== undefined) throw new ReportAlreadySubmittedError(reportId); + const submitted: Report = { + ...report, + ...(note === undefined ? {} : { note }), + submittedAt: deps.clock.now().toISOString(), + }; + await deps.store.save(submitted); + return submitted; + }, + + async metrics(staffId, period, anchor) { + const window = windowOf(period, anchor ?? deps.clock.now()); + const [logs, tasks] = await Promise.all([ + deps.facts.logsOf(staffId, window.from, window.to), + deps.facts.tasksOf(staffId), + ]); + return computeMetrics(logs, tasks); + }, + }; +} diff --git a/apps/api/src/modules/report/package.json b/apps/api/src/modules/report/package.json new file mode 100644 index 0000000..7d8d9fa --- /dev/null +++ b/apps/api/src/modules/report/package.json @@ -0,0 +1 @@ +{ "name": "@modules/report", "private": true, "type": "module", "main": "index.ts" } diff --git a/apps/api/src/modules/report/port.ts b/apps/api/src/modules/report/port.ts new file mode 100644 index 0000000..e41ce07 --- /dev/null +++ b/apps/api/src/modules/report/port.ts @@ -0,0 +1,28 @@ +/** + * module: report 的外部所需(编码规范 §10.1)。 + * 汇报**不自己记账**:事实全部来自 memory(任务日志)与 task(终态 / 预算 / 纠正信号)。 + */ +import type { StaffId } from '@contracts'; +import type { Report } from './contract.ts'; +import type { LogFact, TaskFact } from './internal/domain/aggregate.ts'; + +export type ReportStore = { + list(staffId: StaffId, limit: number): Promise; + get(reportId: string): Promise; + findOpen(staffId: StaffId, period: string, windowFrom: string): Promise; + save(report: Report): Promise; +}; + +/** 事实源 —— app 注入 `memory`(L2)与 `task`(L6);report 在 L8,只读不写 */ +export type FactSource = { + /** 某人(或全员)在窗口内的归侧日志行 */ + logsOf(staffId: StaffId | undefined, from: string, to: string): Promise; + tasksOf(staffId: StaffId | undefined): Promise; +}; + +export type ReportDeps = { + readonly store: ReportStore; + readonly facts: FactSource; + readonly ids: { next(): string }; + readonly clock: { now(): Date }; +}; diff --git a/db/migrations/0008_strange_quasar.sql b/db/migrations/0008_strange_quasar.sql new file mode 100644 index 0000000..2e32238 --- /dev/null +++ b/db/migrations/0008_strange_quasar.sql @@ -0,0 +1,38 @@ +CREATE TABLE "sys"."minute_resolution" ( + "id" text PRIMARY KEY NOT NULL, + "minute_id" text NOT NULL, + "text" text NOT NULL, + "owner_id" text, + "state" text DEFAULT 'open' NOT NULL, + "workflow_id" text, + "registered_by_id" text, + "registered_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "sys"."minute" ( + "id" text PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "author_id" text NOT NULL, + "participants" jsonb DEFAULT '[]'::jsonb NOT NULL, + "occurred_on" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sys"."report" ( + "id" text PRIMARY KEY NOT NULL, + "staff_id" text NOT NULL, + "period" text NOT NULL, + "title" text NOT NULL, + "window_from" text NOT NULL, + "window_to" text NOT NULL, + "draft" jsonb NOT NULL, + "note" text, + "submitted_at" timestamp with time zone, + "generated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "minute_resolution_minute_idx" ON "sys"."minute_resolution" USING btree ("minute_id");--> statement-breakpoint +CREATE INDEX "minute_occurred_idx" ON "sys"."minute" USING btree ("occurred_on");--> statement-breakpoint +CREATE INDEX "report_staff_period_idx" ON "sys"."report" USING btree ("staff_id","period","window_from"); \ No newline at end of file diff --git a/db/migrations/meta/0008_snapshot.json b/db/migrations/meta/0008_snapshot.json new file mode 100644 index 0000000..b680d1f --- /dev/null +++ b/db/migrations/meta/0008_snapshot.json @@ -0,0 +1,3260 @@ +{ + "id": "c7da678d-52d1-42f1-8682-1a7dfac74945", + "prevId": "0e9cf97f-0886-4ab7-ac9a-a11e5f23cdf9", + "version": "7", + "dialect": "postgresql", + "tables": { + "sys.activity_asset": { + "name": "activity_asset", + "schema": "sys", + "columns": { + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective": { + "name": "effective", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_asset_effective_key": { + "name": "activity_asset_effective_key", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"sys\".\"activity_asset\".\"effective\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "activity_asset_activity_id_kind_version_pk": { + "name": "activity_asset_activity_id_kind_version_pk", + "columns": [ + "activity_id", + "kind", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.activity_package": { + "name": "activity_package", + "schema": "sys", + "columns": { + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paradigm": { + "name": "paradigm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "artifact": { + "name": "artifact", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_package_category_idx": { + "name": "activity_package_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "paradigm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "activity_package_activity_id_version_pk": { + "name": "activity_package_activity_id_version_pk", + "columns": [ + "activity_id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.asset_increment": { + "name": "asset_increment", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "diff": { + "name": "diff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_entry_ids": { + "name": "evidence_entry_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'candidate'" + }, + "replay_report": { + "name": "replay_report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "asset_increment_activity_idx": { + "name": "asset_increment_activity_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.channel_instance": { + "name": "channel_instance", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "extract": { + "name": "extract", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "mapping": { + "name": "mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "fallback_position": { + "name": "fallback_position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_window_ms": { + "name": "address_window_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 604800000 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.outbound_mail": { + "name": "outbound_mail", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_addr": { + "name": "from_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_addrs": { + "name": "to_addrs", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cc_addrs": { + "name": "cc_addrs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.event_consumption": { + "name": "event_consumption", + "schema": "sys", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumer": { + "name": "consumer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "consumption_key": { + "name": "consumption_key", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "consumer", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "event_consumption_event_id_consumer_pk": { + "name": "event_consumption_event_id_consumer_pk", + "columns": [ + "event_id", + "consumer" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.event_delivery": { + "name": "event_delivery", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "delivery_pending_idx": { + "name": "delivery_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "delivery_target_idx": { + "name": "delivery_target_idx", + "columns": [ + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_delivery_event_id_event_id_fk": { + "name": "event_delivery_event_id_event_id_fk", + "tableFrom": "event_delivery", + "tableTo": "event", + "schemaTo": "sys", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.event": { + "name": "event", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation": { + "name": "correlation", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "trace": { + "name": "trace", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_target_idx": { + "name": "event_target_idx", + "columns": [ + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "event_occurred_idx": { + "name": "event_occurred_idx", + "columns": [ + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.listener": { + "name": "listener", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_filter": { + "name": "key_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "semantic": { + "name": "semantic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deadline": { + "name": "deadline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "escalation": { + "name": "escalation", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'armed'" + } + }, + "indexes": { + "listener_armed_idx": { + "name": "listener_armed_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "listener_task_idx": { + "name": "listener_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.outbound_ref": { + "name": "outbound_ref", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "outbound_thread_idx": { + "name": "outbound_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbound_addr_idx": { + "name": "outbound_addr_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.flow_state": { + "name": "flow_state", + "schema": "sys", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "model": { + "name": "model", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.chat_card": { + "name": "chat_card", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "superseded_by": { + "name": "superseded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_reason": { + "name": "rejected_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "chat_card_task_idx": { + "name": "chat_card_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_card_recipient_idx": { + "name": "chat_card_recipient_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.work_order": { + "name": "work_order", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "done": { + "name": "done", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "work_order_task_idx": { + "name": "work_order_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "work_order_recipient_idx": { + "name": "work_order_recipient_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.chat_message": { + "name": "chat_message", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "chat_message_task_idx": { + "name": "chat_message_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.task_view": { + "name": "task_view", + "schema": "sys", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "blocks": { + "name": "blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'activating'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.llm_instance": { + "name": "llm_instance", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_cipher": { + "name": "api_key_cipher", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_use": { + "name": "system_use", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "llm_instance_name_key": { + "name": "llm_instance_name_key", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.replay_model": { + "name": "replay_model", + "schema": "sys", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "call": { + "name": "call", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "replay_model_recorded_idx": { + "name": "replay_model_recorded_idx", + "columns": [ + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.loop_state": { + "name": "loop_state", + "schema": "sys", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "model": { + "name": "model", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "parsed": { + "name": "parsed", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.activity_memory": { + "name": "activity_memory", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "layer": { + "name": "layer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "statement": { + "name": "statement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "increment": { + "name": "increment", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pruned_at": { + "name": "pruned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "activity_memory_layer_idx": { + "name": "activity_memory_layer_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "layer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.task_memory_seal": { + "name": "task_memory_seal", + "schema": "sys", + "columns": { + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sealed_at": { + "name": "sealed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.task_memory": { + "name": "task_memory", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "side": { + "name": "side", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stamp": { + "name": "stamp", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "governance": { + "name": "governance", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "links_to": { + "name": "links_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_ref": { + "name": "event_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "task_memory_seq_key": { + "name": "task_memory_seq_key", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_memory_task_idx": { + "name": "task_memory_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.system_message": { + "name": "system_message", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staff_id": { + "name": "staff_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "card": { + "name": "card", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "answer": { + "name": "answer", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_ref": { + "name": "event_ref", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_staff_idx": { + "name": "message_staff_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_unread_idx": { + "name": "message_unread_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.minute_resolution": { + "name": "minute_resolution", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "minute_id": { + "name": "minute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_by_id": { + "name": "registered_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "minute_resolution_minute_idx": { + "name": "minute_resolution_minute_idx", + "columns": [ + { + "expression": "minute_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.minute": { + "name": "minute", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participants": { + "name": "participants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "occurred_on": { + "name": "occurred_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "minute_occurred_idx": { + "name": "minute_occurred_idx", + "columns": [ + { + "expression": "occurred_on", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.position_grant": { + "name": "position_grant", + "schema": "sys", + "columns": { + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object": { + "name": "object", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "position_grant_position_id_position_id_fk": { + "name": "position_grant_position_id_position_id_fk", + "tableFrom": "position_grant", + "tableTo": "position", + "schemaTo": "sys", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "position_grant_position_id_object_key_action_pk": { + "name": "position_grant_position_id_object_key_action_pk", + "columns": [ + "position_id", + "object_key", + "action" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.position": { + "name": "position", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "position_code_key": { + "name": "position_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.staff": { + "name": "staff", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account": { + "name": "account", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_id": { + "name": "position_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_account_key": { + "name": "staff_account_key", + "columns": [ + { + "expression": "account", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_position_idx": { + "name": "staff_position_idx", + "columns": [ + { + "expression": "position_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "staff_position_id_position_id_fk": { + "name": "staff_position_id_position_id_fk", + "tableFrom": "staff", + "tableTo": "position", + "schemaTo": "sys", + "columnsFrom": [ + "position_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.node_instance": { + "name": "node_instance", + "schema": "sys", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance": { + "name": "instance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "running": { + "name": "running", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "waiting": { + "name": "waiting", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "activated": { + "name": "activated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "terminal": { + "name": "terminal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_digested": { + "name": "failure_digested", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "node_instance_task_idx": { + "name": "node_instance_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "node_instance_workflow_id_node_id_instance_pk": { + "name": "node_instance_workflow_id_node_id_instance_pk", + "columns": [ + "workflow_id", + "node_id", + "instance" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.process_package": { + "name": "process_package", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "process_package_state_idx": { + "name": "process_package_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "process_package_id_version_pk": { + "name": "process_package_id_version_pk", + "columns": [ + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.workflow": { + "name": "workflow", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_version": { + "name": "package_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "initiation": { + "name": "initiation", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ports": { + "name": "ports", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "vars": { + "name": "vars", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_state_idx": { + "name": "workflow_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.action_intent": { + "name": "action_intent", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool": { + "name": "tool", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'drafted'" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "action_intent_task_idx": { + "name": "action_intent_task_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.report": { + "name": "report", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "staff_id": { + "name": "staff_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_from": { + "name": "window_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_to": { + "name": "window_to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft": { + "name": "draft", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_staff_period_idx": { + "name": "report_staff_period_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.task": { + "name": "task", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paradigm": { + "name": "paradigm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "terminal": { + "name": "terminal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "envelope": { + "name": "envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "caps": { + "name": "caps", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "used_steps": { + "name": "used_steps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "used_tokens": { + "name": "used_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "used_active_ms": { + "name": "used_active_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "used_human_calls": { + "name": "used_human_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_progress_at": { + "name": "last_progress_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "task_owner_idx": { + "name": "task_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "runtime", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_workflow_idx": { + "name": "task_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_terminal_idx": { + "name": "task_terminal_idx", + "columns": [ + { + "expression": "terminal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.replay_tool": { + "name": "replay_tool", + "schema": "sys", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "call": { + "name": "call", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.tool_allowlist": { + "name": "tool_allowlist", + "schema": "sys", + "columns": { + "activity_id": { + "name": "activity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool": { + "name": "tool", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_allowlist_activity_id_tool_pk": { + "name": "tool_allowlist_activity_id_tool_pk", + "columns": [ + "activity_id", + "tool" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.tool": { + "name": "tool", + "schema": "sys", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effect": { + "name": "effect", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "idempotent": { + "name": "idempotent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timeout_ms": { + "name": "timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "connection_keys": { + "name": "connection_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pure": { + "name": "pure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "owner_activity_id": { + "name": "owner_activity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_cipher": { + "name": "connection_cipher", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tool_effect_idx": { + "name": "tool_effect_idx", + "columns": [ + { + "expression": "effect", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_owner_idx": { + "name": "tool_owner_idx", + "columns": [ + { + "expression": "owner_activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.trigger_claim": { + "name": "trigger_claim", + "schema": "sys", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "sys.trigger": { + "name": "trigger", + "schema": "sys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "process_package_id": { + "name": "process_package_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mapping": { + "name": "mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trigger_enabled_idx": { + "name": "trigger_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 305a9ef..46cd90b 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1785089035016, "tag": "0007_swift_silver_samurai", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1785094481010, + "tag": "0008_strange_quasar", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/contracts/src/api/workbench.ts b/packages/contracts/src/api/workbench.ts index 7a6c62d..2aa4a88 100644 --- a/packages/contracts/src/api/workbench.ts +++ b/packages/contracts/src/api/workbench.ts @@ -7,7 +7,7 @@ import type { RouteContract } from './http.ts'; import { sessionSchema } from '../org.ts'; import { chatMessageSchema, deliverableSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts'; import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts'; -import { taskIdSchema } from '../id.ts'; +import { staffIdSchema, taskIdSchema } from '../id.ts'; export const sessionRoute = { method: 'GET', @@ -118,3 +118,136 @@ export const listMessagesRoute = { ), }), } as const satisfies RouteContract; + +/* ── 纪要:工作台五要素里唯一的主动动作 ─────────────────────────────── */ + +export const minuteResolutionSchema = z.object({ + id: z.string(), + text: z.string(), + ownerId: staffIdSchema.optional(), + state: z.enum(['open', 'registered']), + workflowId: z.string().optional(), + registeredById: staffIdSchema.optional(), + registeredAt: z.string().optional(), +}); + +export const minuteSchema = z.object({ + id: z.string(), + title: z.string(), + body: z.string(), + authorId: staffIdSchema, + authorName: z.string(), + participants: z.array(z.string()), + occurredOn: z.string(), + resolutions: z.array(minuteResolutionSchema), + createdAt: z.string(), + updatedAt: z.string(), +}); +export type MinuteView = z.infer; + +export const listMinutesRoute = { + method: 'GET', + path: '/api/minutes', + output: z.object({ minutes: z.array(minuteSchema) }), +} as const satisfies RouteContract; + +export const createMinuteRoute = { + method: 'POST', + path: '/api/minutes', + body: z.object({ + title: z.string().min(1), + body: z.string(), + occurredOn: z.string().min(1), + participants: z.array(staffIdSchema).default([]), + resolutions: z.array(z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() })).default([]), + }), + output: minuteSchema, +} as const satisfies RouteContract; + +export const appendResolutionRoute = { + method: 'POST', + path: '/api/minutes/:minuteId/resolutions', + params: z.object({ minuteId: z.string() }), + body: z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() }), + output: minuteSchema, +} as const satisfies RouteContract; + +/** 决议 → 登记起单:登记人即来源 */ +export const registerResolutionRoute = { + method: 'POST', + path: '/api/minutes/resolutions/:resolutionId/register', + params: z.object({ resolutionId: z.string() }), + body: z.object({ processPackageId: z.string().min(1), data: z.record(z.string(), z.unknown()) }), + output: z.object({ workflowId: z.string(), resolutionId: z.string() }), +} as const satisfies RouteContract; + +/* ── 汇报:系统代劳的自动产出 ───────────────────────────────────────── */ + +export const reportPeriodSchema = z.enum(['day', 'week', 'month', 'year']); +export type ReportPeriodName = z.infer; + +export const REPORT_PERIOD_LABEL: Readonly> = { + day: '日报', + week: '周报', + month: '月报', + year: '年报', +}; + +export const reportMetricsSchema = z.object({ + autonomy: z.number(), + correction: z.number(), + effectiveAutonomy: z.number(), + interruption: z.number(), + breakpoint: z.number(), + dmnCoverage: z.number(), + unitCost: z.object({ tokens: z.number(), minutes: z.number(), humanCalls: z.number() }), + terminalMix: z.record(z.string(), z.number()), +}); + +export const reportSchema = z.object({ + id: z.string(), + period: reportPeriodSchema, + title: z.string(), + window: z.object({ from: z.string(), to: z.string() }), + sections: z.array( + z.object({ + taskId: z.string(), + title: z.string(), + activityId: z.string(), + terminalLabel: z.string().optional(), + lines: z.array(z.string()), + }), + ), + metrics: reportMetricsSchema, + counts: z.object({ touched: z.number().int(), released: z.number().int(), logLines: z.number().int() }), + note: z.string().optional(), + submittedAt: z.string().optional(), + generatedAt: z.string(), +}); +export type ReportView = z.infer; + +/** 未提交的草案每次取回都重算 —— 汇报是投影,不是账本 */ +export const draftReportRoute = { + method: 'GET', + path: '/api/reports/:period', + params: z.object({ period: reportPeriodSchema }), + output: reportSchema, +} as const satisfies RouteContract; + +export const listReportsRoute = { + method: 'GET', + path: '/api/reports', + output: z.object({ reports: z.array(reportSchema) }), +} as const satisfies RouteContract; + +/** 确认即提交,提交后不再改 */ +export const submitReportRoute = { + method: 'POST', + path: '/api/reports/:reportId/submit', + params: z.object({ reportId: z.string() }), + body: z.object({ note: z.string().optional() }), + output: reportSchema, +} as const satisfies RouteContract; + +export type ListMinutesOutput = z.infer; +export type ListReportsOutput = z.infer;